feat(v3-batch4-wp3): raw-only binding + UNS fan-out + write routing
Wave B of Batch 4 — the runtime binding seam for the dual namespace. Applier (both realms, explicit realm at every sink call site): - MaterialiseRawSubtree: Raw containers as folders + Raw tags as variables keyed by RawPath (native-alarm tag → single Part 9 condition at the RawPath), all in AddressSpaceRealm.Raw; historian tagname = override else RawPath. - MaterialiseUnsReferences: each UNS reference Variable under its equipment folder (Uns realm) + an Organizes UNS->Raw edge; inherits writable/array/ historian tagname from the backing raw tag (both NodeIds -> one tagname). - FeedHistorizedRefs / ProvisionHistorizedTags now source RAW tags (mux ref stays single, keyed by RawPath); ApplyPureRemove tears down raw tags + UNS refs in place (raw-container removal falls back to rebuild). DriverHostActor (dual-NodeId, single-source fan-out): - _nodeIdByDriverRef value gains a realm (NodeRealmRef); rebuilt from RawTags UNION UnsReferenceVariables so one (DriverInstanceId, RawPath) fans to the raw NodeId AND every referencing UNS NodeId with identical value/quality/timestamp. Write inverse map keyed by the bare id; the ns-qualified NodeId the write hook passes is normalised (BareNodeId) so a write to either NodeId resolves the same driver ref (-> RawPath write). - Native raw alarm condition routing is realm-tagged (Raw); AttributeValueUpdate + AlarmStateUpdate carry the realm through to the sink. Retire EquipmentNodeIds -> V3NodeIds.Uns (applier/VirtualTagHostActor) and RawPaths.Combine (DiscoveredNodeMapper, discovered nodes are Raw now). DeploymentArtifact.ParseComposition emits the Raw + UNS subtrees byte-parity with the composer (reconstruct entities -> AddressSpaceComposer.Compose). Sink surface: removed the transitional `= AddressSpaceRealm.Uns` defaults from IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink / SdkAddressSpaceSink / DeferredAddressSpaceSink / NullOpcUaAddressSpaceSink — every call site is now explicit (realm reordered before the trailing optionals on EnsureVariable + MaterialiseAlarmCondition). Node-manager convenience methods keep their defaults (they are not the interface impl; Sdk delegates explicitly). Tests: rewrote DriverHostActorLiveValueTests (fan-out drift, 1:N) + DriverHostActorWriteRoutingTests (dual-NodeId raw/uns write routing) to the v3 raw+uns model; new AddressSpaceApplierRawUnsTests; migrated the EquipmentTags provisioning/feed tests to RawTags; swept EquipmentNodeIds test callers. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
+143
-84
@@ -18,20 +18,20 @@ using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the equipment-tag <b>live-value routing</b> wired into <see cref="DriverHostActor"/>:
|
||||
/// a driver publishes a value keyed by its wire-ref <c>FullName</c>, but the equipment variable was
|
||||
/// materialised under a FOLDER-SCOPED NodeId (<c>{equipmentId}/{folderPath}/{name}</c>). After an
|
||||
/// apply, the host's <c>_nodeIdByDriverRef</c> map resolves <c>(DriverInstanceId, FullName)</c> to
|
||||
/// that folder-scoped NodeId, so <c>ForwardToMux</c> lands the value on the right node (and still
|
||||
/// forwards the raw value to the dependency mux for VirtualTag inputs).
|
||||
///
|
||||
/// v3 Batch 4 (B4-WP3) — the driver <b>single-source fan-out</b> wired into
|
||||
/// <see cref="DriverHostActor"/>. The driver publishes a value keyed by its wire-ref (== the tag's
|
||||
/// <c>RawPath</c>); the host's <c>_nodeIdByDriverRef</c> map — rebuilt each apply from the composition's
|
||||
/// <c>RawTags</c> ∪ <c>UnsReferenceVariables</c> — resolves <c>(DriverInstanceId, RawPath)</c> to the RAW
|
||||
/// NodeId AND every referencing UNS NodeId, so <c>ForwardToMux</c> lands ONE publish on the raw node
|
||||
/// (<see cref="AddressSpaceRealm.Raw"/>) plus every UNS node (<see cref="AddressSpaceRealm.Uns"/>) with
|
||||
/// IDENTICAL value/quality/timestamp (no independent buffer ⇒ no drift). The raw value is still forwarded
|
||||
/// to the dependency mux (VirtualTag inputs), keyed by the RawPath.
|
||||
/// <para>
|
||||
/// Drives a real apply through the existing harness: the seeded artifact carries the
|
||||
/// <c>Namespaces</c> / <c>DriverInstances</c> / <c>Tags</c> arrays that
|
||||
/// <c>DeploymentArtifact.BuildEquipmentTagPlans</c> needs to project equipment tags, and a
|
||||
/// <c>DispatchDeployment</c> triggers the <c>ApplyAndAck → PushDesiredSubscriptions</c> pass
|
||||
/// that builds the map. The OPC UA sink and the dependency mux are injected as
|
||||
/// <see cref="Akka.TestKit.TestProbe"/>s.
|
||||
/// Drives a real apply through the existing harness: the seeded artifact carries the v3 raw-tag chain
|
||||
/// (RawFolder → DriverInstance(RawFolderId) → Device → Tag) + an optional UnsTagReference projecting
|
||||
/// each raw tag into an equipment. <c>DispatchDeployment</c> triggers the
|
||||
/// <c>ApplyAndAck → PushDesiredSubscriptions</c> pass that builds the map; the sink and mux are
|
||||
/// injected as <see cref="Akka.TestKit.TestProbe"/>s.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
@@ -40,55 +40,95 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
|
||||
private static readonly DateTime Ts = new(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>A driver value published by FullName lands on the equipment variable's folder-scoped
|
||||
/// NodeId (here <c>eq-1/speed</c>, no sub-folder), carrying the value/quality/timestamp.</summary>
|
||||
[Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
|
||||
public void Driver_value_routes_to_folder_scoped_equipment_NodeId()
|
||||
/// <summary>A driver value published by RawPath fans to the RAW node AND the referencing UNS node, each
|
||||
/// carrying identical value/quality/timestamp — the fan-out drift guard.</summary>
|
||||
[Fact]
|
||||
public void Driver_value_fans_out_to_raw_and_uns_NodeIds_with_identical_value()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
// One equipment tag: eq-1, drv-1, FullName "40001", no folder, Name "speed".
|
||||
var deploymentId = SeedDeploymentWithEquipmentTags(db, RevA,
|
||||
(Equip: "eq-1", Driver: "drv-1", FullName: "40001", Folder: null, Name: "speed"));
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[] { (Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false) },
|
||||
unsRefs: new[] { (Area: "filling", Line: "line1", Equip: "station1", BackingTag: "speed", Effective: (string?)null) });
|
||||
|
||||
var (actor, publish, mux) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
// The driver publishes by its wire-ref RawPath.
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "40001", 42.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 42.0, OpcUaQuality.Good, Ts));
|
||||
|
||||
var update = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
update.NodeId.ShouldBe("eq-1/speed");
|
||||
update.Value.ShouldBe(42.0);
|
||||
update.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
update.TimestampUtc.ShouldBe(Ts);
|
||||
var a = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
var b = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
var byId = new[] { a, b }.ToDictionary(u => u.NodeId);
|
||||
|
||||
// The raw value is still forwarded to the dependency mux (VirtualTag inputs, keyed by FullName).
|
||||
byId.Keys.ShouldBe(new[] { "Plant/Modbus/dev1/speed", "filling/line1/station1/speed" }, ignoreOrder: true);
|
||||
// Realm travels with each update so the sink resolves the right namespace.
|
||||
byId["Plant/Modbus/dev1/speed"].Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
byId["filling/line1/station1/speed"].Realm.ShouldBe(AddressSpaceRealm.Uns);
|
||||
// Fan-out drift guard: identical value + quality + timestamp on BOTH node states.
|
||||
foreach (var u in byId.Values)
|
||||
{
|
||||
u.Value.ShouldBe(42.0);
|
||||
u.Quality.ShouldBe(OpcUaQuality.Good);
|
||||
u.TimestampUtc.ShouldBe(Ts);
|
||||
}
|
||||
|
||||
// The raw publish still reaches the dependency mux (VirtualTag inputs), keyed by RawPath.
|
||||
mux.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(5))
|
||||
.FullReference.ShouldBe("40001");
|
||||
.FullReference.ShouldBe("Plant/Modbus/dev1/speed");
|
||||
}
|
||||
|
||||
/// <summary>The same driver ref backing two equipments fans out: a single publish produces one
|
||||
/// <see cref="OpcUaPublishActor.AttributeValueUpdate"/> per equipment variable NodeId.</summary>
|
||||
[Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
|
||||
public void Same_ref_on_two_equipments_fans_out_to_both_NodeIds()
|
||||
/// <summary>A raw tag with NO UNS reference fans to ONLY its raw NodeId (Raw realm).</summary>
|
||||
[Fact]
|
||||
public void Unreferenced_raw_tag_fans_to_raw_node_only()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
// Same (drv-1, "40001") wire-ref backs eq-1/speed AND eq-2/speed (identical machines).
|
||||
var deploymentId = SeedDeploymentWithEquipmentTags(db, RevA,
|
||||
(Equip: "eq-1", Driver: "drv-1", FullName: "40001", Folder: null, Name: "speed"),
|
||||
(Equip: "eq-2", Driver: "drv-1", FullName: "40001", Folder: null, Name: "speed"));
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[] { (Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false) },
|
||||
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
||||
|
||||
var (actor, publish, _) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "40001", 7.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 5.0, OpcUaQuality.Good, Ts));
|
||||
|
||||
// One publish → two updates. Assert the SET of NodeIds (order is not contractual).
|
||||
var first = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
var second = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
new[] { first.NodeId, second.NodeId }.ShouldBe(
|
||||
new[] { "eq-1/speed", "eq-2/speed" }, ignoreOrder: true);
|
||||
first.Value.ShouldBe(7.0);
|
||||
second.Value.ShouldBe(7.0);
|
||||
var only = publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5));
|
||||
only.NodeId.ShouldBe("Plant/Modbus/dev1/speed");
|
||||
only.Realm.ShouldBe(AddressSpaceRealm.Raw);
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
|
||||
}
|
||||
|
||||
/// <summary>A raw tag referenced by TWO equipments fans to the raw node PLUS both UNS nodes (3 updates,
|
||||
/// identical value) — the 1:N fan-out.</summary>
|
||||
[Fact]
|
||||
public void Raw_tag_referenced_by_two_equipments_fans_to_raw_plus_both_uns()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[] { (Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false) },
|
||||
unsRefs: new[]
|
||||
{
|
||||
(Area: "filling", Line: "line1", Equip: "station1", BackingTag: "speed", Effective: (string?)null),
|
||||
(Area: "filling", Line: "line1", Equip: "station2", BackingTag: "speed", Effective: (string?)null),
|
||||
});
|
||||
|
||||
var (actor, publish, _) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "Plant/Modbus/dev1/speed", 7.0, OpcUaQuality.Good, Ts));
|
||||
|
||||
var updates = new[]
|
||||
{
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5)),
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5)),
|
||||
publish.ExpectMsg<OpcUaPublishActor.AttributeValueUpdate>(TimeSpan.FromSeconds(5)),
|
||||
};
|
||||
updates.Select(u => u.NodeId).ShouldBe(new[]
|
||||
{
|
||||
"Plant/Modbus/dev1/speed",
|
||||
"filling/line1/station1/speed",
|
||||
"filling/line1/station2/speed",
|
||||
}, ignoreOrder: true);
|
||||
updates.ShouldAllBe(u => (double)u.Value! == 7.0);
|
||||
}
|
||||
|
||||
/// <summary>A value for a ref not in the composition is NOT pushed to the OPC UA sink (no matching
|
||||
@@ -97,25 +137,23 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
public void Unmatched_ref_is_dropped_from_sink_but_still_forwarded_to_mux()
|
||||
{
|
||||
var db = NewInMemoryDbFactory();
|
||||
var deploymentId = SeedDeploymentWithEquipmentTags(db, RevA,
|
||||
(Equip: "eq-1", Driver: "drv-1", FullName: "40001", Folder: null, Name: "speed"));
|
||||
var deploymentId = SeedV3Deployment(db, RevA,
|
||||
rawTags: new[] { (Driver: "drv-1", DriverName: "Modbus", Device: "dev1", Tag: "speed", DataType: "Double", Writable: false) },
|
||||
unsRefs: Array.Empty<(string, string, string, string, string?)>());
|
||||
|
||||
var (actor, publish, mux) = SpawnHostAndApply(db, deploymentId);
|
||||
|
||||
actor.Tell(new DriverInstanceActor.AttributeValuePublished(
|
||||
"drv-1", "59999", 99.0, OpcUaQuality.Good, Ts));
|
||||
"drv-1", "Plant/Modbus/dev1/nope", 99.0, OpcUaQuality.Good, Ts));
|
||||
|
||||
// No equipment-tag NodeId for ("drv-1","59999") → nothing reaches the sink.
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
// The raw publish still reaches the dependency mux.
|
||||
mux.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(5))
|
||||
.FullReference.ShouldBe("59999");
|
||||
.FullReference.ShouldBe("Plant/Modbus/dev1/nope");
|
||||
}
|
||||
|
||||
/// <summary>Spawns the host with publish + mux probes, dispatches the deployment, and waits for the
|
||||
/// Applied ACK so the apply (and thus the map build in PushDesiredSubscriptions) has completed
|
||||
/// before the test publishes a value. A VirtualTag-host probe is injected so the real host isn't
|
||||
/// spawned (it would consume the mux-forwarded publishes otherwise — see SpawnVirtualTagHost).</summary>
|
||||
/// Applied ACK so the map build in PushDesiredSubscriptions has completed before the test publishes a
|
||||
/// value. A VirtualTag-host probe is injected so the real host isn't spawned.</summary>
|
||||
private (IActorRef Actor, Akka.TestKit.TestProbe Publish, Akka.TestKit.TestProbe Mux) SpawnHostAndApply(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, DeploymentId deploymentId)
|
||||
{
|
||||
@@ -134,47 +172,68 @@ public sealed class DriverHostActorLiveValueTests : RuntimeActorTestBase
|
||||
|
||||
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
|
||||
coordinator.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
|
||||
|
||||
// RebuildAddressSpace also lands on the publish probe during apply; drain it so the test's
|
||||
// ExpectMsg<AttributeValueUpdate> assertions see only value updates.
|
||||
publish.ExpectMsg<OpcUaPublishActor.RebuildAddressSpace>(TimeSpan.FromSeconds(5));
|
||||
|
||||
return (actor, publish, mux);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a Sealed deployment whose artifact carries the minimal arrays
|
||||
/// <c>DeploymentArtifact.BuildEquipmentTagPlans</c> needs to project equipment tags:
|
||||
/// <c>Namespaces</c> (one Equipment-kind ns), <c>DriverInstances</c> (each driver bound to that
|
||||
/// ns), and <c>Tags</c> (each with a non-null EquipmentId + a TagConfig blob carrying FullName).
|
||||
/// </summary>
|
||||
private static DeploymentId SeedDeploymentWithEquipmentTags(
|
||||
/// <summary>Seeds a Sealed deployment whose artifact carries the v3 raw-tag chain (RawFolder "Plant" →
|
||||
/// DriverInstance(RawFolderId) → Device → Tag) plus optional UnsTagReferences projecting a raw tag into an
|
||||
/// Area/Line/Equipment. Each raw tag's RawPath is <c>Plant/{DriverName}/{Device}/{Tag}</c>; each UNS
|
||||
/// reference's NodeId is <c>{Area}/{Line}/{Equip}/{Effective ?? backing tag Name}</c>. Enums serialize
|
||||
/// numerically (AccessLevel: ReadWrite = 1).</summary>
|
||||
private static DeploymentId SeedV3Deployment(
|
||||
IDbContextFactory<OtOpcUaConfigDbContext> db, RevisionHash rev,
|
||||
params (string Equip, string Driver, string FullName, string? Folder, string Name)[] tags)
|
||||
(string Driver, string DriverName, string Device, string Tag, string DataType, bool Writable)[] rawTags,
|
||||
(string Area, string Line, string Equip, string BackingTag, string? Effective)[] unsRefs)
|
||||
{
|
||||
var driverIds = tags.Select(t => t.Driver).Distinct(StringComparer.Ordinal).ToArray();
|
||||
var drivers = rawTags
|
||||
.Select(t => (t.Driver, t.DriverName))
|
||||
.Distinct()
|
||||
.Select(d => new { DriverInstanceId = d.Driver, RawFolderId = "rf-plant", Name = d.DriverName, DriverType = "Modbus", DriverConfig = "{}", ClusterId = "c1", Enabled = false })
|
||||
.ToArray();
|
||||
var devices = rawTags
|
||||
.Select(t => (t.Driver, t.Device))
|
||||
.Distinct()
|
||||
.Select(d => new { DeviceId = $"{d.Driver}:{d.Device}", DriverInstanceId = d.Driver, Name = d.Device, DeviceConfig = "{}" })
|
||||
.ToArray();
|
||||
var tags = rawTags.Select(t => new
|
||||
{
|
||||
TagId = t.Tag, // tag id == name for test simplicity (unique per driver here)
|
||||
DeviceId = $"{t.Driver}:{t.Device}",
|
||||
TagGroupId = (string?)null,
|
||||
Name = t.Tag,
|
||||
DataType = t.DataType,
|
||||
AccessLevel = t.Writable ? 1 : 0, // TagAccessLevel.ReadWrite = 1
|
||||
TagConfig = "{}",
|
||||
}).ToArray();
|
||||
|
||||
// UNS topology: one area/line per distinct (Area, Line); one equipment per (Area, Line, Equip).
|
||||
var areas = unsRefs.Select(r => r.Area).Distinct(StringComparer.Ordinal)
|
||||
.Select(a => new { UnsAreaId = $"a-{a}", Name = a, ClusterId = "c1" }).ToArray();
|
||||
var lines = unsRefs.Select(r => (r.Area, r.Line)).Distinct()
|
||||
.Select(l => new { UnsLineId = $"l-{l.Area}-{l.Line}", UnsAreaId = $"a-{l.Area}", Name = l.Line }).ToArray();
|
||||
var equipment = unsRefs.Select(r => (r.Area, r.Line, r.Equip)).Distinct()
|
||||
.Select(e => new { EquipmentId = $"EQ-{e.Area}-{e.Line}-{e.Equip}", UnsLineId = $"l-{e.Area}-{e.Line}", Name = e.Equip, MachineCode = e.Equip }).ToArray();
|
||||
var unsTagReferences = unsRefs.Select((r, i) => new
|
||||
{
|
||||
UnsTagReferenceId = $"ref-{i}",
|
||||
EquipmentId = $"EQ-{r.Area}-{r.Line}-{r.Equip}",
|
||||
TagId = r.BackingTag,
|
||||
DisplayNameOverride = r.Effective,
|
||||
}).ToArray();
|
||||
|
||||
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
Namespaces = new[]
|
||||
{
|
||||
new { NamespaceId = "ns-eq", Kind = 0 }, // NamespaceKind.Equipment = 0
|
||||
},
|
||||
DriverInstances = driverIds.Select(d => new
|
||||
{
|
||||
DriverInstanceId = d,
|
||||
NamespaceId = "ns-eq",
|
||||
}).ToArray(),
|
||||
Tags = tags.Select((t, i) => new
|
||||
{
|
||||
TagId = $"tag-{i}",
|
||||
EquipmentId = t.Equip,
|
||||
DriverInstanceId = t.Driver,
|
||||
Name = t.Name,
|
||||
FolderPath = t.Folder,
|
||||
DataType = "Double",
|
||||
TagConfig = JsonSerializer.Serialize(new { FullName = t.FullName }),
|
||||
}).ToArray(),
|
||||
RawFolders = new[] { new { RawFolderId = "rf-plant", ParentRawFolderId = (string?)null, Name = "Plant", ClusterId = "c1" } },
|
||||
DriverInstances = drivers,
|
||||
Devices = devices,
|
||||
TagGroups = Array.Empty<object>(),
|
||||
Tags = tags,
|
||||
UnsAreas = areas,
|
||||
UnsLines = lines,
|
||||
Equipment = equipment,
|
||||
UnsTagReferences = unsTagReferences,
|
||||
});
|
||||
|
||||
var id = DeploymentId.NewId();
|
||||
|
||||
Reference in New Issue
Block a user