using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Core.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa; /// /// Loads the snapshot the /// consumes, scoped to a single /// (driverInstanceId, generationId) pair. Joins the four row sets the walker expects: /// UnsAreas for the driver's cluster, UnsLines under those areas, Equipment bound to /// this driver + its lines, and Tags bound to this driver + its equipment — all at the /// supplied generation. /// /// /// The walker is driver-instance-scoped (decisions #116–#121 put the UNS in the /// Equipment-kind namespace owned by one driver instance at a time), so this loader is /// too — a single call returns one driver's worth of rows, never the whole fleet. /// /// Returns null when the driver instance has no Equipment rows at the /// supplied generation. The wire-in in treats null as /// "this driver has no UNS content, skip the walker and let DiscoverAsync own the whole /// address space" — the backward-compat path for drivers whose namespace kind is not /// Equipment (Modbus / AB CIP / TwinCAT / FOCAS). /// public sealed class EquipmentNamespaceContentLoader { private readonly OtOpcUaConfigDbContext _db; public EquipmentNamespaceContentLoader(OtOpcUaConfigDbContext db) { _db = db; } /// /// Load the walker-shaped snapshot for at /// . Returns null when the driver has no /// Equipment rows at that generation. /// public async Task LoadAsync( string driverInstanceId, long generationId, CancellationToken ct) { var equipment = await _db.Equipment .AsNoTracking() .Where(e => e.DriverInstanceId == driverInstanceId && e.GenerationId == generationId && e.Enabled) .ToListAsync(ct).ConfigureAwait(false); if (equipment.Count == 0) return null; // Filter UNS tree to only the lines + areas that host at least one Equipment bound to // this driver — skips loading unrelated UNS branches from the cluster. LinesByArea // grouping is driven off the Equipment rows so an empty line (no equipment) doesn't // pull a pointless folder into the walker output. var lineIds = equipment.Select(e => e.UnsLineId).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); var lines = await _db.UnsLines .AsNoTracking() .Where(l => l.GenerationId == generationId && lineIds.Contains(l.UnsLineId)) .ToListAsync(ct).ConfigureAwait(false); var areaIds = lines.Select(l => l.UnsAreaId).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); var areas = await _db.UnsAreas .AsNoTracking() .Where(a => a.GenerationId == generationId && areaIds.Contains(a.UnsAreaId)) .ToListAsync(ct).ConfigureAwait(false); // Tags belonging to this driver at this generation. Walker skips Tags with null // EquipmentId (those are SystemPlatform-kind Galaxy tags per decision #120) but we // load them anyway so the same rowset can drive future non-Equipment-kind walks // without re-hitting the DB. Filtering here is a future optimization; today the // per-tag cost is bounded by driver scope. var tags = await _db.Tags .AsNoTracking() .Where(t => t.DriverInstanceId == driverInstanceId && t.GenerationId == generationId) .ToListAsync(ct).ConfigureAwait(false); return new EquipmentNamespaceContent( Areas: areas, Lines: lines, Equipment: equipment, Tags: tags); } }