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}'";
}