Summary
The fluent node manager (FluentNodeManagerBase, NodeManagerBuilder, the [NodeManager] generated partial) can only address nodes that exist in PredefinedNodes when Configure runs. There is no fluent surface for a manager whose address space is materialised on demand: nodes that are synthesised when a client asks for them, browsed from an external system, or too numerous to keep in memory. Every such manager has to keep overriding GetManagerHandleAsync / ValidateNodeAsync / CreateBrowser on AsyncCustomNodeManager, and none of the per-node builder hooks (OnRead, OnWrite, OnMonitoredItemCreated, ...) can be attached to those nodes.
Where the limit is
src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs: Node(...)/Variable(...) resolve through the predefined-node lookups only and throw BadNodeIdUnknown ("did not resolve to a predefined node") otherwise; every registration is guarded by ThrowIfSealed(), so nothing can be registered after Configure.
src/Opc.Ua.Server/Fluent/IFluentDispatcher.cs: the dispatcher has five routes (TryHandleHistoryRead, TryHandleHistoryUpdate, NotifyMonitoredItemCreated, NotifyNodeAdded, NotifyNodeRemoved), all keyed by the NodeId of a node registered during Configure.
src/Opc.Ua.Server/Fluent/NodeBuilder.cs: OnRead/OnWrite/OnCall write straight into the NodeState slots (OnReadValue, OnWriteValue, OnCallMethod2), so the node instance must exist at configure time.
src/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs: the only extension points are the virtuals GetManagerHandleAsync (line ~2786) and ValidateNodeAsync (~3666), which a generated manager's user partial may still override but which live entirely outside the fluent model.
docs/NodeManagers.md "Current limitations" lists wildcards, historian masking and reserved names only; nothing documents an alternative for on-demand nodes.
Who needs it (from the UA-.NETStandard-Samples audit)
| Sample |
On-demand pattern |
Workshop/DataAccess |
Segments and blocks synthesised per operation from an underlying system, custom SegmentBrowser, multi-parent blocks, tests pin a ParsedNodeId format |
Workshop/PerfTest |
50 000 register variables that never exist as nodes; NodeId is (register << 24) | index, lazy browser |
Opc.Ua.Sample/MemoryBuffer |
Tag nodes buffer[offset] resolved from the NodeId string, synthesised children on browse |
Workshop/HistoricalAccess |
Archive folders and items materialised from the file system under a configured root |
Workshop/Aggregation |
Mirror nodes of a remote server materialised per operation (a proxy; arguably keeps its overrides regardless) |
All of them can switch base class to FluentNodeManagerBase, but the migration is cosmetic: the on-demand behaviour stays in overrides and the builder has nothing to bind to.
Proposal
Add a manager-level "virtual node provider" to the fluent surface, so that the builder can own nodes that are resolved lazily:
// On INodeManagerBuilder (or a dedicated extension):
builder.ResolveNodes(
predicate: nodeId => nodeId.NamespaceIndex == ns && nodeId.IdType == IdType.String,
resolve: async (ISystemContext ctx, NodeId nodeId, CancellationToken ct) => /* NodeState? or null */,
browse: (ISystemContext ctx, NodeState parent) => /* NodeBrowser for synthesised children, optional */);
- The generated partial (and
FluentNodeManager) would route GetManagerHandleAsync / ValidateNodeAsync misses on PredefinedNodes through the registered resolvers, and CreateBrowser through the registered browse provider, caching per operation exactly as AsyncCustomNodeManager does today for validated handles.
- Dispatcher tables would need a fallback path keyed by the resolver (or by namespace/pattern) instead of by NodeId, so
OnRead/OnWrite/OnMonitoredItemCreated handlers can be registered once for a family of virtual nodes: builder.ResolveNodes(...).OnRead(...).
- Nodes returned by a resolver should be able to use the manager's
INodeIdFactory or keep the id they were asked for; the sample tests pin caller-chosen ids in every case above.
Related: monitored-item lifecycle hooks for such nodes are tracked separately (custom monitored-item factory and delete/modify/mode hooks), because a virtual node that is only alive while a monitored item exists needs both.
Related: #4398 (monitored-item lifecycle hooks), #4399 (pre-creation monitored-item hook), #4400 (historian defaults), #4388 (hosting AddNodeManager root folder).
Summary
The fluent node manager (
FluentNodeManagerBase,NodeManagerBuilder, the[NodeManager]generated partial) can only address nodes that exist inPredefinedNodeswhenConfigureruns. There is no fluent surface for a manager whose address space is materialised on demand: nodes that are synthesised when a client asks for them, browsed from an external system, or too numerous to keep in memory. Every such manager has to keep overridingGetManagerHandleAsync/ValidateNodeAsync/CreateBrowseronAsyncCustomNodeManager, and none of the per-node builder hooks (OnRead,OnWrite,OnMonitoredItemCreated, ...) can be attached to those nodes.Where the limit is
src/Opc.Ua.Server/Fluent/NodeManagerBuilder.cs:Node(...)/Variable(...)resolve through the predefined-node lookups only and throwBadNodeIdUnknown("did not resolve to a predefined node") otherwise; every registration is guarded byThrowIfSealed(), so nothing can be registered afterConfigure.src/Opc.Ua.Server/Fluent/IFluentDispatcher.cs: the dispatcher has five routes (TryHandleHistoryRead,TryHandleHistoryUpdate,NotifyMonitoredItemCreated,NotifyNodeAdded,NotifyNodeRemoved), all keyed by the NodeId of a node registered duringConfigure.src/Opc.Ua.Server/Fluent/NodeBuilder.cs:OnRead/OnWrite/OnCallwrite straight into theNodeStateslots (OnReadValue,OnWriteValue,OnCallMethod2), so the node instance must exist at configure time.src/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs: the only extension points are the virtualsGetManagerHandleAsync(line ~2786) andValidateNodeAsync(~3666), which a generated manager's user partial may still override but which live entirely outside the fluent model.docs/NodeManagers.md"Current limitations" lists wildcards, historian masking and reserved names only; nothing documents an alternative for on-demand nodes.Who needs it (from the UA-.NETStandard-Samples audit)
Workshop/DataAccessSegmentBrowser, multi-parent blocks, tests pin aParsedNodeIdformatWorkshop/PerfTest(register << 24) | index, lazy browserOpc.Ua.Sample/MemoryBufferbuffer[offset]resolved from the NodeId string, synthesised children on browseWorkshop/HistoricalAccessWorkshop/AggregationAll of them can switch base class to
FluentNodeManagerBase, but the migration is cosmetic: the on-demand behaviour stays in overrides and the builder has nothing to bind to.Proposal
Add a manager-level "virtual node provider" to the fluent surface, so that the builder can own nodes that are resolved lazily:
FluentNodeManager) would routeGetManagerHandleAsync/ValidateNodeAsyncmisses onPredefinedNodesthrough the registered resolvers, andCreateBrowserthrough the registered browse provider, caching per operation exactly asAsyncCustomNodeManagerdoes today for validated handles.OnRead/OnWrite/OnMonitoredItemCreatedhandlers can be registered once for a family of virtual nodes:builder.ResolveNodes(...).OnRead(...).INodeIdFactoryor keep the id they were asked for; the sample tests pin caller-chosen ids in every case above.Related: monitored-item lifecycle hooks for such nodes are tracked separately (custom monitored-item factory and delete/modify/mode hooks), because a virtual node that is only alive while a monitored item exists needs both.
Related: #4398 (monitored-item lifecycle hooks), #4399 (pre-creation monitored-item hook), #4400 (historian defaults), #4388 (hosting AddNodeManager root folder).