Adopt the fluent builder's node-creation surface in the remaining hand-rolled node managers - #4460
Conversation
FlatTagNodeManager, HaSampleNodeManager and the pump sample's OpenUSD composition each hand-built their instances, minted NodeIds by string concatenation and kept their own externalReferences[ObjectsFolder] entry. All three now derive from (or already derived from) FluentNodeManagerBase and stage their nodes through the builder's Add* surface, so registration and the reverse-reference pass do that bookkeeping instead. The two samples whose identifiers are part of their contract keep them: the aggregating client's Thing Descriptions spell out s=Pump1.Identification.Manufacturer, and the redundant client addresses ns=N;s=Counter directly. Both managers therefore override New to mint the browse path rather than take the default factory's hashed identifier. Each override reproduces that factory's keep-an-existing-id rule, without which NodeState.Create(assignNodeIds: true) silently moves a node the caller named explicitly - the supervision conditions are created that way. FlatTagNodeManager keeps its EventNotifier bits and its root-notifier registration: the pump is the notifier an aggregating server subscribes to, and OPC 10000-3 only delivers the supervision conditions to a client that can reach one. SupervisionSignal now derives its namespace and identifier prefix from the supervision Object it hangs off instead of taking them as parameters. Two candidates named in the issue are deliberately untouched. PubSubNodeManager rebuilds its dynamic subtree from runtime Method calls (AddDataSetFolder, AddPushTarget, AddSecurityGroup) and from ConfigurationChanged, so it needs a mechanism that works after startup - the same reason the DI device builders are out of scope - and its parents belong to the diagnostics node manager, which the staging surface refuses as unknown. UsdSceneMaterializer is a public ISystemContext extension that hands back a detached subtree for the caller to register; no builder is reachable at its call sites, and it already mints through the same NodeId factory the staged path uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code coverage✅ Coverage gate passed.
Coverage is above the recorded baseline - consider ratcheting Thresholds live in |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #4460 +/- ##
=======================================
Coverage 80.98% 80.98%
=======================================
Files 2050 2050
Lines 300775 300775
Branches 52050 52050
=======================================
+ Hits 243582 243589 +7
+ Misses 39209 39206 -3
+ Partials 17984 17980 -4
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
The fluent-staging conversions follow the expected RegisterAuthoredNodesAsync → CompleteConfigureAsync → SealConfigurationAsync lifecycle and preserve the samples’ published NodeId/address-space contracts.
Pull request overview
This PR completes adoption of the new fluent INodeManagerBuilder node-creation surface across the remaining sample node managers that were still hand-rolling NodeState creation, while preserving each sample’s published NodeId/address-space contract.
Changes:
- Converted
FlatTagNodeManagerandHaSampleNodeManagerto derive fromFluentNodeManagerBase, stage nodes viaNodeManagerBuilder, and register viaRegisterAuthoredNodesAsync+CompleteConfigureAsync+SealConfigurationAsync. - Added
New(ISystemContext, NodeState)overrides to keep sample-contract NodeIds stable (browse-name / dotted-path identifiers instead of default hashed ids). - Updated DI Pump sample OpenUSD composition to stage authored subtrees on the fluent builder during
ConfigureAsyncrather than registering them immediately.
File summaries
| File | Description |
|---|---|
| samples/WotCon/FlatTagServer/SupervisionSignal.cs | Derives supervision tag/alarm NodeId namespace + prefix from the parent supervision object to match staged creation. |
| samples/WotCon/FlatTagServer/FlatTagNodeManager.cs | Migrates to FluentNodeManagerBase and fluent staging; overrides New to preserve dotted-path NodeIds and replaces manual externalReferences maintenance with CompleteConfigureAsync. |
| samples/Redundancy/RedundantServer/HaSampleNodeManager.cs | Migrates to FluentNodeManagerBase and fluent staging; overrides New to preserve browse-name NodeIds used by the redundant client. |
| samples/DI/PumpDeviceIntegrationServer/PumpNodeManager.cs | Threads the fluent builder into the async configure phase so OpenUSD composition can stage authored nodes instead of registering them immediately. |
| samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs | Converts two materialization paths to stage nodes on the builder (sync) rather than calling AddPredefinedNodeAsync directly. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Conflict in HaSampleNodeManager.CreateAddressSpaceAsync, where #4445 added AddFactoryAssignedNodes next to the HistoryEvents object this branch had converted to the staged builder. Both are kept: the factory-assigned subtree stays hand-built and unstaged on purpose, because it exists to show what an identity minted straight through NodeIdFactory looks like beside the sample's named ones, and it is attached after the folder has been staged so those identifiers survive untouched. The merge also exposed a defect in this branch's New override. Naming every node after its browse name is too broad: a browse name is unique only among its siblings, and the historian hangs an "HA Configuration" object off each node it historizes, so the Counter's and the HistoryEvents' one both landed on ns=2;s=HA Configuration and the strong active/passive replicas refused to start. The override is now limited to the sample folder and its direct children - the nodes the redundant client spells out - and everything else keeps the default factory's derived identifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #4429.
PR #4428 added the node-creation surface to
INodeManagerBuilder. This picks up the remaining candidates that needed a base-class change first, and reports on the two that turn out not to be candidates at all.Converted
samples/WotCon/FlatTagServer/FlatTagNodeManager.cs— now derives fromFluentNodeManagerBase. The privateCreateObject/CreateVariable/CreateMethodhelpers are gone, along with everypumpNodeId + ".Operational.Measurements.MassFlow"concatenation; nodes are staged withAddObject/AddVariable<T>/AddMethodand the hand-maintainedexternalReferences[ObjectsFolder]entry is replaced byCompleteConfigureAsync.The
EventNotifierbits andAddRootNotifierAsync(pump)are kept verbatim — the pump is the notifier an aggregating server subscribes to, and OPC 10000-3 only delivers the supervision conditions to a client that can reach one.SupervisionSignalloses itsnamespaceIndexandtagPathparameters and derives both from the supervision Object it hangs off, so the signal follows wherever that Object was staged.samples/Redundancy/RedundantServer/HaSampleNodeManager.cs— same shape. TheCounterstill reachesEnableDistributedValueParticipationthroughINodeBuilder.Node. The variables keep theirOrganizesreference type, which the fluent default would otherwise have madeHasComponent.samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs— bothAddPredefinedNodeAsyncsites stage on the builder now, threaded throughConfigureAsync;DiNodeManager.CreateAddressSpaceAsyncalready callsRegisterAuthoredNodesAsyncbetweenConfigureAsyncand the reverse-reference pass, so no new plumbing was needed. Both methods became synchronous. The NodeIds are unchanged because the handSystemContext.NodeIdFactory.New(...)calls andAssignInstanceChildNodeIdswere already doing exactly what staging does.CreateRepresentedComponentis left alone: it sits onCreatePumpAsync, a public runtime API, and so is out of scope for the same reason as the DI device builders.NodeIds are part of two samples' contracts
The issue expected the staged path to mint through the manager's
INodeIdFactory, and it does — but for two of these samples the identifiers are a published contract, not an implementation detail:samples/WotCon/AggregationClient/Documents/spell outnsu=…SourceA;s=Pump1.Identification.Manufacturer;samples/Redundancy/RedundantClient/Program.csaddressesnew NodeId("Counter", ns)directly.Both managers therefore override
Newfor those nodes instead of taking the default factory's derived identifier, which satisfies the acceptance criterion that each manager keeps its current address-space shape. FlatTag mints the full dotted browse path; the HA sample mints the bare browse name, but only for the sample folder and its direct children.Two things are worth knowing for anyone writing such an override.
It has to keep an identifier the caller already chose.
DefaultNodeIdFactory.Newreturnsnode.NodeIdunchanged when it is non-null and in the manager's own namespace. Without that same guard,NodeState.Create(context, nodeId, …, assignNodeIds: true)sets the explicit id and thenAssignNodeIdsoverwrites it from the factory — which silently moved…SupervisionProcessFluid.Cavitation.Alarmto…SupervisionProcessFluid.CavitationAlarmand broke five alarm tests until the guard was added.It has to be narrow. A browse name is unique only among its siblings. The HA override originally named every node the manager minted, and the historian hangs an
HA Configurationobject off each node it historizes — so the Counter's and the HistoryEvents' one both landed onns=2;s=HA Configurationand the strong active/passive replicas refused to start. It is now limited to the nodes the redundant client actually spells out.Merge with master
Merged
origin/master(0e600c0f0). The one conflict was inHaSampleNodeManager.CreateAddressSpaceAsync, where #4445 addedAddFactoryAssignedNodesnext to theHistoryEventsobject this branch had converted. Both are kept, and the factory-assigned subtree stays hand-built and unstaged on purpose: it exists to show what an identity minted straight throughNodeIdFactorylooks like beside the sample's named ones. It is attached once the folder has been staged, so those identifiers survive untouched and the subtree is still registered as part of the folder's. Its parent's identifier iss=HighAvailabilityon both sides, so the ids it mints are unchanged from master.Not candidates
src/Opc.Ua.PubSub.Server/PubSubNodeManager.csis out for the issue's own DI-device-builder reasoning. All three of its build paths are reachable from runtime Method calls —OnAddDataSetFolderandOnRemoveDataSetFolder→RebuildConfigurationAddressSpaceAsync,OnAddPushTarget/OnRemovePushTarget→RebuildKeyPushTargetAddressSpaceAsync,OnGetSecurityGroup/OnAddSecurityGroup/OnRemoveSecurityGroup→RebuildSecurityGroupAddressSpaceAsync— plusConfigurationChanged. Each removes its previous roots and rebuilds them, andAdd*throwsBadInvalidStateonceRegisterAuthoredNodesAsynchas run. Independently of that, its parents (PublishSubscribe,PublishedDataSets,SecurityGroups,KeyPushTargets) are owned by the diagnostics node manager, soAttachToParentwould reject them asBadNodeIdUnknown.src/Opc.Ua.OpenUsd.Server/Scene/UsdSceneMaterializer.Properties.cs— the issue asked whether a builder is reachable at those call sites. It is not.MaterializeUsdStageis a public, documentedISystemContextextension (docs/OpenUsd.md, the package README) whose only input is the context; it returns a detached subtree for the caller to register. It already mints throughcontext.RequireNodeIdFactory(), which is the same factory the staged path uses, so there is nothing to gain and a public signature to break.Verification
Run against the merged tree:
Opc.Ua.Di.Tests— 425/425 pass (coversPumpOpenUsdE2eTests,PumpHostedReferenceTests).Opc.Ua.Redundancy.Samples.Tests— 14/14 pass, including both variants of master's newFactoryAssignedNodeIdsSurviveActiveReplicaFailureAsyncandStrongHistorianContinuationsSurviveActiveReplicaFailureAsync.Opc.Ua.WotCon.Samples.Tests— 25 pass, 4 fail. The same 4 fail on a clean baseline worktree (CompanionTypeDefinitionsMatchNativePumpServerAsync,WotPumpInstanceMatchesNativePumpSubsetAsync,RealSamplesAggregateSubscribeAndReplaceGenerationAsync,RealSamplesRouteManagementAndConditionActionsToEachSourceAsync— allBadConfigurationErrorout of the aggregation client). Pre-existing, unrelated to this change.🤖 Generated with Claude Code