87 lines
4.0 KiB
C#
87 lines
4.0 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// Loads the <see cref="EquipmentNamespaceContent"/> snapshot the
|
||
/// <see cref="EquipmentNodeWalker"/> 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.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para>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.</para>
|
||
///
|
||
/// <para>Returns <c>null</c> when the driver instance has no Equipment rows at the
|
||
/// supplied generation. The wire-in in <see cref="OpcUaApplicationHost"/> 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).</para>
|
||
/// </remarks>
|
||
public sealed class EquipmentNamespaceContentLoader
|
||
{
|
||
private readonly OtOpcUaConfigDbContext _db;
|
||
|
||
public EquipmentNamespaceContentLoader(OtOpcUaConfigDbContext db)
|
||
{
|
||
_db = db;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Load the walker-shaped snapshot for <paramref name="driverInstanceId"/> at
|
||
/// <paramref name="generationId"/>. Returns <c>null</c> when the driver has no
|
||
/// Equipment rows at that generation.
|
||
/// </summary>
|
||
public async Task<EquipmentNamespaceContent?> 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);
|
||
}
|
||
}
|