# OPC UA Device Integration (DI) > **Glossary:** In this document, **DI** refers to **OPC UA Device > Integration** (companion specification [OPC 10000-100](https://reference.opcfoundation.org/specs/OPC-10000-100)), > *not* to .NET Dependency Injection (covered in > [DependencyInjection.md](DependencyInjection.md)). Wherever this > document uses the unqualified initialism "DI" it always means the > companion spec; .NET-DI mentions are spelled out as "Dependency > Injection (.NET DI)" or appear with `Microsoft.Extensions.DependencyInjection` > in context. End-to-end developer guide for the `Opc.Ua.Di*` library trio shipped with this repository and the `services.AddOpcUa()` hosting model that plugs it together. ## Contents - [Library layout](#library-layout) - [Quick start](#quick-start) - [Device builder](#device-builder) - [Device sub-type extensions](#device-sub-type-extensions) - [Hosting integration](#hosting-integration) - [Lock service](#lock-service) - [Software update](#software-update) - [Client helpers](#client-helpers) - [What is supported (OPC 10000-100)](#what-is-supported-opc-10000-100) - [See also](#see-also) ## Library layout | Library | Role | |---------|------| | `Opc.Ua.Di` | Model assembly: source-generated NodeId tables, DataTypes, ObjectType client proxies, `AddOpcUaDi(NodeStateCollection)` extension. | | `Opc.Ua.Di.Server` | Server: `DiNodeManager`, fluent topology/device builders, locking service, software-update package store, hosting integration. | | `Opc.Ua.Di.Client` | Client: `DiDeviceClient`, `DiDiscoveryClient`, `DiTopologyClient`, `DiLockClient`, `SoftwareUpdateClient`, hosting integration. | The running example is `samples/DI/PumpDeviceIntegrationServer`, a companion-spec server that exposes a configurable number of fully simulated `PumpType` instances. The sample uses the hand-written fluent node-manager style for the common pump model and simulation wiring, then uses `ConfigureDevicesFor` with the topology-element builder to add an ad-hoc Diagnostics functional group to each pump. The simulated asset is specified by its own product datasheet, [`samples/DI/PumpDeviceIntegrationServer/DATASHEET.md`](../samples/DI/PumpDeviceIntegrationServer/DATASHEET.md), which maps every nameplate field, engineering range and alarm trip point to its OPC UA browse path. ## Quick start ### Plain Device Integration (DI) server ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services .AddOpcUa() .AddServer(o => { o.ApplicationName = "MyDiServer"; o.EndpointUrls.Add("opc.tcp://localhost:48010/MyDiServer"); }) .AddOpcUaDi() .ConfigureDevicesFor(async ctx => { var device = await ctx.CreateDeviceAsync( new QualifiedName("Sensor_1", ctx.Manager.InstanceNamespaceIndex)); device.WithIdentification(id => { id.Manufacturer = new LocalizedText("Acme"); id.SerialNumber = "SN-001"; id.DeviceClass = "Sensor"; }); }); await builder.Build().RunAsync(); ``` ### Companion-spec server (Pumps + Device Integration) ```csharp builder.Services .AddOpcUa() .AddServer(o => { ... }) .AddNodeManager() // Pump factory already loads OPC UA Device Integration via its LoadPredefinedNodesAsync // direct chain; do NOT call AddOpcUaDi() — that would // double-register the OPC UA Device Integration namespace. .ConfigureDevicesFor(ctx => { var manager = (Pumps.PumpNodeManager)ctx.Manager; foreach (NodeId pumpNodeId in manager.PumpNodeIds) { ITopologyElementBuilder pump = ctx.TopologyElement(pumpNodeId); pump.WithFunctionalGroup( new QualifiedName("Diagnostics", manager.InstanceNamespaceIndex), group => group.Configure(node => node.WithProperty("LastError", Variant.From(string.Empty)))); } return ValueTask.CompletedTask; }); ``` ### Client ```csharp builder.Services .AddOpcUa() .AddClient(o => { o.Configuration = ...; o.Session.Endpoint = ...; }) .AddOpcUaDi(); public sealed class MyAppService( IDiDiscoveryService discovery, Func> deviceFactory, Func> topologyFactory) { public async Task PrintAsync(CancellationToken ct) { DiTopologyClient topology = await topologyFactory(ct); await foreach (TopologyEntry entry in topology.EnumerateDevicesAsync(ct)) { DiDeviceClient device = await deviceFactory(entry.NodeId, ct); DeviceIdentification id = await device.ReadIdentificationAsync(ct); Console.WriteLine($"{id.Manufacturer} {id.Model} ({id.SerialNumber})"); } } } ``` ## Device builder The `IDeviceBuilder` fluent surface is the recommended way to create and configure Device Integration device instances programmatically. It lives in `Opc.Ua.Di.Server.Builders` and integrates with the broader fluent API for node managers. > **Runtime-created NodeIds use the server namespace.** `DiNodeManager` > now mints NodeIds for device and topology instances in > `InstanceNamespaceIndex`, the server application's namespace derived > from `ApplicationConfiguration.ApplicationUri`, rather than in the > DI or companion-spec namespace of the parent. This follows the > companion-spec modelling rules (for example OPC 10000-100 §13.4 and > OPC 40223): nodes not defined by a specification must not use the > standard namespace. The local server defines such nodes in its Local > Server URI namespace. BrowseNames defined by DI or another companion > specification keep that specification's namespace. Use > `InstanceNamespaceIndex` for application-defined instance names, > functional groups, and properties. ### Entry points All entry points live on `DiNodeManager`: ```csharp // Default DeviceState organized by the Device Integration DeviceSet (or attached // below whatever ResolveDefaultDeviceParent() returns). ValueTask> CreateDeviceAsync( QualifiedName browseName, NodeState? parent = null, CancellationToken ct = default); // Typed factory for ComponentType-derived companion-spec devices. ValueTask> CreateDeviceAsync( QualifiedName browseName, NodeId typeDefinitionId, Func factory, NodeState? parent = null, CancellationToken ct = default) where TDevice : ComponentState; // Wrap an existing device. IDeviceBuilder Device(TDevice device) where TDevice : ComponentState; IDeviceBuilder Device(NodeId nodeId) where TDevice : ComponentState; IDeviceBuilder DeviceByBrowseName( QualifiedName browseName, NodeState? parent = null) where TDevice : ComponentState; // Wrap an existing TopologyElementType-derived companion instance. ITopologyElementBuilder TopologyElement(TElement element) where TElement : TopologyElementState; ITopologyElementBuilder TopologyElement(NodeId nodeId) where TElement : TopologyElementState; ITopologyElementBuilder TopologyElementByBrowseName( QualifiedName browseName, NodeState? parent = null) where TElement : TopologyElementState; ``` `CreateDeviceAsync` performs five steps: 1. Resolves the parent (default: Device Integration `DeviceSet`; subclasses override `ResolveDefaultDeviceParent()` — e.g. machinery managers can return the `Machines` folder). 2. Uses `Organizes` for the well-known DI `DeviceSet`; explicit custom parents retain `HasComponent`. 3. Fails fast if a child with the same browse name already exists (`StatusCodes.BadBrowseNameDuplicated`). 4. Materializes the instance through the source-generated `CreateInstanceOf` factory, such as `CreateInstanceOfDeviceType`. The factory adds the type's **mandatory** children and `HasInterface` references. For `DeviceType`, the children are eight nameplate variables: `Manufacturer`, `Model`, `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, `DeviceManual`, `SerialNumber`, and `RevisionCounter`. It assigns each variable a DI-namespace BrowseName. Because the factory receives a BrowseName, it also rebases the subtree onto per-instance NodeIds in the server's application namespace (`DiNodeManager.InstanceNamespaceIndex`, the Local Server URI namespace). This prevents instances of the same type from colliding on the generator's type NodeIds. It also keeps application nodes out of DI and other companion-spec namespaces. Finally, the factory sets `BrowseName`, `SymbolicName`, and `DisplayName`, and stamps `TypeDefinitionId`. 5. Calls the real `AsyncCustomNodeManager.AddPredefinedNodeAsync` so the node create lifecycle, subscription wiring, type-tree registration, and root-notifier propagation all happen exactly as for nodes loaded from a NodeSet2. Machinery-aware subclasses can additionally reference managed machines from the OPC 40001 `Machines` entry point. `DiNodeManager`'s helper adds an `Organizes` reference to the existing instance instead of re-parenting it, so the same machine remains organized by `DeviceSet` while also being browsable from `Machines`. ### Fluent surface ```csharp IDeviceBuilder .WithIdentification(action) .WithIdentificationGroup(action) .WithConfigurationGroup(action) .WithMaintenanceGroup(action) .WithDiagnosticsGroup(action) .WithStatusGroup(action) .WithOperationalGroup(action) .WithStatisticsGroup(action) .WithOperationCountersGroup(action) .WithFunctionalGroup(QualifiedName name, action) .ConnectsTo(NodeId other) .ConnectsToParent(NodeId parent) .Configure(action) .WithDeviceHealth(DeviceHealthEnumeration) // extension; requires TDevice : DeviceState ``` `ITopologyElementBuilder` is the parallel surface for generated companion types that derive directly from `TopologyElementType`. It exposes only common topology operations: functional groups, `ConnectsTo` references, typed node access, and raw state configuration. The internal device-builder implementation reuses the same behavior without changing the existing public `IDeviceBuilder` contract. Component nameplates, support information, software update, lifetime indications, and `DeviceHealth` remain on their narrower component/device builders. #### Identification properties `WithIdentification(action)` populates the standard Device Integration nameplate properties on the device. The mutable `DeviceIdentificationData` record holds: - `Manufacturer` (`LocalizedText`, `IsNull` = unset) - `ManufacturerUri` (`string?`) - `Model` (`LocalizedText`, `IsNull` = unset) - `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, `ProductCode`, `DeviceManual`, `DeviceClass`, `SerialNumber`, `ProductInstanceUri` (`string?`) - `RevisionCounter` (`int?`) Properties that exist as typed children on the device are updated in place. The eight mandatory nameplate properties are always present (materialised by `CreateInstanceOfDeviceType`), so setting them takes the in-place path. Optional nameplate properties (`ManufacturerUri`, `ProductCode`, `ProductInstanceUri`, `DeviceClass`) are created on demand via `NodeState.AddProperty`, re-qualified into the **DI namespace** (never namespace 0), and registered with the manager. #### DI companion-spec compliance Devices created through `CreateDeviceAsync` are modelled to the DI companion spec: mandatory `DeviceType` children are present with DI-namespace BrowseNames, the type's `HasInterface` references are carried, and the software-update state-machine method children point at their in-type `MethodDeclarationId`s. The model is intentionally **lean** — OPC UA does not require *optional* interface members (`IVendorNameplateType`/`ITagNameplateType`/`ISupportInfoType` members such as `AssetId`, `ProductCode`, `DeviceTypeImage`, `Documentation`, `ImageSet`, `ProtocolSupport`) to be instantiated, so they are omitted unless the application adds them. An external compliance checker may still flag omitted optional members. It may also report placeholder warnings for application-added `HasComponent` children, such as a custom `Diagnostics` functional group or the `SoftwareUpdate` facet. The specification permits these omissions and extensions; they are not modelling errors. #### Functional groups Common Device Integration functional groups have typed builder methods. Spec-defined groups keep their standard BrowseName namespace, for example `Identification`, `Maintenance`, and `Operational` in the DI namespace and `Events` in the Pumps namespace. Application-defined groups use the server instance namespace; the diagnostics helper follows that rule because the diagnostics group it creates is vendor-defined. Arbitrary group names go through `WithFunctionalGroup(qualifiedName, action)`. Each call is idempotent: invoking the same accessor twice reuses the existing group. The order of operations inside the builder is: 1. Create the `FunctionalGroupState` child (via `TopologyElementState.AddIdentification` or `AddGroupIdentifier`). 2. Normalise BrowseName, SymbolicName, DisplayName, NodeId (via `Context.NodeIdFactory`) and `TypeDefinitionId`. 3. Register the group with the manager via `AddPredefinedNodeAsync`. 4. Hand the `IFunctionalGroupBuilder` to the configure delegate. ```csharp device.WithMaintenanceGroup(fg => { fg.Organizes(device.Manufacturer!.NodeId); fg.Node.WithProperty("LastMaintenanceDate", DateTime.UtcNow); }); ``` For vendor-specific groups, use the non-typed `WithFunctionalGroup(QualifiedName, action)` overload. The browse name's namespace index controls where the group lives; pass `DiNodeManager.InstanceNamespaceIndex` for application-defined groups. Reserve DI, Pumps, Machinery, or other companion-spec BrowseName namespaces for nodes that are defined by those specifications. The `PumpDeviceIntegrationServer` sample demonstrates this on `Pump #2` to surface a custom `Diagnostics` folder: ```csharp pump.WithFunctionalGroup( new QualifiedName("Diagnostics", ctx.Manager.InstanceNamespaceIndex), fg => fg.Configure(node => node.WithProperty("LastError", string.Empty) .WithProperty("ErrorCount", 0) .WithProperty("LastSelfTest", DateTime.UtcNow))); ``` #### Topology references `ConnectsTo(NodeId)` adds a forward `Opc.Ua.Di.ReferenceTypes.ConnectsTo` reference; `ConnectsToParent(NodeId)` adds the inverse. Useful for declaring physical / logical topology relationships outside the hierarchical address-space tree. #### Device health `WithDeviceHealth(DeviceHealthEnumeration)` is an extension method constrained to `TDevice : DeviceState` (where the typed `DeviceHealth` child variable exists). Throws `StatusCodes.BadInvalidState` if the device was constructed without the `DeviceHealth` child — typically because the factory was `p => new DeviceState(p)` rather than the generator-produced `CreateDeviceType` factory. Callers can pre-populate the child via `Configure((dev, ctx) => ...)` to avoid the error. ### Subclass support Companion-spec managers (`PumpNodeManager`, machinery managers) inherit from `DiNodeManager` and gain the entire builder surface for free. They typically override `ResolveDefaultDeviceParent()` to return the companion-spec-specific container (`Machines` folder, etc.) and supply their own typed factories to `CreateDeviceAsync(...)`. ## Device sub-type extensions Three extension surfaces on top of `IDeviceBuilder` cover optional OPC 10000-100 features: ### Software / Block / ConfigurableObject (§5.4 advanced sub-types) `DeviceBuilderTypeExtensions` adds typed materialisation helpers for the §5.4 sub-types that are not provided by `WithDeviceType` itself: ```csharp SoftwareState sw = builder.AddSoftware(qn("Firmware")); BlockState blk = builder.AddBlock(qn("InputBlock")); ConfigurableObjectState cfg = builder.AddConfigurableObject(qn("DriverFolder")); ``` Each helper creates a typed child under the device, stamps a manager-assigned `NodeId` via `INodeIdFactory`, sets the `HasComponent` reference, registers the child via `AddPredefinedNodeAsync`, and invokes an optional `configure` callback. The returned typed state is ready for further configuration through the standard `NodeState` APIs. ### Lifetime indication (§10.6) `DeviceBuilderLifetimeExtensions.AddLifetimeIndication` creates a `LifetimeVariableState` under the device with a `LifetimeIndicationKind` classifier covering all six §10.6 indication sub-types: ```csharp device.AddLifetimeIndication( qn("OperatingHours"), LifetimeIndicationKind.Time, startValue: 0.0); device.AddLifetimeIndication( qn("PartsProduced"), LifetimeIndicationKind.NumberOfParts, startValue: 0.0); ``` The classifier enum maps directly to OPC 10000-100 ObjectType ids: | `LifetimeIndicationKind` | OPC UA ObjectType | |---|---| | `Time` | `TimeIndicationType` | | `NumberOfParts` | `NumberOfPartsIndicationType` | | `NumberOfUsages` | `NumberOfUsagesIndicationType` | | `Length` | `LengthIndicationType` | | `Diameter` | `DiameterIndicationType` | | `SubstanceVolume` | `SubstanceVolumeIndicationType` | `DeviceBuilderLifetimeExtensions.ResolveIndicationTypeId(kind, namespaceUris)` resolves the matching NodeId at runtime when applications need to materialise the indication classifier object themselves. ### Support info (§5.15) `DeviceBuilderSupportInfoExtensions.WithSupportInfo` creates an `ISupportInfoState` child on the device (idempotent — re-uses the existing instance on subsequent calls) and yields it through a configure callback. The interface exposes `DocumentationFiles` / `ImageSet` / `ProtocolSupport` folder properties; consumers populate them by attaching `FileState` or `BaseObjectState` children through the standard `NodeState` API. File-backed children commonly use `FileState` wired to an `IFileSystemProvider` from `src/Opc.Ua.Server/FileSystem`. ```csharp device.WithSupportInfo(info => { info.Description = new LocalizedText("Support information for device 1"); // attach FileState children for DocumentationFiles / ImageSet // / ProtocolSupport folders here, using IFileSystemProvider // for content backing. }); ``` ## Hosting integration Use `AddOpcUaDi()` and `ConfigureDevicesFor()` to add the Device Integration library to the unified `AddOpcUa()` `Microsoft.Extensions.DependencyInjection` hosting pattern. ### Server-side surface `Microsoft.Extensions.DependencyInjection.OpcUaServerDiBuilderExtensions` adds three methods on `IOpcUaServerBuilder`: ```csharp IOpcUaServerBuilder AddOpcUaDi(); IOpcUaServerBuilder ConfigureDevicesFor( Action configure) where TNodeManager : DiNodeManager; IOpcUaServerBuilder ConfigureDevicesFor( Func configure) where TNodeManager : DiNodeManager; ``` #### When to use `AddOpcUaDi()` Call this when you want a **plain Device Integration (DI) server** without any companion-spec subclass. It registers the `DiNodeManagerFactory` so the hosted service stands up a pure `DiNodeManager`. ```csharp services.AddOpcUa() .AddServer(o => { ... }) .AddOpcUaDi(); ``` **Do NOT call `AddOpcUaDi()`** alongside a companion-spec factory that already loads OPC UA Device Integration (e.g. `PumpNodeManagerFactory` which loads Device Integration + Machinery + Pumps). The Device Integration namespace would be registered twice and the OPC UA server may reject the duplicate. `AddOpcUaDi()` throws on its second invocation to surface the misuse early. #### `ConfigureDevicesFor(action)` Registers a post-setup configurator targeted at a specific manager type. The runner invokes each matching delegate **after**: - the manager's `LoadPredefinedNodesAsync` has populated `PredefinedNodes`; - the manager's `CreateAddressSpaceAsync` base has wired up the type tree and root notifiers; - (for `FluentNodeManagerBase` subclasses) the user's `ConfigureAsync(builder, ct)` / `Configure(builder)` wiring and the subsequent `await builder.SealAsync(ct)` are complete. Configurator-type matching follows `Type.IsAssignableFrom`. A delegate targeting `DiNodeManager` runs against every Device Integration-derived manager (including `PumpNodeManager`); a delegate targeting `PumpNodeManager` will NOT run against a plain `DiNodeManager`. Multiple `ConfigureDevicesFor` calls accumulate and run in registration order. **Exception semantics are fail-fast**: any exception thrown by a configurator aborts hosted-server startup with a diagnostic that identifies the failing index and target type. ### Context surface The configurator receives an `IDiPostSetupContext`: ```csharp public interface IDiPostSetupContext { DiNodeManager Manager { get; } CancellationToken CancellationToken { get; } T GetRequiredService() where T : notnull; ValueTask> CreateDeviceAsync(...); ValueTask> CreateDeviceAsync(...) where TDevice : ComponentState; IDeviceBuilder Device(NodeId nodeId) where TDevice : ComponentState; IDeviceBuilder DeviceByBrowseName(QualifiedName name, NodeState? parent = null) where TDevice : ComponentState; ITopologyElementBuilder TopologyElement(NodeId nodeId) where TElement : TopologyElementState; ITopologyElementBuilder TopologyElementByBrowseName( QualifiedName name, NodeState? parent = null) where TElement : TopologyElementState; } ``` The context **does not** expose a raw `IServiceProvider`. Use `GetRequiredService()` to resolve application services (intentionally narrow to discourage service-locator anti-patterns and lifetime traps). ### Architecture | Component | Lifetime | Where | |-----------|----------|-------| | `IDiPostSetupRunner` | Singleton (registered by `AddOpcUaDi`/`ConfigureDevicesFor`) | `Opc.Ua.Di.Server.Hosting` | | `IDiPostSetupConfigurator` | Singleton (one per `ConfigureDevicesFor` call) | `Opc.Ua.Di.Server.Hosting` | | `DiNodeManagerFactory` / `PumpNodeManagerFactory` | Singleton (.NET-DI-aware ctor injects runner) | server / app | | `DiNodeManager` / `PumpNodeManager` | Per-server startup (factory passes runner) | app | The runner is injected into the manager via the factory. The base `DiNodeManager` auto-invokes it at the end of its own `CreateAddressSpaceAsync` for every concrete subclass: - The base `DiNodeManager.CreateAddressSpaceAsync` calls `LoadPredefinedNodesAsync` → `CreateFluentBuilder(...)` → `ConfigureAsync(builder, ct)` → `RegisterAuthoredNodesAsync` → `CompleteConfigureAsync` → `PostSetupRunner.RunAsync(this, ct)` → `SealConfigurationAsync(builder, ct)` in that order. Sealing comes last because the fluent registries (simulations, event sources) are owned by the manager, not by a single builder, and sealing starts them — a post-setup configurator must still be able to register a simulation loop. - Subclasses (e.g. `PumpNodeManager`) override the `protected virtual ValueTask ConfigureAsync(INodeManagerBuilder, CancellationToken)` hook to materialise instances and drive the fluent `INodeManagerBuilder`. Because the hook is awaited, asynchronous setup and synchronous wiring live in one method; the base class owns node registration and sealing. The runner fires automatically once the builder is sealed — subclasses do not need to call `PostSetupRunner.RunAsync` themselves. ```mermaid sequenceDiagram autonumber participant Host as Hosting / factory participant Base as DiNodeManager participant Sub as Subclass, e.g. PumpNodeManager participant B as NodeManagerBuilder participant Run as IDiPostSetupRunner Host->>Base: CreateAddressSpaceAsync(externalReferences, ct) Base->>Base: await LoadPredefinedNodesAsync Note over Base: predefined nodes loaded, type tree wired Base->>B: CreateFluentBuilder(InstanceNamespaceIndex) Note over B: attached to the manager's event-source
and simulation registries Base->>Sub: await ConfigureAsync(builder, ct) Note over Sub: asynchronous setup and synchronous wiring
in one awaited method Base->>Base: await RegisterAuthoredNodesAsync(builder, ct) Base->>Base: await CompleteConfigureAsync(externalReferences, ct) Base->>Run: await RunAsync(this, ct) Note over Run: configurators still register simulation
loops and event sources here Base->>B: await SealConfigurationAsync(builder, ct) Note over B: drain staged root notifiers,
then start the simulations ``` Sealing is the last step for a reason: the fluent registries belong to the manager rather than to one builder, and sealing starts them. Sealing before `RunAsync` would leave the post-setup configurators unable to register a simulation loop of their own. ### Runtime conformance advertisement `DiNodeManager` implements `IConformanceContributor`, so the hosted server publishes the DI facets and conformance units that are actually wired at startup. The server merges these contributions into `Server/ServerCapabilities/ServerProfileArray` and `Server/ServerCapabilities/ConformanceUnits` alongside the base server profiles. The conformance units follow what is built: | Runtime condition | Conformance units | | --- | --- | | DI `DeviceSet` exists | `DI DeviceTopology` | | A `LockingServicesState` has `InitLock`, `RenewLock`, or `ExitLock` wired | `DI Locking` | | The same lock service also wires `BreakLock` | `DI BreakLocking` | | Any software-update loading mode is recorded | `DI SU Software Update`, `DI SU PrepareForUpdate`, `DI SU Resume Update` | | `SoftwareLoadingMode.Direct` | `DI SU DirectLoading` | | `SoftwareLoadingMode.Cached` | `DI SU CachedLoading` | A facet is advertised only while every conformance unit the DI 1.05 profile database marks mandatory for it is advertised: | Server profile URI | Mandatory conformance units | Advertised | | --- | --- | --- | | `http://opcfoundation.org/UA-Profile/DI/Server/DeviceIntegrationHost` | `DI DeviceTopology`, `DI Offline` | No: offline and online device representations are not built | | `http://opcfoundation.org/UA-Profile/DI/Server/Locking` | `DI Locking` | When a lock service is wired | | `http://opcfoundation.org/UA-Profile/DI/Server/SoftwareUpdateBase` | `DI SU Software Update` | When a software update is attached | | `http://opcfoundation.org/UA-Profile/DI/Server/FileSystemLoading` | Software Update Base, `DI SU FileSystem Loading`, `DI SU Installation for File System` | No: `Package` builds a `PackageLoadingType`, not a `FileSystemLoadingType`, and the installation offers `InstallSoftwarePackage` | | `http://opcfoundation.org/UA-Profile/DI/Server/DirectLoading` | Software Update Base, `DI SU DirectLoading`, `DI SU UpdateStatus` | No: the `UpdateStatus` variable is not built | | `http://opcfoundation.org/UA-Profile/DI/Server/CachedLoading` | Software Update Base, `DI SU CachedLoading`, `DI SU Installation for Cached Loading`, `DI SU UpdateStatus` | No: the `UpdateStatus` variable is not built and the installation offers `InstallFiles` | ### Client-side surface `Microsoft.Extensions.DependencyInjection.OpcUaClientDiBuilderExtensions` adds a single extension method on `IOpcUaClientBuilder`: ```csharp IOpcUaClientBuilder AddOpcUaDi(); ``` This registers four .NET-DI-friendly services that wrap the lazy `ManagedSession` accessor produced by `AddClient(...)`: - `IDiDiscoveryService` — recursive device discovery from the `Objects` folder; streams `DeviceEntry` records as `IAsyncEnumerable` so callers can begin processing before the browse completes. - `Func>` — validates a device NodeId and returns a typed client. - `Func>` — client wrapper for `LockingServicesType.InitLock` / `RenewLock` / `ExitLock` / `BreakLock`. - `Func>` — topology browser for the Device Integration well-known folders. - `Func>` — software-version reader for a SoftwareUpdate instance. Requires `AddClient(...)` to be called first — the extension throws `InvalidOperationException` if the managed-session accessor is missing when one of the factories is resolved. ## Lock service The lock service implements the OPC 10000-100 §10.5 locking facet. It tracks ownership of every `TopologyElementType.Lock` instance in the address space, enforces configurable timeouts, releases locks automatically when the owning session closes, and exposes the four spec-defined methods through the typed `LockingServicesState` proxy. ### Components | Type | Purpose | |------|---------| | `ILockService` | Application-facing facade: `InitLock`, `RenewLock`, `ExitLock`, `BreakLock`, `GetState`. Thread-safe. | | `LockState` | Snapshot record (Locked, LockingClient, LockingUser, RemainingLockTimeSeconds). | | `LockStatus` | OPC 10000-100 status-code constants. | | `DefaultLockService` | Default in-memory implementation backed by a `ConcurrentDictionary`. | | `LockingServicesExtensions.BindToLockService` | Wires the four generated `*MethodState.OnCall` handlers on a `LockingServicesState` instance through an `ILockService`. | Namespaces: `Opc.Ua.Di.Server.Locking`. ### Status codes Following OPC 10000-100 §10.5: | Constant | Value | Returned from | |----------|-------|---------------| | `LockStatus.Ok` | 0 | All methods (success) | | `LockStatus.AlreadyLocked` | 1 | `InitLock` only | | `LockStatus.CouldNotLock` | 2 | `InitLock` only | | `LockStatus.NotLocked` | 1 | `RenewLock` / `ExitLock` / `BreakLock` | | `LockStatus.WrongClient` | 2 | `RenewLock` / `ExitLock` | ### Default lock service ```csharp var lockService = new DefaultLockService( lockDuration: TimeSpan.FromMinutes(5), // optional, defaults to 5 min timeProvider: TimeProvider.System); // optional, for tests // Hook session-close so locks held by a disconnecting session are // released automatically. lockService.AttachToSessionManager(server.SessionManager); ``` Lock ownership is keyed by `ServerSystemContext.SessionId`. When the service is invoked from a test (`SystemContext` rather than `ServerSystemContext`), it falls back to a synthetic session id derived from `ISystemContext.UserId` so unit tests can still distinguish callers. ### Binding to a `LockingServicesState` The Device Integration spec defines a `Lock` child on every `TopologyElementType` that implements four method invocations. Wire them through your service: ```csharp LockingServicesState lockNode = device.Lock!; lockNode.BindToLockService(elementId: device.NodeId, service: lockService); ``` `BindToLockService` reads `lockNode.InitLock`, `RenewLock`, `ExitLock`, `BreakLock` and replaces their `OnCall` handlers with delegates that route into the `ILockService`. The `elementId` argument keys the service's internal dictionary — typically the topology element's own NodeId so two different devices can hold independent locks. ### End-to-end flow 1. Client calls `InitLock(context: "tag")` on a device's Lock. 2. The method's `OnCall` runs the bound `ILockService.InitLock`, which checks the per-device record: - If unlocked / expired → record `(SessionId, "tag", UserId, now + duration)`, return `Ok` (0). - If already locked by another session → return `AlreadyLocked` (1). 3. `RenewLock` (same session) extends the expiry timestamp; from a different session it returns `WrongClient` (2). 4. `ExitLock` removes the record (subject to ownership check). 5. `BreakLock` removes the record regardless of ownership; intended for administrative recovery. 6. If the owning session closes (server-emitted `SessionClosing` event), the `DefaultLockService` walks its records and releases anything that was held by that session. ### Hosting Register the service as a singleton through standard `Microsoft.Extensions.DependencyInjection`: ```csharp services.AddSingleton(); ``` Then attach the session-close hook from inside a `ConfigureDevicesFor` configurator once the server's `SessionManager` is available: ```csharp .ConfigureDevicesFor(ctx => { ILockService svc = ctx.GetRequiredService(); if (svc is DefaultLockService defaultSvc) { defaultSvc.AttachToSessionManager(ctx.Manager.Server.SessionManager); } foreach (DeviceState device in ctx.Manager.PredefinedNodes .Values.OfType()) { device.Lock?.BindToLockService(device.NodeId, svc); } }); ``` ## Software update The software-update facet implements OPC 10000-100 workflows for loading, preparation, installation, power cycle, and confirmation. Attach it with `WithSoftwareUpdate`; application handlers supply device-specific behavior. The SDK supplies the state-machine wiring and file-transfer infrastructure. ### Server-side: package store `ISoftwarePackageStore` stores package metadata and payloads. `ISoftwareFolder` maintains each device's Current / Previous / Future versions. Both have memory and file-system implementations. See [Software Update storage abstractions](SoftwareUpdate.md#storage-abstractions) for their contracts and persistence choices. #### Surface Use the [package-store contract](SoftwareUpdate.md#package-store-contract) for operations, metadata, and stream ownership. The Software Update guide also owns `WithSoftwareUpdate` options and handler hooks. #### `FileSystemPackageStore` layout Use the store through `ISoftwarePackageStore`, rather than modifying its backing files directly. See [file-system layout and provider composition](SoftwareUpdate.md#file-system-layout-and-provider-composition) for the file-system provider and persistence options. #### Composing with `IFileSystemProvider` Reuse the configured provider as described in [provider composition](SoftwareUpdate.md#file-system-layout-and-provider-composition). ### Hosting Register the package store with the host and attach it to the device's software-update facet. See the [hosted-server walkthrough](SoftwareUpdate.md#hosted-server-walkthrough) and [package seeding example](SoftwareUpdate.md#seeding-a-package). ### Client-side software update `SoftwareUpdateClient` reads versions, uploads packages, and exposes typed state-machine operations. The usual workflow is upload, prepare, install, and confirm; supported steps depend on the device's advertised update capabilities. See [uploading a package](SoftwareUpdate.md#uploading-a-package) and [typed state-machine operations](SoftwareUpdate.md#typed-part-16-state-machine-surface) for runnable call sequences. The DI client integration registers `Func>` through `services.AddOpcUa().AddClient(...).AddOpcUaDi()`. ## Client helpers Beyond `DiDeviceClient` and `DiDiscoveryClient`, the `Opc.Ua.Di.Client` library ships three typed client wrappers that compose the standard OPC UA `Session` surface. | Helper | Purpose | |--------|---------| | `DiLockClient` | Wraps the four `LockingServicesType` methods. | | `DiTopologyClient` | Browses `DeviceSet`, `NetworkSet`, `DeviceTopology`. | | `SoftwareUpdateClient` | Reads the software-version property of a `SoftwareUpdateType` instance. | All three live in `Opc.Ua.Di.Client` and are registered automatically when applications call `services.AddOpcUa().AddClient(...).AddOpcUaDi()` on the client-side builder. ### DiLockClient ```csharp public sealed class DiLockClient { public DiLockClient(ISession session, NodeId lockNodeId, ITelemetryContext telemetry); public ValueTask InitLockAsync(string context, CancellationToken ct = default); public ValueTask RenewLockAsync(CancellationToken ct = default); public ValueTask ExitLockAsync(CancellationToken ct = default); public ValueTask BreakLockAsync(CancellationToken ct = default); } ``` Each method invokes the corresponding `LockingServicesType.{Method}` generated NodeId on the server and returns the integer status code (see the [`LockStatus` table](#status-codes) for the OPC spec values). The constructor takes the NodeId of the `Lock` instance — typically the `device.Lock` child, not the device itself. Resolve it via `Session.TranslateBrowsePathsToNodeIdsAsync` or by browsing the device's `HasComponent` references for a child named `"Lock"`. ```csharp DiLockClient lockClient = new(session, lockNodeId, telemetry); int status = await lockClient.InitLockAsync("client-tag"); if (status == LockStatus.Ok) { try { // Do work that requires exclusive access. } finally { await lockClient.ExitLockAsync(); } } ``` ### DiTopologyClient ```csharp public sealed class DiTopologyClient { public DiTopologyClient(ISession session, ITelemetryContext telemetry); public NodeId DeviceSetId { get; } public NodeId NetworkSetId { get; } public NodeId DeviceTopologyId { get; } public IAsyncEnumerable EnumerateDevicesAsync(CancellationToken ct = default); public IAsyncEnumerable EnumerateNetworksAsync(CancellationToken ct = default); public IAsyncEnumerable EnumerateChildrenAsync(NodeId parentNodeId, CancellationToken ct = default); } public sealed record TopologyEntry( NodeId NodeId, string DisplayName, NodeId TypeDefinitionId, QualifiedName BrowseName); ``` `EnumerateDevicesAsync` and `EnumerateNetworksAsync` browse the hierarchical references of `DeviceSet` and `NetworkSet` and return the direct children that are Objects. `EnumerateChildrenAsync` provides the same surface for arbitrary nodes — useful when walking the `DeviceTopology` tree. ```csharp DiTopologyClient topology = new(session, telemetry); await foreach (TopologyEntry device in topology.EnumerateDevicesAsync()) { Console.WriteLine($"{device.BrowseName}: {device.DisplayName}"); } ``` ### Hosting registration When `services.AddOpcUa().AddClient(o => { ... }).AddOpcUaDi()` is called, the following factories are registered as singletons: ```csharp Func> Func> Func> Func> IDiDiscoveryService ``` Inject the factory you need into your application services; the factory opens (or reuses) the lazy `ManagedSession` on first call. ```csharp public sealed class MyMonitor( Func> topologyFactory, Func> deviceFactory, Func> lockFactory) { public async Task ScanAsync(CancellationToken ct) { DiTopologyClient topo = await topologyFactory(ct); await foreach (TopologyEntry e in topo.EnumerateDevicesAsync(ct)) { DiDeviceClient dev = await deviceFactory(e.NodeId, ct); DeviceIdentification id = await dev.ReadIdentificationAsync(ct); // ... } } } ``` ## What is supported (OPC 10000-100) References are to the OPC 10000-100 (DI v1.05) specification sections. ### Foundation — nameplates and topology elements - `TopologyElementType` (§5.2) — abstract base exposed through `ITopologyElementBuilder`. - `IVendorNameplateType` (§5.10) exposes `Manufacturer`, `Model`, `SerialNumber`, `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, `DeviceManual`, `DeviceClass`, `ProductInstanceUri`, and `ProductCode`. Populate these properties with `IDeviceBuilder.WithIdentification(...)`. - `ITagNameplateType` (§5.11) — AssetId, ComponentName, DeviceRevision. Populated by the same builder. - `IDeviceHealthType` (§5.12) — DeviceHealth enum plus the four NAMUR alarm references. - `IAssetLocationIndicationType` (§5.13) — `StartLocationIndication` / `StopLocationIndication` methods. - `IOperationCounterType` (§5.14) — PowerOnDuration, OperationDuration, EstimatedReturnedOperationDuration, EstimatedReturnedPowerOnDuration. ### Device tree - `ComponentType` (§5.3) — abstract; materialised via `IDeviceBuilder`. - `DeviceType` (§5.4) — `IDeviceBuilder.WithDeviceType(factory)`. - `SoftwareType`, `BlockType`, `ConfigurableObjectType` (§5.4 advanced sub-types) — `IDeviceBuilder.AddSoftware`, `AddBlock`, `AddConfigurableObject`. ### Topology references - `ConnectsTo` (§5.6.2) — `ITopologyElementBuilder.ConnectsTo(other)`. - `ConnectsToParent` (§5.6.3) — `ITopologyElementBuilder.ConnectsToParent(other)`. - `IsOnline` (§5.6.4) — exposed for online-component wiring. - `UpdateParent`, `CanUpdate` (§5.6.5–§5.6.6) — used by software update. ### Topology containers - `DeviceSet` (§6.2) — well-known instance, auto-created by `DiNodeManager` under `ObjectsFolder`; `IDeviceBuilder` parents devices here by default. - `NetworkSet` (§6.3) — well-known instance. - `DeviceTopology` (§6.4) — well-known instance; topology references attach here. - `NetworkType`, `ConnectionPointType`, `ProtocolType` (§6.5–§6.7). ### Functional groups (§5.7) All eight well-known Device Integration functional groups are exposed through typed methods on `ITopologyElementBuilder`; device builders inherit the same surface: - `Identification`, `Configuration`, `Maintenance`, `Diagnostics`, `Statistics`, `Status`, `Operational`, `OperationCounters`. Custom groups go through `WithFunctionalGroup(qualifiedName, action)`. ### Lock service (§10.5) - `LockingServicesType` and four method types: `InitLockMethodType`, `RenewLockMethodType`, `ExitLockMethodType`, and `BreakLockMethodType`. `ILockService` and `DefaultLockService` wire these methods and provide session ownership, configurable timeouts, and cleanup when a session closes. ### Software update (§10.3) - `SoftwareUpdateType` — orchestrates the loading + state-machine wiring. - `SoftwareLoadingType`, `PackageLoadingType` (§10.3.4) — abstract bases. - `DirectLoadingType`, `CachedLoadingType`, `FileSystemLoadingType` (§10.3.4) — the three loading variants. - `SoftwareVersionType` (§10.3.6) — Manufacturer, ProductInstanceUri, SoftwareRevision, PatchIdentifiers, ReleaseDate, ChangeLog, Hash. - Generated proxies for `PrepareForUpdateStateMachineType` (§10.3.7), `InstallationStateMachineType` (§10.3.8), `PowerCycleStateMachineType` (§10.3.9), and `ConfirmationStateMachineType` (§10.3.10), driven by the application. - Storage abstraction: `ISoftwarePackageStore` with `MemoryPackageStore` and `FileSystemPackageStore` implementations. ### Support info & lifetime indication - `ISupportInfoType` (§5.15) — `IDeviceBuilder.WithSupportInfo(configure)`. Backing folders `DocumentationFiles` / `ImageSet` / `ProtocolSupport` accept `FileState` / `BaseObjectState` children; file content commonly backed by `IFileSystemProvider`. - `LifetimeVariableType` + 6 indication sub-types (§10.6) — `IDeviceBuilder.AddLifetimeIndication(kind, …)`. Classifier kinds: `Time`, `NumberOfParts`, `NumberOfUsages`, `Length`, `Diameter`, `SubstanceVolume`. ### NAMUR alarms (§10.2) - `DeviceHealthDiagnosticAlarmType` (abstract) and the four concrete alarm types (`FailureAlarmType`, `CheckFunctionAlarmType`, `OffSpecAlarmType`, `MaintenanceRequiredAlarmType`) — generated proxies wired via the fluent `IAlarmBuilder` + `ActivatesAlarm` patterns from `src/Opc.Ua.Server/Fluent`. ### DataTypes & VariableTypes The `Opc.Ua.Di` model library includes these source-generated Device Integration DataTypes: - `DeviceHealthEnumeration` - `SoftwareClass` - `LocationIndicationType` - `SoftwareVersionFileType` - `UpdateBehavior` - `FetchResultDataType` - `TransferResultErrorDataType` - `TransferResultDataDataType` - `ParameterResultDataType` ### Not yet implemented - `TransferServicesType` (§10.4) — parameter set transfer. - Offline and online device representations (§6.3) — needed for the `DI Offline` conformance unit and so for the DeviceIntegrationHost facet. - The `UpdateStatus` variable of `SoftwareUpdateType` (§8.4.1.8) and a `FileSystemLoadingType` loading mode — needed for the Direct, Cached and FileSystem Loading facets (see [Runtime conformance advertisement](#runtime-conformance-advertisement)). ## See also - [FileSystemClient](FileSystemClient.md) — file-transfer client used by `SoftwareUpdateClient` and `FileSystemPackageStore`. - [Source-generated NodeManagers](NodeManagers.md#source-generated-node-managers) — the underlying source generator and fluent API.