|
| 1 | +# Runtime NodeSets |
| 2 | + |
| 3 | +This guide explains how to load one or more NodeSet2 XML documents into the server's address space at startup without writing a source-generated or hand-coded NodeManager. You configure which files or streams to load; the server imports them in dependency order and registers the resulting nodes. |
| 4 | + |
| 5 | +## When to use the runtime NodeSet path |
| 6 | + |
| 7 | +Use `AddRuntimeNodeSet` when: |
| 8 | + |
| 9 | +- You receive a NodeSet2 XML from a companion-specification vendor and want to host its nodes without regenerating source. |
| 10 | +- Your information model changes frequently enough that rebuilding the source-generated manager for every XML update would be disruptive. |
| 11 | +- You are prototyping or testing a new NodeSet2 design. |
| 12 | + |
| 13 | +Use the [source-generated path](SourceGeneratedNodeManagers.md) when you want compile-time safety, strong typing, and AOT-safe named constants for every node in your model. The runtime path gives you generic `NodeState` objects and untyped browse-path wiring. |
| 14 | + |
| 15 | +## Startup-only semantics |
| 16 | + |
| 17 | +The runtime NodeSet loader runs **once during server startup**, inside `CreateAddressSpaceAsync`. There is no mechanism to hot-reload, add, or remove NodeSets after the server has accepted its first client connection. If you need to pick up changes to the NodeSet XML, restart the server. |
| 18 | + |
| 19 | +## Quick-start examples |
| 20 | + |
| 21 | +### Single file |
| 22 | + |
| 23 | +```csharp |
| 24 | +services.AddOpcUa() |
| 25 | + .AddServer(o => { /* … */ }) |
| 26 | + .AddRuntimeNodeSet("Models/MyMachine.NodeSet2.xml"); |
| 27 | +``` |
| 28 | + |
| 29 | +### Single file with a fluent callback |
| 30 | + |
| 31 | +Wire read/write/method handlers on top of the imported nodes using the existing untyped `INodeManagerBuilder` surface: |
| 32 | + |
| 33 | +```csharp |
| 34 | +services.AddOpcUa() |
| 35 | + .AddServer(o => { /* … */ }) |
| 36 | + .AddRuntimeNodeSet( |
| 37 | + "Models/MyMachine.NodeSet2.xml", |
| 38 | + nodes => |
| 39 | + { |
| 40 | + nodes.Node("Machines/Machine1/Start") |
| 41 | + .OnCall(StartMachineAsync); |
| 42 | + |
| 43 | + nodes.Variable<double>("Machines/Machine1/Temperature") |
| 44 | + .OnRead(ReadTemperature); |
| 45 | + }); |
| 46 | +``` |
| 47 | + |
| 48 | +### Group of dependent NodeSets |
| 49 | + |
| 50 | +Register multiple NodeSet2 sources that depend on each other. The factory resolves the import order automatically from the `RequiredModel` declarations in each document. |
| 51 | + |
| 52 | +```csharp |
| 53 | +services.AddOpcUa() |
| 54 | + .AddServer(o => { /* … */ }) |
| 55 | + .AddRuntimeNodeSet(options => |
| 56 | + { |
| 57 | + options.Sources = |
| 58 | + [ |
| 59 | + RuntimeNodeSetSource.FromFile("Models/Opc.Ua.Di.NodeSet2.xml"), |
| 60 | + RuntimeNodeSetSource.FromFile("Models/MyMachine.NodeSet2.xml") |
| 61 | + ]; |
| 62 | + options.DefaultNamespaceUri = "urn:example:MyMachine"; |
| 63 | + options.Configure = nodes => |
| 64 | + { |
| 65 | + nodes.Node("Machines/Machine1/Start").OnCall(StartMachineAsync); |
| 66 | + }; |
| 67 | + }); |
| 68 | +``` |
| 69 | + |
| 70 | +### Custom stream source |
| 71 | + |
| 72 | +Use `RuntimeNodeSetSource.FromStream` when you want to open the NodeSet2 document lazily at server startup — for example from a database, a blob store, or an assembly resource. The delegate is called once during startup and must return a fresh, readable `Stream` each time. |
| 73 | + |
| 74 | +```csharp |
| 75 | +services.AddOpcUa() |
| 76 | + .AddServer(o => { /* … */ }) |
| 77 | + .AddRuntimeNodeSet(options => |
| 78 | + { |
| 79 | + options.Sources = |
| 80 | + [ |
| 81 | + RuntimeNodeSetSource.FromStream( |
| 82 | + name: "MyMachine", |
| 83 | + openStream: ct => OpenNodeSetStreamAsync(ct), |
| 84 | + modelNamespaceUris: ["urn:example:MyMachine"]) |
| 85 | + ]; |
| 86 | + }); |
| 87 | +``` |
| 88 | + |
| 89 | +### Direct factory registration |
| 90 | + |
| 91 | +If you prefer to create the factory manually — for example to inject it into an existing `StandardServer` subclass — construct a `RuntimeNodeSetNodeManagerFactory` directly: |
| 92 | + |
| 93 | +```csharp |
| 94 | +var factory = new RuntimeNodeSetNodeManagerFactory(new RuntimeNodeSetOptions |
| 95 | +{ |
| 96 | + Sources = [RuntimeNodeSetSource.FromFile("Models/MyMachine.NodeSet2.xml")] |
| 97 | +}); |
| 98 | + |
| 99 | +// In a StandardServer subclass constructor: |
| 100 | +AddNodeManager(factory); |
| 101 | +``` |
| 102 | + |
| 103 | +## Stream ownership contract |
| 104 | + |
| 105 | +When `FromStream` is used, the runtime loader calls the `openStream` delegate once while the NodeManager factory is created during server startup and closes the returned stream after deserialization. You must ensure that: |
| 106 | + |
| 107 | +1. Each call to `openStream` returns a **new** stream positioned at the beginning. |
| 108 | +2. The stream is **readable** and contains a valid NodeSet2 XML document. |
| 109 | +3. You do not close the stream yourself; the factory disposes it after `UANodeSet.Read`. |
| 110 | + |
| 111 | +If the delegate returns `null` or the stream does not contain valid NodeSet2 XML, server startup fails with a clear `InvalidOperationException` that names the source. |
| 112 | + |
| 113 | +## Default namespace for unqualified browse paths |
| 114 | + |
| 115 | +When the `Configure` callback uses browse paths without an explicit `ns=N;` prefix (for example `"Machines/Machine1/Start"`), the runtime loader must know which namespace index to apply for the first path segment. The resolution is: |
| 116 | + |
| 117 | +1. If `RuntimeNodeSetOptions.DefaultNamespaceUri` is set, that URI is used. |
| 118 | +2. Otherwise the factory infers the **unique leaf model** — the one model in the loaded group that is not required by any other included source. |
| 119 | +3. If inference is ambiguous (multiple leaf models and a `Configure` callback is present), startup fails with an error message that lists the candidates. In this case, set `DefaultNamespaceUri` explicitly. |
| 120 | + |
| 121 | +When no `Configure` callback is registered, `DefaultNamespaceUri` has no effect and may be omitted. |
| 122 | + |
| 123 | +## Dependency sorting |
| 124 | + |
| 125 | +The factory reads the `Models/Model/RequiredModel` entries from each parsed NodeSet document and performs a topological sort (Kahn's algorithm) before importing. Import order guarantees that a required model's nodes are in the address space before any document that depends on them imports its nodes. |
| 126 | + |
| 127 | +Dependencies on models **not included in the group** — for example the OPC UA base namespace or a third-party model hosted by a generated NodeManager — are silently allowed and treated as external. The server resolves cross-manager references through the normal `AddReverseReferencesAsync` mechanism. |
| 128 | + |
| 129 | +Cycles among the included sources cause `InvalidOperationException` at startup with an error message that lists the participating sources. |
| 130 | + |
| 131 | +## Complex types |
| 132 | + |
| 133 | +Runtime complex type loading (structures, enumerations, union types) is **on by default** and requires no extra configuration. After all NodeManagers have built their address spaces, `StandardServer.OnNodeManagerStartedAsync` scans every DataType node whose `DataTypeDefinition` attribute is populated and registers a NativeAOT-safe stand-in encodeable in the server's factory. The same stand-ins are reused for client decode/encode via the OPC UA Part 6 binary protocol. |
| 134 | + |
| 135 | +You can tune this behaviour through the existing `ServerComplexTypeOptions`: |
| 136 | + |
| 137 | +```csharp |
| 138 | +services.AddOpcUa() |
| 139 | + .AddServer(o => { /* … */ }) |
| 140 | + .AddRuntimeNodeSet("Models/MyMachine.NodeSet2.xml") |
| 141 | + .AddComplexTypeSystem(o => o.Enabled = false); // disable if not needed |
| 142 | +``` |
| 143 | + |
| 144 | +Setting `StandardServer.LoadComplexTypes = false` or disabling the complex type system entirely suppresses the stand-in loading without producing a warning. No second complex-type loading path is introduced by the runtime NodeSet feature. |
| 145 | + |
| 146 | +For a complete description of the server-side complex type system, see [ComplexTypes.md](ComplexTypes.md). |
| 147 | + |
| 148 | +## Comparison with source-generated NodeManagers |
| 149 | + |
| 150 | +| Aspect | Runtime NodeSet (`AddRuntimeNodeSet`) | Source-generated (`[NodeManager]`) | |
| 151 | +|--------|--------------------------------------|-------------------------------------| |
| 152 | +| Node access in callbacks | Generic `NodeState` / `BaseVariableState` via untyped browse paths | Strongly typed, compiler-checked fluent accessors per node | |
| 153 | +| Compilation required on model change | No — reload the XML and restart | Yes — regenerate and rebuild | |
| 154 | +| AOT / trimming compatibility | Full (uses the existing `UANodeSet.Read` XmlSerializer path) | Full (generated code is static) | |
| 155 | +| Named NodeId constants | Not generated | Generated (`Variables.*`, `Objects.*`, etc.) | |
| 156 | +| Multiple namespaces in one manager | Yes — group multiple sources | One namespace per generator run | |
| 157 | +| DI registration | `AddRuntimeNodeSet(...)` | `AddNodeManager<TFactory>()` | |
| 158 | +| Stream / file input | Files and custom stream factories | MSBuild `AdditionalFiles` only | |
| 159 | + |
| 160 | +Source-generated managers are the recommended path for production code where type safety and compile-time validation matter. Runtime NodeSets are the recommended path for rapid prototyping, model-file delivery scenarios, and cases where the XML content changes independently of the server binary. |
| 161 | + |
| 162 | +## Related documentation |
| 163 | + |
| 164 | +- [Source-Generated NodeManagers](SourceGeneratedNodeManagers.md) — strongly typed alternative. |
| 165 | +- [Dependency Injection](DependencyInjection.md) — `IOpcUaServerBuilder` and service registration. |
| 166 | +- [ComplexTypes.md](ComplexTypes.md) — server-side complex type loading and client-side decoding. |
0 commit comments