Skip to content

Fluent node manager: no surface for on-demand (virtual) nodes; managers must still override GetManagerHandleAsync/ValidateNodeAsync #4397

Description

@romanett

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementAPI or feature enhancement

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions