46aba992c5
Filter ExternalIdReservations to WHERE ReleasedAt IS NULL so DraftSnapshot.ActiveReservations matches its documented semantics and ValidateReservationPreflight cannot emit spurious BadDuplicateExternalIdentifier errors from already-released rows. Adds a focused unit test seeding one active and one released reservation and asserting only the active row is returned.
52 lines
3.0 KiB
C#
52 lines
3.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
/// <summary>
|
|
/// Materialises a <see cref="DraftSnapshot"/> from the live config DB so
|
|
/// <see cref="DraftValidator"/> can run against the current edit state at deploy time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This is a whole-DB ("global") snapshot — every cluster's rows in one pass — which is
|
|
/// what the deploy path needs: the admin-operations actor snapshots and flattens the
|
|
/// entire config, not one cluster. The validator's rules compare entity-level
|
|
/// <c>ClusterId</c> fields against each other (e.g. namespace binding), so the snapshot's
|
|
/// own <see cref="DraftSnapshot.ClusterId"/> is not read by any rule and is left empty.
|
|
/// </para>
|
|
/// <para>
|
|
/// <see cref="DraftSnapshot.GenerationId"/> is a placeholder (the generation model was
|
|
/// dropped); no rule reads it. <see cref="DraftSnapshot.PriorEquipment"/> is empty because
|
|
/// there is no prior-generation table to diff against. <see cref="DraftSnapshot.Enterprise"/>
|
|
/// / <see cref="DraftSnapshot.Site"/> are left null so the path-length rule uses its
|
|
/// conservative upper bound.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class DraftSnapshotFactory
|
|
{
|
|
/// <summary>Builds a <see cref="DraftSnapshot"/> from the current config DB rows.</summary>
|
|
/// <param name="db">The config DB context to read from.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>A snapshot populated from the live DB, ready for <see cref="DraftValidator.Validate"/>.</returns>
|
|
public static async Task<DraftSnapshot> FromConfigDbAsync(OtOpcUaConfigDbContext db, CancellationToken ct = default)
|
|
=> new DraftSnapshot
|
|
{
|
|
GenerationId = 0, // generation model dropped; placeholder (no rule reads it)
|
|
ClusterId = string.Empty, // global snapshot; rules compare entity ClusterId fields, not this
|
|
Namespaces = await db.Namespaces.AsNoTracking().ToListAsync(ct),
|
|
DriverInstances = await db.DriverInstances.AsNoTracking().ToListAsync(ct),
|
|
Devices = await db.Devices.AsNoTracking().ToListAsync(ct),
|
|
UnsAreas = await db.UnsAreas.AsNoTracking().ToListAsync(ct),
|
|
UnsLines = await db.UnsLines.AsNoTracking().ToListAsync(ct),
|
|
Equipment = await db.Equipment.AsNoTracking().ToListAsync(ct),
|
|
Tags = await db.Tags.AsNoTracking().ToListAsync(ct),
|
|
VirtualTags = await db.VirtualTags.AsNoTracking().ToListAsync(ct),
|
|
PollGroups = await db.PollGroups.AsNoTracking().ToListAsync(ct),
|
|
PriorEquipment = [],
|
|
ActiveReservations = await db.ExternalIdReservations
|
|
.AsNoTracking()
|
|
.Where(r => r.ReleasedAt == null) // active only — matches DraftSnapshot.ActiveReservations semantics
|
|
.ToListAsync(ct),
|
|
};
|
|
}
|