Make NodeId assignment during a node copy explicit - #4157
Conversation
A node copy initializes every child from its source right after creating it, which overwrites whatever NodeId was assigned along the way. Assigning one therefore only consumes identifiers, and permanently leaks them for factories that track outstanding allocations. The copy used to avoid that by wrapping the context in one that reports its NodeIdFactory as absent, which is hard to reason about for anyone implementing a custom node manager. State the intent instead. NodeState gains a FindChild overload carrying assignInstanceNodeIds, a matching CreateChild, and a SupportsInstanceNodeIdAssignmentControl property saying whether a type honours it. Source generated types, MethodState and BaseDataVariableState override both, so a copy of those declines assignment outright and still sees the real context. The wrapper stays as a compatibility fallback and is now applied at the point where control is handed to an override that has no way of being told - either a type that predates the overload, or a derived type that overrides only the four argument FindChild and inherits the capability from a base that does not. A copy also keeps dispatching through the original CreateChild for types that do not opt in, so an override of it still runs. Also fixes MethodState.FindChild returning OutputArguments when asked for InputArguments without createOrReplace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
There was a problem hiding this comment.
Pull request overview
This PR fixes NodeId “leak/burn” behavior during NodeState copies by making instance NodeId assignment an explicit opt-in/opt-out during child materialization, while preserving compatibility for legacy overrides that can’t be told about the new intent flag.
Changes:
- Added
FindChild(..., bool assignInstanceNodeIds)andCreateChild(..., bool assignInstanceNodeIds)to allow callers (notably copies) to decline per-instance NodeId assignment explicitly. - Added
SupportsInstanceNodeIdAssignmentControland updated generated types plusMethodState/BaseDataVariableStateto honor the opt-out without lying about the context (while retaining the wrapper fallback at the legacy override boundary). - Added regression tests covering “copy consumes no NodeIds” across modern, legacy, and derived-legacy override scenarios; also fixed a pre-existing
MethodState.FindChildbug (InputArguments vs OutputArguments).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateTemplates.cs | Updates generated node templates to opt into assignment-control and thread the flag through child creation paths. |
| src/Opc.Ua.Types/State/NodeState.cs | Introduces assignment-control overloads and the opt-in capability flag; updates copy path to decline assignment. |
| src/Opc.Ua.Types/State/MethodState.cs | Opts into assignment-control, adds the new overload, and fixes InputArguments resolution bug. |
| src/Opc.Ua.Types/State/BaseDataVariableState.cs | Opts into assignment-control and threads the flag through EnumStrings child creation. |
| src/Opc.Ua.Types/State/NodeIdFactorySuppressedContext.cs | Refines documentation: wrapper remains as a compatibility fallback for legacy override shapes. |
| tests/Opc.Ua.Types.Tests/State/NodeInstanceExtensionsTests.cs | Adds regression coverage for copy behavior (no NodeIdFactory consumption) and for legacy/derived override edge cases. |
romanett
left a comment
There was a problem hiding this comment.
IMO we should do a breaking change here, to have this cleanly resolved, and also a better more explicit NodeState.CreateChild interface
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Collapse the assignment control overloads into the existing virtuals instead of layering a compatibility mechanism on top of them. NodeState.FindChild and NodeState.CreateChild now take assignInstanceNodeIds as their last parameter, defaulting to true. Call sites keep compiling and keep the 1.5.378 behaviour; overrides must add the parameter, which is the accepted breaking change for 2.0. With the request carried as an argument there is no override shape left that cannot be told, so SupportsInstanceNodeIdAssignmentControl and the NodeIdFactorySuppressedContext wrapper are gone - no context misreports its NodeIdFactory any more. The private FindDeclaredChild helpers only existed to keep the two overrides from recursing into each other and are inlined back into the single override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f45109d9-59cb-4ffd-a4f7-16e1ea449ae4
|
Took the breaking change - pushed in 26b572c. The two overloads, the capability property and the context wrapper are all gone; public virtual BaseInstanceState? CreateChild(
ISystemContext context,
QualifiedName browseName,
bool assignInstanceNodeIds = true);
protected virtual BaseInstanceState? FindChild(
ISystemContext context,
QualifiedName browseName,
bool createOrReplace,
BaseInstanceState? replacement,
bool assignInstanceNodeIds = true);What that buys:
Migration is documented in |
Note the reduced NodeIdFactory call count during a node copy as a behaviour change, and point at the node-states sub-doc for the override migration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f45109d9-59cb-4ffd-a4f7-16e1ea449ae4
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f45109d9-59cb-4ffd-a4f7-16e1ea449ae4
Changes applied, re-requested review
Fixes #4137.
The problem
NodeState.Initialize(ISystemContext, NodeState)copies a node. Every child it materialises throughCreateChildgets a per-instance NodeId fromISystemContext.NodeIdFactory, and is then initialised from its source on the very next statement - which overwrites that NodeId. The assignment is therefore always discarded: it only burns identifiers, and permanently leaks them for factories that track outstanding allocations. Booting the Robotics node manager left ten reservations behind that every later allocation skipped.#4123 fixed the symptom by hiding the factory for the duration of the copy, wrapping the context in one that reports
NodeIdFactoryasnull. The review objection was fair: a context that lies about one property is hard to reason about, and it only works because that property is allowed to be null.What this does
States the intent instead of faking the environment. NodeId assignment is now an argument of the two members that materialise children:
A copy passes
assignInstanceNodeIds: falseand every node type - generated or hand-written - sees the real context. There is no capability property, no wrapper, and no second overload to keep in sync.MethodState,BaseDataVariableStateand the source generator template each collapse to a singleFindChildoverride that threads the argument into itsCreateOrReplace<Child>helpers.Breaking change
The four argument
FindChildand the two argumentCreateChildvirtuals are gone.true, soCreateChild(context, browseName)andFindChild(context, browseName, true, null)keep compiling and keep the 1.5.378 behaviour.CS0115until it does. The compiler points at every site;docs/migrate/2.0.x/node-states.mdshows the before/after.This is deliberate, per review: the stack ships all base types, and types generated from a model are regenerated for 2.0 anyway.
Also fixed
MethodState.FindChildreturnedOutputArgumentswhen asked forInputArgumentswithcreateOrReplace: false- a pre-existing bug in the method being restructured. Covered byFindChildReturnsTheRequestedArgumentsProperty.Validation
dotnet build UA.slnx: 0 errors, 0 warnings.Opc.Ua.Types.Tests,Opc.Ua.SourceGeneration.Core.Tests,Opc.Ua.SourceGeneration.Tests,Opc.Ua.Di.Tests,Opc.Ua.Robotics.Tests- all passing.