using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// /// Default . Loads the owning equipment's current effective-name /// set — references (DisplayNameOverride else the backing raw tag's Name), /// VirtualTags (Name), and ScriptedAlarms (Name) — and reports the first collision /// with a proposed name. Comparison is to match the /// {EquipmentId}/{EffectiveName} NodeId semantics and the deploy-time /// DraftValidator UnsEffectiveNameCollision rule. /// /// /// Each call creates and disposes its own pooled context — the same per-call pattern /// uses — so the guard is safe to register Scoped. /// public sealed class EffectiveNameGuard(IDbContextFactory dbFactory) : IEffectiveNameGuard { /// public async Task CheckAsync( string equipmentId, string proposedEffectiveName, EffectiveNameSourceKind kind, string? excludeSourceId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); // References for this equipment. The effective name is the display-name override else the // backing raw tag's current Name, so the two sets (references + tags) are loaded and joined // in memory — the per-equipment set is small and this mirrors the deploy rule exactly // (a reference whose backing tag is missing contributes no effective name, so it is skipped). var references = await db.UnsTagReferences .AsNoTracking() .Where(r => r.EquipmentId == equipmentId) .Select(r => new { r.UnsTagReferenceId, r.TagId, r.DisplayNameOverride }) .ToListAsync(ct); var tagIds = references.Select(r => r.TagId).Distinct(StringComparer.Ordinal).ToList(); var tagNameById = (await db.Tags .AsNoTracking() .Where(t => tagIds.Contains(t.TagId)) .Select(t => new { t.TagId, t.Name }) .ToListAsync(ct)) .GroupBy(t => t.TagId, StringComparer.Ordinal) .ToDictionary(g => g.Key, g => g.First().Name, StringComparer.Ordinal); foreach (var r in references) { if (IsSelf(EffectiveNameSourceKind.Reference, r.UnsTagReferenceId, kind, excludeSourceId)) continue; var effective = r.DisplayNameOverride ?? (tagNameById.TryGetValue(r.TagId, out var n) ? n : null); if (Collides(effective, proposedEffectiveName)) return Message(proposedEffectiveName, "reference", r.UnsTagReferenceId, equipmentId); } var virtualTags = await db.VirtualTags .AsNoTracking() .Where(v => v.EquipmentId == equipmentId) .Select(v => new { v.VirtualTagId, v.Name }) .ToListAsync(ct); foreach (var v in virtualTags) { if (IsSelf(EffectiveNameSourceKind.VirtualTag, v.VirtualTagId, kind, excludeSourceId)) continue; if (Collides(v.Name, proposedEffectiveName)) return Message(proposedEffectiveName, "VirtualTag", v.VirtualTagId, equipmentId); } var scriptedAlarms = await db.ScriptedAlarms .AsNoTracking() .Where(a => a.EquipmentId == equipmentId) .Select(a => new { a.ScriptedAlarmId, a.Name }) .ToListAsync(ct); foreach (var a in scriptedAlarms) { if (IsSelf(EffectiveNameSourceKind.ScriptedAlarm, a.ScriptedAlarmId, kind, excludeSourceId)) continue; if (Collides(a.Name, proposedEffectiveName)) return Message(proposedEffectiveName, "ScriptedAlarm", a.ScriptedAlarmId, equipmentId); } return null; } /// True when a row is the self-row being edited — same kind AND same logical id — so it /// is excluded from the collision set (a rename / override-change of a row cannot collide with itself). private static bool IsSelf( EffectiveNameSourceKind rowKind, string rowId, EffectiveNameSourceKind editedKind, string? excludeSourceId) => excludeSourceId is not null && rowKind == editedKind && string.Equals(rowId, excludeSourceId, StringComparison.Ordinal); private static bool Collides(string? existing, string proposed) => existing is not null && string.Equals(existing, proposed, StringComparison.Ordinal); private static string Message(string name, string sourceLabel, string sourceId, string equipmentId) => $"effective name '{name}' already used by {sourceLabel} '{sourceId}' in equipment '{equipmentId}'"; }