mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
e78d36ce4b
# Description Layer 2 of the upstream non-UaLens extraction, with core/types prerequisite #4801 (`marcschier-extract-core-and-types`). The initial extraction commit `86ff10d3994f132f7343a929abdae209bc2e69b7` started from `8881e096fb5e4eaa36f3c13f8caa57733373bd19` and copied exactly 13 client/server paths from #4462 at frozen head `390c4a7a51b71da40caec2d22ef74518bbe670ce`, using its original base `5b03e9afa492cdb36049ee52ae844f630f89d8db`. Those initial blobs were source-identical; the core/types layer was not duplicated or rewritten. - Add bounded temporary-file copying with byte/request limits, session ownership checks, and independent bounded cleanup. - Refresh revised monitored-item parameters after snapshot transfer and notify the target handler after successful handle synchronization. - Apply WSS opening deadlines while preserving caller cancellation, and cover the wire envelope and request timeout behavior. - Make hosted server presets honor role configuration without replacing an explicitly registered role manager; include the corresponding tests and documentation. **Review follow-up:** commit `a5c8fe0884ad3346106529deba71d93c5df20a9a` intentionally corrects three defects in the extracted bounded-copy implementation. It captures one `SessionRequestContext` for managed transfers, calls the standard `FileType.Close` declaration without metadata discovery, and retains valid generation handles before rejecting non-Good status or malformed metadata. Read/Close remain strictly validated, and transfer/cleanup failures remain independently visible. The follow-up also adds 21 direct-client regression cases and updates `docs/FileSystemClient.md`, bringing this PR to 15 changed paths. The current bounded-copy implementation is therefore intentionally no longer identical to the frozen source. No UaLens projects/UI, AMB or Machinery implementations, samples, CI/native renderer changes, release changes, or generic infrastructure files are included. The follow-up does not merge/rebase a lower or upper stack layer. **Current review-fix validation:** normal analyzers and warnings-as-errors remained enabled, using the existing SDK **10.0.401** without changing `global.json`, installing an SDK, or adding suppressions. The final file-system and session-context selection passed **241 tests on net10.0 and 241 tests on net48**, with **0 failed and 0 skipped** on each. Both selections include all 21 new regressions. Before the production fix, the first 20-case regression suite had 13 failures and 7 passes, reproducing the lost cleanup, cancellation, session-recreation and Close-discovery defects. An additional deterministic recovery-gate race case was then added and passed on both frameworks. Informational-severity formatting/style/analyzer checks passed for the changed C# files. | Review requirement | Direct regression evidence | | --- | --- | | Pin the managed generation through copying and cleanup | `CopyRejectsRecreatedManagedGenerationAsync`, `CopyRejectsRecoveryBetweenGenerationAndReadDispatchAsync` | | Preserve a generation response that returns after cancellation | `ManagedCopyClosesHandleReturnedAfterCancellationAsync` with immediate and delayed responses through a real managed transport lease | | Do not spend the Close deadline discovering metadata | `CopyUsesDeclarationCloseWithoutDiscoveryAsync` for ordinary and managed sessions | | Retain valid handles before rejecting generation status/metadata | `CopyClosesHandleBeforeRejectingGenerationStatusAsync`, `CopyClosesHandleFromMalformedGenerationResponseAsync` | | Preserve error semantics and strict Read/Close validation | `CopyRejectsGenerationWithoutInventingHandleAsync`, `CopyPreservesGenerationAndCleanupFailuresAsync`, `CopyKeepsReadAndCloseStatusValidationAsync` | <details> <summary>Exact review-fix validation commands and results</summary> Commands ran from this layer's repository root. The following variables expand the actual executable, filters and results paths. Console logs and TRX reports were retained in the session artifacts. ```powershell $dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' $env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk' $env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' $client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj' $filter = 'FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem|FullyQualifiedName~Opc.Ua.Client.Tests.SessionContexts' $r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net10' $r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net48' # Before modifying production code, after the new tests compiled: & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-red.trx' --results-directory $r10 --verbosity quiet # Expected regression failure: Passed: 7, Failed: 13, Skipped: 0. # After the production fix, before adding the explicit queued-recovery case: & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-green.trx' --results-directory $r10 --verbosity quiet # Passed: 20, Failed: 0, Skipped: 0. # Final complete affected selections, including all 21 new cases: & $dotnet test $client --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r48 --verbosity quiet # Passed: 241, Failed: 0, Skipped: 0. & $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r10 --verbosity quiet # Passed: 241, Failed: 0, Skipped: 0. $sourceFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\src\Opc.Ua.Client\FileSystem\TemporaryFileTransferClient.BoundedRead.cs' $testFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\tests\Opc.Ua.Client.Tests\FileSystem\TemporaryFileTransferClientBoundedReadTests.cs' $env:CustomTestTarget = 'net48' & $dotnet format 'src\Opc.Ua.Client\Opc.Ua.Client.csproj' --include $sourceFile --no-restore --verify-no-changes --severity info --verbosity quiet # Passed, exit 0. $env:CustomTestTarget = 'net10.0' & $dotnet format $client --include $testFile --no-restore --verify-no-changes --severity info --verbosity quiet # Passed, exit 0. git diff --check # Passed. CRLF, final newlines and the 120-column C# limit were also checked. ``` Initial no-restore test attempts after switching frameworks exited without executing tests and were not counted as passes. Diagnostic `dotnet build ... --no-restore` runs reported `NETSDK1005` for the missing framework assets; normal restore was then allowed. Test-authoring compile errors were corrected before recording the red run, without suppressions. Intermediate broader selections passed 240/240 on each framework before the queued-recovery case increased the final count to 241. </details> **Initial-extraction validation (commit `86ff10d3994f132f7343a929abdae209bc2e69b7`):** these results are historical, not a claim that the broader initial selections were rerun after the review fix. The server test assembly was built as a dependency of the client project before its `--no-build` runs. | Initial targeted scope | net10.0 | net48 | | --- | --- | --- | | Client subscriptions and file system | Passed: 546, Failed: 0, Skipped: 0 | Passed: 542, Failed: 0, Skipped: 0 | | Hosted roles, identity, composition, fluent APIs and builder coverage | Passed: 164, Failed: 0, Skipped: 0 | Passed: 164, Failed: 0, Skipped: 0 | | WebAPI WSS transport | Passed: 37, Failed: 0, Skipped: 0 | Not supported: the repository excludes this .NET 8+ test project on net48 | | V2 transfer and snapshot integration | Passed: 6, Failed: 0, Skipped: 0 | Passed: 6, Failed: 0, Skipped: 0 | The first broader net48 hosting run had **9 startup timeouts and certificate leaks** (155 passed). A single startup case reproduced the failure. With only a shorter `NUnit.WorkDirectory`, the isolated case and then all 164 hosting cases passed, with certificate leak tracking enabled and no teardown leaks. This is consistent with the fixture's documented .NET Framework PKI path-length constraint; no source or diagnostic suppression was changed. Both failed runs and successful reruns are recorded below. **Deferred coverage:** the broader domain-level `MachineryResultTransferTests*.cs` fixtures remain in Machinery layer #4804. They were not imported or run here. This review follow-up adds direct-client coverage for the three defects instead. The Machinery owner has been informed that the old expectations for failed Close discovery and zero cleanup after a returned non-Good generation response must be reconciled when the lower fixes are propagated. **Not run:** complete `UA.slnx` suites on net48/net10.0, a coverage measurement, CI/CodeQL completion, native/fuzz qualifications, or package publishing. This remains a draft pending full-stack integration and CI evidence. <details> <summary>Initial-extraction validation command arguments and outcomes</summary> All commands ran from this layer's repository root. Variables below expand the executable, project paths, filters and results directories used by the actual commands; test output was also retained in session-local log files. ```powershell $dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' $env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk' $env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' $r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net10' $r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net48' $client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj' $server = 'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' $webapi = 'tests\Opc.Ua.Bindings.Https.WebApi.Tests\Opc.Ua.Bindings.Https.WebApi.Tests.csproj' $subscriptions = 'tests\Opc.Ua.Subscriptions.Tests\Opc.Ua.Subscriptions.Tests.csproj' $clientFilter = 'FullyQualifiedName~Opc.Ua.Client.Subscriptions|FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem' $serverFilter = 'FullyQualifiedName~HostedRolePresetTests|FullyQualifiedName~IdentityHostingTests|FullyQualifiedName~ServerFluentApiHostingTests|FullyQualifiedName~ServerCompositionHostingTests|FullyQualifiedName~OpcUaServerBuilderExtensionsCoverageTests' $subscriptionFilter = 'FullyQualifiedName~Opc.Ua.Subscriptions.Tests.TransferSubscriptionTests|FullyQualifiedName~Opc.Ua.Subscriptions.Tests.SubscriptionSnapshotTests' & $dotnet --version # Passed: 10.0.401. & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet # Exit 0 but no tests executed: missing restore metadata. NOT counted as validation. & $dotnet build $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --verbosity quiet # Failed: NETSDK1004, missing obj\project.assets.json. Normal restore was then allowed in test commands. & $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 546, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $serverFilter --logger 'trx;LogFileName=server-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 164, Failed: 0, Skipped: 0. & $dotnet test $webapi -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~WebApiWssTransportChannelTests' --logger 'trx;LogFileName=webapi-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 37, Failed: 0, Skipped: 0. & $dotnet test $client -c Release -f net48 -p:CustomTestTarget=net48 --filter $clientFilter --logger 'trx;LogFileName=client-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 542, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48.trx' --results-directory $r48 --verbosity quiet # Failed: Passed: 155, Failed: 9, Skipped: 0; startup timeouts and 9 undisposed certificates at teardown. & $dotnet test $subscriptions -c Release -f net48 -p:CustomTestTarget=net48 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 6, Failed: 0, Skipped: 0. & $dotnet test $subscriptions -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 6, Failed: 0, Skipped: 0. & $dotnet msbuild $webapi -p:CustomTestTarget=net48 -p:TargetFramework=net48 -getProperty:TargetFramework,RestrictForLegacyTfm,_RestrictedToLegacyTfm,IsTestProject,EnableDefaultCompileItems # Passed: net48 / true / true / false / false. Confirms the net48 project is an empty non-test shell. # Each following diagnostic invocation had certificate allocation tracking enabled. $env:OPCUA_CERTIFICATE_LEAK_TRACKING = '1' & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~HostedRolePresetTests' --logger 'trx;LogFileName=hosted-role-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 10, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48.trx' --results-directory $r48 --verbosity quiet # Failed: Passed: 0, Failed: 1, Skipped: 0; 30-second startup timeout and one certificate leak. # A fresh, dedicated short working directory was created for the following two runs. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556' # Passed: 1, Failed: 0, Skipped: 0; no certificate leak. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556' # Passed: 164, Failed: 0, Skipped: 0; no certificate leak or assembly-level teardown error. # The dedicated short working directory and its generated PKI contents were removed afterward. ``` TRX inspection confirmed all initially added client (3), WSS (4), and hosted-role (10) regression cases executed on supported frameworks. The initial 13-path set and source blobs were verified before the extraction commit. A separate mistyped SDK availability probe (`uals-528-deep-repro` instead of `ualens-528-deep-repro`) failed before launching any process; all actual builds/tests used the correct verified SDK above. </details> ## Related Issues Related to #4461; extracted from source PR #4462. Neither is closed by this PR. Prerequisite: #4801, the core/types layer. _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received.
2022 lines
93 KiB
Markdown
2022 lines
93 KiB
Markdown
# Dependency Injection
|
|
|
|
This guide describes the unified `Microsoft.Extensions.DependencyInjection`
|
|
surface for OPC UA .NET Standard libraries. Start with
|
|
`services.AddOpcUa()`, which returns an `IOpcUaBuilder`. Feature libraries
|
|
extend the builder with their own fluent `.AddXxx(...)` methods.
|
|
For a complete, runnable client and server, see
|
|
[Getting started](GettingStarted.md); this guide is the reference for the
|
|
builder extensions.
|
|
|
|
The dependency injection surface is consistent across:
|
|
|
|
- The OPC UA Core stack (`src/Opc.Ua.Core`)
|
|
- Application configuration (`src/Opc.Ua.Configuration`)
|
|
- The client (`src/Opc.Ua.Client`)
|
|
- The complex types client (`src/Opc.Ua.Client.ComplexTypes`)
|
|
- Alarms and conditions client (`src/Opc.Ua.Client.Alarms`)
|
|
- The server (`src/Opc.Ua.Server`)
|
|
- The GDS client (`src/Opc.Ua.Gds.Client`)
|
|
- The GDS server (`src/Opc.Ua.Gds.Server`)
|
|
- The LDS server (`src/Opc.Ua.Lds.Server`)
|
|
- The WoT Connectivity server (`src/Opc.Ua.WotCon.Server`)
|
|
- The WoT Connectivity client (`src/Opc.Ua.WotCon.Client`)
|
|
- The PubSub stack (`src/Opc.Ua.PubSub`,
|
|
`src/Opc.Ua.PubSub.Udp`, `src/Opc.Ua.PubSub.Mqtt`,
|
|
`src/Opc.Ua.PubSub.Server`) — see [`PubSub.md`](PubSub.md)
|
|
for the full library reference.
|
|
|
|
Public constructors and factories remain available for applications that do
|
|
not use dependency injection. Examples include `new ApplicationInstance(telemetry)`,
|
|
`new StandardServer(telemetry)`, `new LdsServer(telemetry)`, and
|
|
`new ManagedSession(...)`. Use dependency injection when you want the .NET
|
|
Generic Host to manage application lifetime, logging, and configuration. Use
|
|
the manual constructors when you need finer control.
|
|
|
|
## Contents
|
|
|
|
- [Quick reference](#quick-reference)
|
|
- [Root: `services.AddOpcUa()`](#root-servicesaddopcua)
|
|
- [Buffer managers](#buffer-managers)
|
|
- [Shared application configuration](#shared-application-configuration)
|
|
- [Application identity and certificate defaults](#application-identity-and-certificate-defaults)
|
|
- [Advanced application certificate validation](#advanced-application-certificate-validation)
|
|
- [Options binding](#options-binding)
|
|
- [Server feature](#server-feature)
|
|
- [Server metadata](#server-metadata)
|
|
- [Node-manager factories](#node-manager-factories)
|
|
- [Post-startup tasks](#post-startup-tasks)
|
|
- [Alias-name stores and standard browse nodes](#alias-name-stores-and-standard-browse-nodes)
|
|
- [Localized resources](#localized-resources)
|
|
- [Migrating with an existing configuration XML file](#migrating-with-an-existing-configuration-xml-file)
|
|
- [Server security and resource controls](#server-security-and-resource-controls)
|
|
- [First-class server options](#first-class-server-options)
|
|
- [Committed session bindings](#committed-session-bindings)
|
|
- [Server-side reverse connect](#server-side-reverse-connect)
|
|
- [Operation limits](#operation-limits)
|
|
- [User token policies](#user-token-policies)
|
|
- [Identity (server)](#identity-server)
|
|
- [Fluent shortcuts and one-shot presets](#fluent-shortcuts-and-one-shot-presets)
|
|
- [Positioning](#positioning)
|
|
- [Robotics](#robotics)
|
|
- [Client feature](#client-feature)
|
|
- [Client: using an existing configuration XML file](#client-using-an-existing-configuration-xml-file)
|
|
- [Client: loading the configuration eagerly](#client-loading-the-configuration-eagerly)
|
|
- [Fluent shortcuts](#fluent-shortcuts)
|
|
- [Identity (client)](#identity-client)
|
|
- [Complex types](#complex-types)
|
|
- [Alarms and conditions](#alarms-and-conditions)
|
|
- [Programs](#programs)
|
|
- [Client-side reverse connect](#client-side-reverse-connect)
|
|
- [Channel manager](#channel-manager)
|
|
- [Application instance (advanced)](#application-instance-advanced)
|
|
- [GDS Client](#gds-client)
|
|
- [GDS Server](#gds-server)
|
|
- [Identity (GDS server)](#identity-gds-server)
|
|
- [LDS Server](#lds-server)
|
|
- [WoT Connectivity Server](#wot-connectivity-server)
|
|
- [WoT Connectivity Client](#wot-connectivity-client)
|
|
- [Combined hosts](#combined-hosts)
|
|
- [Native AOT](#native-aot)
|
|
- [Telemetry](#telemetry)
|
|
- [See also](#see-also)
|
|
|
|
## Quick reference
|
|
|
|
| Feature library | Method on `IOpcUaBuilder` | Returns | Hosted? | Section |
|
|
|--------------------------------|------------------------------------------|--------------------------|---------|--------------------------|
|
|
| `Opc.Ua.Core` (root) | `services.AddOpcUa()` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Configuration` | `builder.ConfigureApplication(opt => …)` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Configuration` | `builder.AddApplicationInstance()` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Core` (crypto) | `builder.AddCryptoProvider(crypto => …)` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Security.Pkcs11` | `builder.AddPkcs11CertificateStore(…)` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Client` | `builder.AddClient(opt => …)` | `IOpcUaClientBuilder` | — | `OpcUa:Client` |
|
|
| `Opc.Ua.Client.ComplexTypes` | `builder.AddComplexTypes()` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Client.Alarms` (within `Opc.Ua.Client`) | `builder.AddAlarms()` | `IOpcUaBuilder` | — | — |
|
|
| `Opc.Ua.Server` | `builder.AddServer(opt => …)` | `IOpcUaServerBuilder` | yes | `OpcUa:Server` |
|
|
| `Opc.Ua.Server` (node manager)| `builder.AddNodeManager<T>()` | `IOpcUaServerBuilder` | — | — |
|
|
| `Opc.Ua.Server` (runtime NodeSet) | `builder.AddRuntimeNodeSet(…)` | `IOpcUaServerBuilder` | — | — |
|
|
| `Opc.Ua.Server` (live NodeManagers) | resolve `INodeManagerLifecycle` | `INodeManagerLifecycle` | yes | — |
|
|
| `Opc.Ua.Positioning.Server` | `serverBuilder.AddPositioningServer()` / `AddPositioningFor<T>()` | `IPositioningServerBuilder` | yes (via `AddServer`) | — |
|
|
| `Opc.Ua.Positioning.Client` | `clientBuilder.AddPositioningClient()` | `IOpcUaClientBuilder` | — | — |
|
|
| `Opc.Ua.Robotics.Server` | `serverBuilder.AddRobotics(opt => …)` | `IOpcUaServerBuilder` | yes (via `AddServer`) | — |
|
|
| `Opc.Ua.Robotics.Server` (model) | `serverBuilder.AddRoboticsModel<T>()` | `IOpcUaServerBuilder` | — | — |
|
|
| `Opc.Ua.Robotics.Server` (build) | `serverBuilder.ConfigureRobotics(…)` / `ConfigureRoboticsFor<T>(…)` | `IOpcUaServerBuilder` | — | — |
|
|
| `Opc.Ua.Robotics.Client` | `clientBuilder.AddRoboticsClient()` | `IOpcUaClientBuilder` | — | — |
|
|
| `Opc.Ua.Gds.Client` | `builder.AddGdsClient(opt => …)` | `IGdsClientBuilder` | — | `OpcUa:Gds:Client` |
|
|
| `Opc.Ua.Gds.Server` | `builder.AddGdsServer(opt => …)` | `IGdsServerBuilder` | yes | `OpcUa:Gds:Server` |
|
|
| `Opc.Ua.Lds.Server` | `builder.AddLdsServer(opt => …)` | `ILdsServerBuilder` | yes | `OpcUa:Lds` |
|
|
| `Opc.Ua.WotCon.Server` | `builder.AddWotConServer(opt => …)` | `IWotConServerBuilder` | yes (via `AddServer`) | `OpcUa:WotCon:Server` |
|
|
| `Opc.Ua.WotCon.Client` | `builder.AddWotConClient(opt => …)` | `IOpcUaBuilder` | — | `OpcUa:WotCon:Client` |
|
|
| `Opc.Ua.PubSub` | `builder.AddPubSub(opt => …)` | `IPubSubBuilder` | yes | `OpcUa:PubSub` |
|
|
| `Opc.Ua.PubSub` (publish-only) | `builder.AddPubSubPublisher(opt => …)` | `IPubSubBuilder` | yes | `OpcUa:PubSub` |
|
|
| `Opc.Ua.PubSub` (subscribe-only) | `builder.AddPubSubSubscriber(opt => …)`| `IPubSubBuilder` | yes | `OpcUa:PubSub` |
|
|
| `Opc.Ua.PubSub` (SKS client) | `builder.AddPubSubSecurityKeyServiceClient(...)` | `IPubSubBuilder` | — | `OpcUa:PubSub:Sks:Client`|
|
|
| `Opc.Ua.PubSub` (SKS server) | `builder.AddPubSubSecurityKeyServiceServer(...)` | `IPubSubBuilder` | yes | `OpcUa:PubSub:Sks:Server`|
|
|
| `Opc.Ua.PubSub.Udp` | `pubSub.AddUdpTransport()` | `IPubSubBuilder` | — | — |
|
|
| `Opc.Ua.PubSub.Mqtt` | `pubSub.AddMqttTransport()` | `IPubSubBuilder` | — | — |
|
|
| `Opc.Ua.PubSub.Server` | `serverBuilder.AddPubSubAddressSpace(...)` | `IPubSubServerBuilder` | yes | `OpcUa:PubSub:AddressSpace` |
|
|
| `Opc.Ua.Redundancy.Server` | `serverBuilder.UseDistributedAddressSpace(...)` / `UseReplicatedAddressSpace(...)` / `UseActiveActiveRedundancy(...)` / `UsePeerDiscovery(...)` / `AddServerRedundancy(...)` | `IOpcUaServerBuilder` | yes (via `AddServer`) | see [HighAvailability.md](HighAvailability.md) |
|
|
| `Opc.Ua.Redundancy.Kubernetes` | `serverBuilder.UseKubernetesRaftConsensus(...)` | `IOpcUaServerBuilder` | yes (via `AddServer`) | see [Kubernetes.md](Kubernetes.md) |
|
|
|
|
Identity-provider extensions hang off `IOpcUaServerBuilder`,
|
|
`IOpcUaClientBuilder`, and `IGdsServerBuilder`:
|
|
|
|
| Extension | Builder | Section |
|
|
|-----------------------------------------------|--------------------------|----------------------------------|
|
|
| `AddDefaultIdentityAuthenticators(...)` | server, gds | `OpcUa:Server:Identity:Defaults` |
|
|
| `AddIdentityAuthenticator<T>()` | server, gds | — |
|
|
| `AddIdentityAuthenticator(instance)` / `(factory)` | server | — |
|
|
| `AddIdentityAugmenter<T>()` | server, gds | — |
|
|
| `AddGdsApplicationSelfAdminProvider()` | gds | — |
|
|
| `AddJwtIssuer(...)` | server, gds | `OpcUa:Server:Identity:Issuers[]`|
|
|
| `WithAuthorizationService(...)` / `<TIssuer>()` | gds | — |
|
|
| `WithKeyCredentialPush(...)` | server | — |
|
|
| `AddSessionlessInvocation(...)` | server | — |
|
|
| `ConfigureRoles(...)` | server, gds | `OpcUa:Server:Roles` |
|
|
| `AddIdentityProvider(...)` / `<T>()` | client | `OpcUa:Client:Identity` |
|
|
| `AddAccessTokenProvider(...)` / `<T>()` | client | — |
|
|
|
|
See the [Identity (server)](#identity-server),
|
|
[Identity (client)](#identity-client), and
|
|
[Identity (GDS server)](#identity-gds-server) sections below and the
|
|
full [Identity Providers](IdentityProviders.md) guide.
|
|
|
|
Server features marked **Hosted? = yes** register an `IHostedService` so
|
|
the .NET Generic Host (`Host.CreateApplicationBuilder(args)`) owns their
|
|
lifetime, certificate setup, and Ctrl+C / SIGTERM handling.
|
|
|
|
## Root: `services.AddOpcUa()`
|
|
|
|
`services.AddOpcUa()` is the only entry point. It does three things:
|
|
|
|
1. Registers `ITelemetryContext` as a `ServiceProviderTelemetryContext`
|
|
singleton (via `TryAddSingleton`, so any prior user registration
|
|
wins).
|
|
2. Registers the default `IBufferManagerFactory` and
|
|
`BufferManagerFactoryOptions` singletons (also via `TryAddSingleton`).
|
|
3. Returns an `IOpcUaBuilder` whose `.Services` property exposes the
|
|
underlying `IServiceCollection` for advanced scenarios.
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
services.AddOpcUa();
|
|
```
|
|
|
|
`AddOpcUa` is idempotent. The returned `IOpcUaBuilder` derives from the
|
|
older `IDependencyInjectionBuilder` so existing
|
|
`builder.AddLogging()` / `builder.AddMetrics()` extensions still compile
|
|
unchanged. New `AddLogging(this IOpcUaBuilder)` /
|
|
`AddMetrics(this IOpcUaBuilder)` overloads return `IOpcUaBuilder` for
|
|
fluent chaining into feature methods:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddLogging(b => b.AddConsole())
|
|
.AddMetrics()
|
|
.AddServer(o => /* … */)
|
|
.AddNodeManager<MyNodeManagerFactory>();
|
|
```
|
|
|
|
### Buffer managers
|
|
|
|
Transport listeners and channels resolve `IBufferManagerFactory` from dependency injection. The default factory selects `FastBufferManager` in Release builds, `CookieBufferManager` in Debug builds, and `TracingBufferManager` when the stack is compiled with `TRACK_MEMORY`.
|
|
|
|
The factory does not limit outstanding buffers by default. Incomplete server
|
|
messages have a separate, enabled-by-default
|
|
[chunk reassembly budget](RateLimiting.md#incomplete-messages), so they do not
|
|
consume the allocation capacity needed for unrelated requests and responses.
|
|
|
|
Register options before `AddOpcUa()` to select an implementation explicitly or
|
|
opt into an outstanding-buffer budget:
|
|
|
|
```csharp
|
|
services.AddSingleton(new BufferManagerFactoryOptions
|
|
{
|
|
ImplementationKind = BufferManagerImplementationKind.Fast,
|
|
MaxOutstandingBytesPerProcess = 256L * 1024 * 1024
|
|
});
|
|
|
|
services.AddOpcUa()
|
|
.AddOpcTcpTransport()
|
|
.AddHttpsTransport();
|
|
```
|
|
|
|
When `MaxOutstandingBytesPerProcess` is positive, the singleton factory wraps every
|
|
manager it creates with `LimitingBufferManager` and shares one
|
|
`BufferManagerMemoryLimiter` across them. A rent waits synchronously for
|
|
capacity. Use this optional policy only where
|
|
buffer returns can progress independently; it is not a substitute for the
|
|
nonblocking server reassembly budget. A single rent whose conservative expected
|
|
size exceeds the budget fails immediately rather than waiting forever.
|
|
Zero or `null` leaves allocation unrestricted. Independently constructed
|
|
factories have independent budgets. Accounting uses actual rented-array lengths,
|
|
including pool rounding and metadata, and excludes arrays returned to the pool.
|
|
|
|
Capacity changes notify only currently registered renters; idle buffer returns
|
|
do not accumulate wakeups. Cancellation removes any unclaimed wakeup, and
|
|
disposal wakes all blocked renters before releasing the wait primitive.
|
|
|
|
Applications can replace the complete policy by registering an `IBufferManagerFactory` before `AddOpcUa()`:
|
|
|
|
```csharp
|
|
services.AddSingleton<IBufferManagerFactory, MyBufferManagerFactory>();
|
|
services.AddOpcUa();
|
|
```
|
|
|
|
The existing `new BufferManager(name, maxBufferSize, telemetry)` path remains available for direct creation. `BufferManager` is a compatibility facade over `IBufferManager`; `FastBufferManager`, `CookieBufferManager`, `TracingBufferManager`, and `LimitingBufferManager` can also be created directly for advanced scenarios.
|
|
|
|
## Shared application configuration
|
|
|
|
`ConfigureApplication(...)` defines the application identity, PKI root, and common certificate-validation settings once. `AddClient(...)`, `AddServer(...)`, or both contribute their feature-specific sections to one `ApplicationConfiguration`; the provider calls `ApplicationConfigurationBuilder.CreateAsync`, validates the completed configuration, and ensures the application instance certificate exists before a client connects or a hosted server starts. Use `ConfigureApplication(...)` for combined client/server hosts, or when several `AddClient(...)`/`AddServer(...)` registrations must share one application identity and certificate lifecycle. A client-only host can instead set the same identity fields directly on `OpcUaClientOptions` inside `AddClient(...)` (see [Client feature](#client-feature)) without a separate `ConfigureApplication(...)` call.
|
|
|
|
```csharp
|
|
IOpcUaBuilder opcUa = services.AddOpcUa()
|
|
.ConfigureApplication(options =>
|
|
{
|
|
options.ApplicationName = "MyApplication";
|
|
options.ApplicationUri = "urn:localhost:MyApplication";
|
|
options.ProductUri = "uri:example.com:MyApplication";
|
|
options.SubjectName = "CN=MyApplication, O=Example, DC=localhost";
|
|
options.PkiRoot = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"Example",
|
|
"MyApplication",
|
|
"pki");
|
|
options.AutoAcceptUntrustedCertificates = false;
|
|
options.RejectSHA1SignedCertificates = true;
|
|
options.MinimumCertificateKeySize = 2048;
|
|
|
|
// Code-only advanced settings, applied after the values above.
|
|
options.ConfigureSecurity = security => security
|
|
.SetMaxRejectedCertificates(20)
|
|
.SetRejectUnknownRevocationStatus(true)
|
|
.SetUseValidatedCertificates(false)
|
|
.SetSendCertificateChain(true);
|
|
});
|
|
|
|
opcUa.AddClient(options =>
|
|
{
|
|
options.Session = new ManagedSessionOptions
|
|
{
|
|
SessionName = "MyClient"
|
|
};
|
|
});
|
|
|
|
opcUa.AddServer(options =>
|
|
{
|
|
options.EndpointUrls.Add("opc.tcp://localhost:4840/MyApplication");
|
|
});
|
|
```
|
|
|
|
When both features are registered, the shared configuration has `ApplicationType.ClientAndServer` and one application-certificate lifecycle. An explicitly supplied `OpcUaClientOptions.Configuration` still wins for that client registration.
|
|
|
|
### Application identity and certificate defaults
|
|
|
|
| Setting | Behavior when omitted |
|
|
|---------|-----------------------|
|
|
| `ApplicationName` | Required after client/server feature defaults are applied. The server feature defaults to `OpcUaServer`; client-only applications should set it explicitly. |
|
|
| `ApplicationUri` | Generated from the host name and application name during validation. Set a stable URI for deployed applications. |
|
|
| `ProductUri` | Uses the contributing feature value. Set a stable product URI for deployed applications. |
|
|
| `SubjectName` | `CN={ApplicationName}, O=OPC Foundation, DC=localhost`; `DC=localhost` is replaced with the host name. |
|
|
| `PkiRoot` | A per-application `OPC Foundation/{ApplicationName}/pki` directory below the per-user local application-data directory (`Environment.SpecialFolder.LocalApplicationData`). The shared temporary directory is not used, because other local users could pre-create it. Configuration fails if no application-data directory is available. When certificate stores of an earlier version (which defaulted to the temporary directory) exist but the new default does not, a warning names both paths: configure `PkiRoot` or move the stores to keep the existing application certificate. Configure a persistent, access-controlled location in production. |
|
|
| Application certificates and stores | Directory-backed application, trusted peer/issuer, HTTPS, user, and rejected stores are created below `PkiRoot`; default RSA and supported ECC application-certificate identifiers are selected. |
|
|
|
|
The security builder uses these secure defaults:
|
|
|
|
- It does not auto-accept unknown certificates or copy the application
|
|
certificate into a shared trusted store.
|
|
- It rejects SHA-1 certificates and unknown revocation status.
|
|
- It does not suppress nonce-validation errors.
|
|
- It sends certificate chains and requires an RSA key size of at least 2048.
|
|
- It retains at most five rejected certificates.
|
|
- It disables validated-certificate caching unless explicitly enabled.
|
|
|
|
### Advanced application certificate validation
|
|
|
|
`OpcUaApplicationOptions.ConfigureSecurity` is an
|
|
`Action<IApplicationConfigurationBuilderSecurityOptions>?`. It is invoked after
|
|
the default application certificates and stores and after the first-class
|
|
`AutoAcceptUntrustedCertificates`, `RejectSHA1SignedCertificates`, and
|
|
`MinimumCertificateKeySize` values. It is the final security customization
|
|
before `CreateAsync`, so it can override those first-class values. The callback
|
|
is code-only and is not bound from `IConfiguration`; exceptions are not
|
|
swallowed and fail application-configuration provider creation.
|
|
|
|
Prefer the first-class properties for their common settings and use
|
|
`ConfigureSecurity` only for advanced certificate/store behavior:
|
|
|
|
| Method | Purpose and secure default |
|
|
|--------|----------------------------|
|
|
| `SetApplicationCertificates(...)` | Replaces the generated application-certificate identifiers. Prefer `SubjectName` and `PkiRoot` for the normal generated layout. |
|
|
| `SetMaxRejectedCertificates(...)` | Sets rejected-certificate retention; default `5`. Zero keeps unlimited history; a negative value disables new storage. Directory prunes prior history at a negative limit; the shared key/value store retains it. |
|
|
| `SetAutoAcceptUntrustedCertificates(...)` | Accepts otherwise-valid unknown peer certificates; default `false`. Prefer `AutoAcceptUntrustedCertificates`. Use `true` only in an isolated lab. |
|
|
| `SetAddAppCertToTrustedStore(...)` | Adds a newly created application certificate to a shared trusted store; default `false`. |
|
|
| `SetRejectSHA1SignedCertificates(...)` | Rejects SHA-1-signed certificates; default `true`. Prefer `RejectSHA1SignedCertificates`. |
|
|
| `SetRejectUnknownRevocationStatus(...)` | Rejects chains when CA revocation status cannot be determined; default `true`. |
|
|
| `SetUseValidatedCertificates(...)` | Reuses previously validated certificates without repeating the full validation path; default `false`. Leave disabled unless the reduced revalidation is explicitly acceptable for the deployment. |
|
|
| `SetSuppressNonceValidationErrors(...)` | Suppresses zero/weak nonce errors; default `false`. Enabling it weakens user-token protection and is only for unavoidable legacy interoperability. |
|
|
| `SetSendCertificateChain(...)` | Sends the chain with a CA-signed application certificate; default `true`. |
|
|
| `SetMinimumCertificateKeySize(...)` | Sets the minimum accepted RSA key size; default `2048`. Prefer `MinimumCertificateKeySize`. |
|
|
| `AddCertificatePasswordProvider(...)` | Supplies an `ICertificatePasswordProvider` for protected private keys; no provider is registered by default. |
|
|
|
|
See [Certificates](Certificates.md) for trust-store deployment and validation,
|
|
and [Certificate Manager](CertificateManager.md) for injectable certificate
|
|
lifecycle management.
|
|
|
|
Discovery servers expose the same transport and reverse-connect shortcuts as
|
|
the regular server builder:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddInMemoryGdsServer(o => o.EndpointUrls.Add("opc.tcp://localhost:58810/GDS"))
|
|
.AddOpcTcpTransport()
|
|
.AddHttpsTransport()
|
|
.AddReverseConnect(o => o.Clients.Add(new ServerReverseConnectClientOptions
|
|
{
|
|
EndpointUrl = "opc.tcp://client.example.com:4841"
|
|
}));
|
|
|
|
services.AddOpcUa()
|
|
.AddLdsServer(o => o.EndpointUrls.Add("opc.tcp://localhost:4840/LDS"))
|
|
.AddRegistrationStore<MyRegisteredServerStore>()
|
|
.AddMulticastDiscovery<MyLdsMeFactory>();
|
|
|
|
services.AddOpcUa()
|
|
.AddWotConnectivityServer(
|
|
server => server.EndpointUrls.Add("opc.tcp://localhost:4840/WoT"),
|
|
wot => wot.AssetNamespaceUri = WotConnectivityServerOptions.DefaultAssetNamespaceUri);
|
|
```
|
|
|
|
`AddHttpsTransport()` is supplied by
|
|
`OPCFoundation.NetStandard.Opc.Ua.Bindings.Https`. Add that package directly
|
|
when enabling HTTPS, WSS, Kestrel-hosted OPC TCP, or the REST binding; the GDS
|
|
server package does not reference it transitively.
|
|
|
|
## Options binding
|
|
|
|
Every feature `.AddXxx(...)` has three overloads:
|
|
|
|
```csharp
|
|
// 1. Action — AOT-safe, recommended for all consumers.
|
|
builder.AddServer(o => { o.ApplicationName = "MyServer"; /* … */ });
|
|
|
|
// 2. IConfiguration — bind from a configuration root's default section.
|
|
builder.AddServer(builder.Configuration);
|
|
|
|
// 3. IConfigurationSection — bind from an explicit section.
|
|
builder.AddServer(builder.Configuration.GetSection("MyApp:Server"));
|
|
```
|
|
|
|
The default section names are:
|
|
|
|
| Feature | Section |
|
|
|--------------------------------|--------------------------|
|
|
| Server | `OpcUa:Server` |
|
|
| Client | `OpcUa:Client` |
|
|
| GDS Client | `OpcUa:Gds:Client` |
|
|
| GDS Server | `OpcUa:Gds:Server` |
|
|
| LDS Server | `OpcUa:Lds` |
|
|
| WoT Connectivity Server | `OpcUa:WotCon:Server` |
|
|
| WoT Connectivity Client | `OpcUa:WotCon:Client` |
|
|
|
|
The `IConfiguration` / `IConfigurationSection` overloads are
|
|
**AOT-safe** on every library. Each dependency-injection-emitting library opts into the
|
|
.NET 8+ [Configuration Binding Source Generator](https://learn.microsoft.com/dotnet/core/extensions/configuration-generator)
|
|
(`<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>`),
|
|
which replaces the reflection-based binder with statically-generated
|
|
[C# 12 interceptors](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-12#interceptors).
|
|
The `tests/Opc.Ua.Aot.Tests` project verifies that `dotnet publish`
|
|
under `PublishAot=true` produces zero `IL2026` / `IL3050` warnings
|
|
from the dependency injection surface.
|
|
|
|
## Server feature
|
|
|
|
`builder.AddServer(o => …)` hosts a `DependencyInjectionStandardServer`
|
|
via a private `OpcUaServerHostedService`. This implementation derives
|
|
from `ReverseConnectServer`, so configured reverse connections work
|
|
alongside the dependency-injection hooks without an application server
|
|
subclass. The ordinary `StandardServer` and its direct-construction
|
|
behavior are unchanged. Endpoints, PKI root, security policies, and the
|
|
application instance certificate are all set up on host startup.
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
|
builder.Logging.AddConsole();
|
|
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer(o =>
|
|
{
|
|
o.ApplicationName = "MyServer";
|
|
o.ApplicationUri = "urn:localhost:MyOrg:MyServer";
|
|
o.ProductUri = "uri:myorg:myserver";
|
|
o.AutoAcceptUntrustedCertificates = false;
|
|
o.EndpointUrls.Add("opc.tcp://localhost:51210/MyServer");
|
|
})
|
|
.AddNodeManager<MyNodeManagerFactory>() // IAsyncNodeManagerFactory
|
|
.AddSyncNodeManager<LegacyNodeManagerFactory>(); // INodeManagerFactory
|
|
|
|
await builder.Build().RunAsync();
|
|
```
|
|
|
|
`.AddNodeManager(...)`, `.AddNodeManagers(...)`, and
|
|
`.AddSyncNodeManager<T>()` register factories under an
|
|
`OpcUaServerNodeManagerRegistration` wrapper that is
|
|
**scoped to the regular server feature**. Node managers registered this
|
|
way are **not** visible to the GDS / LDS hosted services running in the
|
|
same container. See *Combined hosts* below.
|
|
|
|
`.AddServer(...)` throws `InvalidOperationException` on a second call:
|
|
at most one regular server may be registered per service collection.
|
|
|
|
The regular hosted server also registers `INodeManagerLifecycle` as a singleton forwarding provider. After the server reaches `Running`, application services can inject it to add, reload, or remove lifecycle-owned NodeManagers. Calls made before startup or after shutdown fail explicitly.
|
|
|
|
```csharp
|
|
public sealed class RuntimeModelService(INodeManagerLifecycle lifecycle)
|
|
{
|
|
public ValueTask<NodeManagerRegistration> AddAsync(CancellationToken ct)
|
|
{
|
|
return lifecycle.AddRuntimeNodeSetAsync(
|
|
new RuntimeNodeSetOptions
|
|
{
|
|
Sources = [RuntimeNodeSetSource.FromFile("Models/Line.NodeSet2.xml")]
|
|
},
|
|
callerContext: null,
|
|
ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
Applications that construct `StandardServer` directly use `server.NodeManagerLifecycle` instead. Both paths use the same lifecycle implementation and generation-aware registration handles.
|
|
|
|
Advanced server services can be supplied through the same fluent builder:
|
|
|
|
```csharp
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer(o => o.ApplicationName = "MyServer")
|
|
.AddSessionManager<MySessionManager>()
|
|
.AddSubscriptionManager<MySubscriptionManager>()
|
|
.AddDurableSubscriptions(subscriptionStore, monitoredItemQueueFactory)
|
|
.AddHistorian(historianProvider)
|
|
.AddFileSystem(fileSystemProvider)
|
|
.AddSecretStore(secretStore)
|
|
.AddCertificateManager(certificateManager)
|
|
.AddAliasNameStore(aliasNameStore);
|
|
```
|
|
|
|
If no identity authenticator is configured, the regular hosted server adds
|
|
an anonymous authenticator matching its default anonymous user-token policy.
|
|
If a non-anonymous user-token policy is configured without a corresponding
|
|
authenticator, startup logs a warning.
|
|
|
|
The customization extensions below are methods on `IOpcUaServerBuilder`
|
|
in `Microsoft.Extensions.DependencyInjection`; they return the same builder.
|
|
They use the existing server and dependency-injection dependencies.
|
|
|
|
Adopting these hooks is optional. Existing direct server construction and
|
|
subclass-based customization remain supported; upgrading does not require
|
|
switching to dependency injection.
|
|
|
|
### Server metadata
|
|
|
|
`ConfigureServerProperties(Action<Opc.Ua.ServerProperties>)` configures
|
|
the metadata published in the server's build information:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddServer(options =>
|
|
{
|
|
options.ApplicationName = "PlantServer";
|
|
options.ProductUri = "urn:example:PlantServer";
|
|
})
|
|
.ConfigureServerProperties(properties =>
|
|
{
|
|
properties.ManufacturerName = "Example Automation";
|
|
properties.SoftwareVersion = "2.0.0";
|
|
properties.BuildNumber = "42";
|
|
properties.BuildDate = new DateTime(2026, 9, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
});
|
|
```
|
|
|
|
The callback configures `IOptions<Opc.Ua.ServerProperties>`. A directly
|
|
registered `Opc.Ua.ServerProperties` service takes precedence over those
|
|
options; the two sources are not merged.
|
|
|
|
| Property left unspecified | Default |
|
|
|---------------------------|---------|
|
|
| `ProductName` | The effective `ApplicationConfiguration.ApplicationName`. |
|
|
| `ProductUri` | The effective `ApplicationConfiguration.ProductUri`. |
|
|
| `SoftwareVersion` | The existing stack software-version helper's value. |
|
|
| `BuildNumber` | The existing stack build-number helper's value. |
|
|
| `ManufacturerName`, `BuildDate` | No publisher identity or build date is inferred; configure these explicitly. |
|
|
|
|
The version and build-number fallbacks are **stack defaults**, not the
|
|
publisher application's version. Supply the publisher's own metadata for
|
|
released products. On the XML/stream path, the effective application
|
|
identity comes from the loaded configuration, including any
|
|
`ConfigureLoadedConfiguration` changes.
|
|
|
|
### Node-manager factories
|
|
|
|
Use `AddNodeManager<TFactory>()` for an `IAsyncNodeManagerFactory` resolved
|
|
from dependency injection, or the existing `AddSyncNodeManager<TFactory>()`
|
|
for an `INodeManagerFactory`. Generic factories are resolved lazily, after
|
|
the effective application configuration has been loaded or built.
|
|
Already-constructed instances are also supported through
|
|
`AddNodeManager(IAsyncNodeManagerFactory)` and
|
|
`AddNodeManager(INodeManagerFactory)`.
|
|
|
|
Configuration-dependent constructors should use
|
|
`AddNodeManagers(Func<IServiceProvider, ApplicationConfiguration, ArrayOf<IAsyncNodeManagerFactory>>)`.
|
|
The callback receives the effective configuration, including loaded-file
|
|
overrides, and can return zero, one, or many factories:
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
services.AddOpcUa()
|
|
.AddServer("PlantServer.Config.xml")
|
|
.AddNodeManagers((sp, configuration) =>
|
|
[
|
|
ActivatorUtilities.CreateInstance<PlantNodeManagerFactory>(sp, configuration)
|
|
]);
|
|
```
|
|
|
|
Here `PlantNodeManagerFactory` is an application-defined
|
|
`IAsyncNodeManagerFactory` whose constructor accepts
|
|
`ApplicationConfiguration` and any other injected dependencies. Use `[]`
|
|
when no factories are needed, or include more factories in the returned
|
|
`ArrayOf<IAsyncNodeManagerFactory>`. The loaded configuration is supplied
|
|
explicitly; it is **not** registered as a global `ApplicationConfiguration`
|
|
service for constructor injection.
|
|
|
|
### Post-startup tasks
|
|
|
|
`AddStartupTask<TTask>()`, where `TTask : class, IServerStartupTask`,
|
|
registers an injected task implementing
|
|
`OnServerStartedAsync(IServerContext, CancellationToken)`. Alternatively,
|
|
`AddStartupTask(Func<IServiceProvider, IServerContext, CancellationToken, ValueTask>)`
|
|
registers an asynchronous delegate. `IServerStartupTask` is in
|
|
`Opc.Ua.Server.Hosting`.
|
|
|
|
For example, register an application task, or delegate to an injected
|
|
initializer whose `InitializeAsync` returns `ValueTask`:
|
|
|
|
```csharp
|
|
serverBuilder.AddStartupTask<MyStartupTask>();
|
|
serverBuilder.AddStartupTask((sp, context, cancellationToken) =>
|
|
sp.GetRequiredService<MyInitializer>().InitializeAsync(context, cancellationToken));
|
|
```
|
|
|
|
Type registration is idempotent: registering the same `TTask` again does
|
|
not run it twice. Separate delegate registrations are distinct tasks.
|
|
Tasks run sequentially in registration order, once after the server has
|
|
started. An exception or cancellation aborts hosted startup and triggers
|
|
server cleanup; subsequent tasks do not run.
|
|
|
|
`StopAsync` waits for execution cleanup even after a host shutdown timeout,
|
|
so the host does not dispose dependencies that the server is still using.
|
|
Slow cleanup can therefore outlast that timeout.
|
|
|
|
Use injected services for application dependencies and the existing typed
|
|
node-manager lookup for server subsystems. For example, inside a task:
|
|
|
|
```csharp
|
|
using System.Linq;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
|
|
IDiagnosticsNodeManager diagnostics =
|
|
context.FindNodeManagers<IDiagnosticsNodeManager>().Single();
|
|
HistoryServerCapabilitiesState capabilities =
|
|
await diagnostics.GetDefaultHistoryCapabilitiesAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
```
|
|
|
|
No extra `IServerContext` members or server subclass are needed. Prefer
|
|
the historian providers' automatic
|
|
[capabilities rollup](HistoricalAccess.md#capability-discovery) when it
|
|
already describes the server's history support, rather than manually
|
|
overriding the published capabilities in a task.
|
|
|
|
### Alias-name stores and standard browse nodes
|
|
|
|
`AddAliasNameStore(store)` and `AddAliasNameStoreRegistry(registry)`
|
|
make registered stores available before address-space startup.
|
|
Application `IServerPreStartupTask` registrations run first, in their
|
|
registration order, so they can initialize source registries before the
|
|
stores are copied into the server's registry.
|
|
`ConfigureAliasNames(Action<AliasNameServerOptions>)` controls whether
|
|
the normal `ConfigurationNodeManager` also materializes their aliases
|
|
and declared optional capabilities under the standard `TagVariables`
|
|
and `Topics` categories:
|
|
|
|
```csharp
|
|
serverBuilder
|
|
.AddAliasNameStore(aliasNameStore)
|
|
.ConfigureAliasNames(options => options.MaterializeAliasNodes = true);
|
|
```
|
|
|
|
`AliasNameServerOptions` is in `Opc.Ua.Server.AliasNames`; its
|
|
`MaterializeAliasNodes` defaults to `false`. Materialized browse nodes
|
|
are a startup snapshot by default. Also set `RefreshAliasNodesOnChange = true`
|
|
to opt a materialized host into bounded, coalesced live refresh. This second
|
|
option does not enable materialization by itself; query-only servers remain
|
|
query-only.
|
|
This setting is independent of `AliasNameNodeManagerOptions` for custom
|
|
categories, whose `MaterializeAliasNodes` default remains `true`.
|
|
See [Alias Names](AliasNames.md#browsable-alias-nodes) for capabilities,
|
|
mutation behavior, and the lower-level materialization helper.
|
|
|
|
### Localized resources
|
|
|
|
Use `ConfigureResources(Action<Opc.Ua.Server.ResourceManager>)` or
|
|
`ConfigureResources(Action<IServiceProvider, Opc.Ua.Server.ResourceManager>)`
|
|
to add translations or replace default status text. The callbacks run
|
|
after the base resource manager has loaded the stack's default status
|
|
text. In a Generic Host, the service-provider overload can also consume
|
|
application configuration:
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
serverBuilder
|
|
.ConfigureResources(resources =>
|
|
resources.Add("DeviceUnavailable", "de-DE", "Das Gerät ist nicht verfügbar."))
|
|
.ConfigureResources((sp, resources) =>
|
|
{
|
|
string text = sp.GetRequiredService<IConfiguration>()["Messages:DeviceUnavailable"]
|
|
?? "The device is unavailable.";
|
|
resources.Add("DeviceUnavailable", "en-US", text);
|
|
});
|
|
```
|
|
|
|
The server creates, retains, and disposes its own resource manager.
|
|
These callbacks do not register a live `Opc.Ua.Server.ResourceManager`
|
|
service for injection. Resolve supporting services from `sp` and use the
|
|
manager passed to the callback.
|
|
|
|
Translated text retains its original `TranslationInfo` fallback locale,
|
|
template, and arguments. A retained result can therefore be translated for
|
|
another session without losing its original fallback.
|
|
|
|
### Migrating with an existing configuration XML file
|
|
|
|
Applications that already own a classic OPC UA application configuration
|
|
file (`*.Config.xml`, previously loaded through
|
|
`ApplicationInstance.LoadApplicationConfigurationAsync`) can pass that
|
|
file to `AddServer` directly and keep every setting in it — base
|
|
addresses, security policies, certificate stores, transport quotas,
|
|
operation limits, user-token policies — while adopting the hosted
|
|
dependency-injection surface:
|
|
|
|
```csharp
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer("MyServer.Config.xml")
|
|
.AddNodeManager<MyNodeManagerFactory>();
|
|
```
|
|
|
|
The same file can be referenced from configuration instead
|
|
(`OpcUa:Server:ConfigurationFile`), and a custom `StandardServer`
|
|
subclass works too:
|
|
|
|
```json
|
|
{ "OpcUa": { "Server": { "ConfigurationFile": "MyServer.Config.xml" } } }
|
|
```
|
|
|
|
```csharp
|
|
builder.Services.AddOpcUa().AddServer<MyServer>("MyServer.Config.xml");
|
|
```
|
|
|
|
On this path, the file is authoritative and takes precedence over any
|
|
application registered with `ConfigureApplication(...)`. The file also
|
|
overrides configuration-builder settings from `OpcUaServerOptions`, including:
|
|
|
|
- `ApplicationName`, `EndpointUrls`, and `PkiRoot`
|
|
- Security-policy toggles and transport quotas
|
|
- `ReverseConnect` and `ConfigureBuilder`
|
|
|
|
Hosted-server settings such as `Identity` and `ConfigureRateLimits` still
|
|
apply, as do runtime registrations such as node managers, startup tasks,
|
|
authenticators, metadata, resources, and alias settings. Configuration
|
|
shortcuts such as `AddReverseConnect` and `ConfigureOperationLimits` do not
|
|
override file settings.
|
|
|
|
To override individual file settings in code, use
|
|
`ConfigureLoadedConfiguration`, also available through the optional
|
|
`AddServer` callback below. It runs after the file is loaded and validated,
|
|
but before the server checks certificates or starts:
|
|
|
|
```csharp
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer("MyServer.Config.xml", configuration =>
|
|
{
|
|
configuration.ServerConfiguration.MaxSessionCount = 25;
|
|
});
|
|
```
|
|
|
|
A configuration document that is not a file on disk — an embedded
|
|
resource, a document fetched from a store — can be supplied as a
|
|
`Stream` instead (`OpcUaServerOptions.ConfigurationStream`; mutually
|
|
exclusive with `ConfigurationFile`). The stream must remain open until
|
|
the host starts the server; it is read once and disposed by the hosted
|
|
service during startup, so do not wrap it in a `using` yourself:
|
|
|
|
```csharp
|
|
Stream stream = typeof(Program).Assembly
|
|
.GetManifestResourceStream("MyApp.MyServer.Config.xml")!;
|
|
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer(stream) // read + disposed when the host starts the server
|
|
.AddNodeManager<MyNodeManagerFactory>();
|
|
```
|
|
|
|
The certificate manager and certificate password provider registered in
|
|
dependency injection are honored the same way as on the options path,
|
|
and the unmatched user-token-policy startup warning is derived from the
|
|
policies advertised by the file. The client offers the same migration
|
|
path — see
|
|
[Client: using an existing configuration XML file](#client-using-an-existing-configuration-xml-file).
|
|
|
|
### Server security and resource controls
|
|
|
|
The hosted server is secure by default: sign-and-encrypt policies are included,
|
|
`SecurityPolicy#None` is excluded, SHA-1 certificates are rejected, the minimum
|
|
RSA key size is 2048, and unknown certificates are not auto-accepted. Review
|
|
these controls explicitly for every deployment:
|
|
|
|
- Keep `IncludeSignAndEncryptPolicies = true`. Set
|
|
`IncludeUnsecurePolicyNone = true` only for an isolated lab endpoint with no
|
|
sensitive data or credentials.
|
|
- Enable `IncludeEccPolicies` only when the deployment certificates and clients
|
|
support the advertised ECC policies.
|
|
- Configure `UserTokenPolicies` together with matching authenticators. An empty
|
|
list advertises `Anonymous` (or, when the identity defaults disable anonymous
|
|
access, the token types of the registered authenticators); adding a token
|
|
policy alone does not authenticate it. See [Identity Providers](IdentityProviders.md) and
|
|
[Role-Based User Management](RoleBasedUserManagement.md).
|
|
- Keep certificate auto-accept disabled and provision trust lists. For
|
|
advanced validation, use the shared
|
|
`ConfigureApplication(...).ConfigureSecurity` callback described above.
|
|
- Bound transport and service work for the deployment. `MaxByteStringLength`,
|
|
`MaxArrayLength`, `MaxMessageSize`, and `OperationTimeoutMs` control transport
|
|
quotas; `OperationLimits` bounds nodes processed by individual services.
|
|
- Set deployment-specific channel, session, and failed-authentication ceilings
|
|
through `ConfigureBuilder` to reduce resource-exhaustion and credential-guessing
|
|
exposure.
|
|
|
|
`OpcUaServerOptions.ConfigureBuilder` receives
|
|
`IApplicationConfigurationBuilderServerSelected` after the standard transport
|
|
quotas, policies, user-token policies, and server options are applied, but
|
|
**before** application certificates and security stores are added.
|
|
Use it for server policy and server-runtime options. In contrast,
|
|
`OpcUaApplicationOptions.ConfigureSecurity` runs **after** certificates, stores,
|
|
and first-class certificate-validation values are applied. Use it for
|
|
post-security certificate and validation options.
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.ConfigureApplication(application =>
|
|
{
|
|
application.ApplicationName = "MyServer";
|
|
application.AutoAcceptUntrustedCertificates = false;
|
|
application.RejectSHA1SignedCertificates = true;
|
|
application.MinimumCertificateKeySize = 2048;
|
|
application.ConfigureSecurity = security => security
|
|
.SetRejectUnknownRevocationStatus(true);
|
|
})
|
|
.AddServer(server =>
|
|
{
|
|
server.IncludeSignAndEncryptPolicies = true;
|
|
server.IncludeUnsecurePolicyNone = false;
|
|
server.IncludeEccPolicies = true;
|
|
server.ConfigureBuilder = configuration => configuration
|
|
.SetMaxFailedAuthenticationAttempts(5)
|
|
.SetMaxSessionCount(100)
|
|
.SetMaxChannelCount(200)
|
|
.SetAuditingEnabled(true)
|
|
.SetHttpsMutualTls(true);
|
|
server.ConfigureRateLimits = limits =>
|
|
{
|
|
limits.ConnectionsPerSecond = 200;
|
|
limits.ConnectionBurst = 400;
|
|
limits.MaxConcurrentSessionEstablishment = 64;
|
|
};
|
|
});
|
|
```
|
|
|
|
`SetAuditingEnabled(true)` enables OPC UA audit-event reporting.
|
|
`SetHttpsMutualTls(true)` requests and validates a client certificate when one
|
|
is supplied on HTTPS endpoints; it does not by itself require every client to
|
|
present one. Enforce certificate-only Web API access through the corresponding
|
|
authentication and authorization setup. Server admission rate limiting is
|
|
enabled by default with conservative connection and concurrent
|
|
session-establishment limits. Tune it with `ConfigureRateLimits`, or register a
|
|
custom `IServerRateLimiterProvider`; do not disable it without an equivalent
|
|
upstream control. See [Rate Limiting](RateLimiting.md) rather than duplicating
|
|
the full algorithm and deployment guidance here.
|
|
|
|
### First-class server options
|
|
|
|
In addition to the basic application-identity / endpoint / PKI knobs
|
|
covered above, `OpcUaServerOptions` exposes the following first-class
|
|
properties (bindable from `IConfiguration` or set via the
|
|
`Action<OpcUaServerOptions>` overload). Certificate options added by
|
|
`AddSecurityConfiguration` are instead reached through
|
|
`OpcUaApplicationOptions.ConfigureSecurity`.
|
|
|
|
| Property | Underlying builder call | Purpose |
|
|
|----------|-------------------------|---------|
|
|
| `IncludeSignAndEncryptPolicies` | `AddSignAndEncryptPolicies()` | Add the standard sign-and-encrypt policies. On by default. |
|
|
| `IncludeUnsecurePolicyNone` | `AddUnsecurePolicyNone()` | Advertise an unsecured endpoint. Off by default; lab use only. |
|
|
| `IncludeEccPolicies` | `AddEccSignAndEncryptPolicies()` | Add ECC sign-and-encrypt security policies. Off by default. |
|
|
| `UserTokenPolicies` | `AddUserTokenPolicy(UserTokenType)` | List of user-token policies advertised on every endpoint. Defaults to `Anonymous` when empty. |
|
|
| `MaxByteStringLength` | `SetMaxByteStringLength(int)` | Transport byte-string quota; defaults to 4 MiB. |
|
|
| `MaxArrayLength` | `SetMaxArrayLength(int)` | Transport array-element quota; defaults to 1 Mi elements. |
|
|
| `MaxMessageSize` | `SetMaxMessageSize(int)` | Transport quota in bytes; `null` keeps the stack default. |
|
|
| `OperationTimeoutMs` | `SetOperationTimeout(int)` | Transport operation timeout in ms; `null` keeps the stack default. |
|
|
| `RejectSHA1Certificates` | `SetRejectSHA1SignedCertificates(bool)` | Security hardening — defaults to `true`. |
|
|
| `MinCertificateKeySize` | `SetMinimumCertificateKeySize(ushort)` | Security hardening — defaults to `2048`. Set to `0` to keep the stack default. |
|
|
| `RegistrationEndpointUrl` | `SetRegistrationEndpoint(EndpointDescription)` | LDS/GDS endpoint URL the server registers itself with on startup. |
|
|
| `ReverseConnect` | `SetReverseConnect(ReverseConnectServerConfiguration)` | Server-side reverse-connect clients (see below). |
|
|
| `OperationLimits` | `SetOperationLimits(OperationLimits)` | Per-service node limits (max nodes per read/write/browse/...). |
|
|
| `ConfigurationFile` | `LoadApplicationConfigurationAsync(path)` | Load the configuration from an existing OPC UA XML configuration file instead of building it from the options (see [Migrating with an existing configuration XML file](#migrating-with-an-existing-configuration-xml-file)). |
|
|
| `ConfigurationStream` | `LoadApplicationConfigurationAsync(stream)` | Code-only: load the configuration from a stream (e.g. an embedded resource); read once and disposed at startup. Mutually exclusive with `ConfigurationFile`. |
|
|
| `ConfigureLoadedConfiguration` | Code-only callback | Override individual settings of the configuration loaded from `ConfigurationFile` / `ConfigurationStream`. |
|
|
| `ConfigureBuilder` | Code-only callback | Pre-security server-policy and server-option escape hatch, including max failed authentication attempts, sessions, channels, auditing, and HTTPS mutual TLS. |
|
|
| `ConfigureRateLimits` | Code-only callback | Tunes the default connection and session-establishment admission controls. |
|
|
| `ResourceIsolation` | `ConfigureResourceIsolation(...)` | Limits concurrent connections, retained data, and queued/running requests by caller; defaults to Balanced. Applied even when loading XML configuration. |
|
|
|
|
Configure incomplete-message capacity with
|
|
`builder.AddServer(...).WithChunkReassemblyBudget(maxBytes)`. This registers one
|
|
`ChunkReassemblyBudget` for all listeners of the hosted server. Without an
|
|
explicit budget, the server sizes one from `MaxMessageSize`; see
|
|
[incomplete messages](RateLimiting.md#incomplete-messages) for the defaults,
|
|
sessionless headroom, and direct-construction equivalent.
|
|
|
|
### Server resource isolation
|
|
|
|
`OpcUaServerOptions.ResourceIsolation` controls whether the running server
|
|
accepts a connection, retains message data, or queues/executes a request when
|
|
resources are limited. These capacity checks are additional to connection
|
|
rate limits and service authentication. Configure them with
|
|
`ConfigureResourceIsolation(...)` or the `OpcUa:Server:ResourceIsolation`
|
|
configuration section. See [server resource isolation](ResourceIsolation.md)
|
|
for direct, DI, and JSON examples.
|
|
|
|
**Balanced is the default.** Startup validates that finite configured totals
|
|
can hold its shared and reserved capacity. It does not remove reserves or
|
|
raise totals to make an invalid configuration fit, including when you load an
|
|
XML configuration. The [startup validation guide](ResourceIsolation.md#startup-validation-and-sizing)
|
|
explains how to correct insufficient limits. Choose SharedOnly when you want
|
|
shared rate, message, and reassembly limits without per-caller isolation.
|
|
Without an explicitly supplied provider, this mode also accepts unlimited
|
|
settings that do not satisfy the other profiles' finite-capacity requirements.
|
|
|
|
`AddResourceIsolationClassifier<T>()` registers your application's implementation
|
|
of `IResourceIsolationClassifier`. This code identifies callers that may use
|
|
protected startup capacity or maps authenticated callers to configured groups.
|
|
Reserves alone do not give unknown connections protected access, and an
|
|
observed IP address is not proof of identity. TrustedReservations requires
|
|
both a classifier and provisioned `TrustedOwners`. See the
|
|
[dedicated-ingress example](ResourceIsolation.md#example-dedicated-trusted-ingress)
|
|
and its deployment requirements.
|
|
|
|
Direct servers expose `ResourceIsolationOptions`,
|
|
`ResourceIsolationClassifier`, and `ResourceIsolationProvider`. An explicitly
|
|
registered `IServerResourceIsolationProvider` takes precedence over the options,
|
|
including SharedOnly. The server does not validate it by constructing a default
|
|
plan or take ownership of its disposal; its creator or DI container owns it.
|
|
The provider author is responsible for matching the actual listener and queue
|
|
capacities. The server owns and disposes only the default provider it creates.
|
|
|
|
### Committed session bindings
|
|
|
|
Managed servers automatically supply their session manager's committed-binding
|
|
view to transport listeners. A custom `IServerSessionBindingProvider` registered as a
|
|
singleton is applied by the hosted server; direct hosts can assign
|
|
`ServerBase.SessionBindingProvider` before startup. This optional seam preserves
|
|
existing session-manager and transport-callback interfaces. Its snapshots are
|
|
classification inputs, not authorization decisions; see
|
|
[committed session bindings](Transports.md#committed-session-bindings).
|
|
|
|
### Server-side reverse connect
|
|
|
|
A regular DI server can dial back to clients via reverse-hello using
|
|
`OpcUaServerOptions.ReverseConnect` or the fluent `AddReverseConnect(...)`
|
|
shortcut. Its `ReverseConnectServer` base runs the configured outbound
|
|
connections while retaining the other DI customization hooks.
|
|
The options projection into `ServerConfiguration.ReverseConnect` is
|
|
unchanged. The data binds directly from
|
|
`OpcUa:Server:ReverseConnect`:
|
|
|
|
```jsonc
|
|
{
|
|
"OpcUa": {
|
|
"Server": {
|
|
"ReverseConnect": {
|
|
"ConnectIntervalMs": 15000,
|
|
"ConnectTimeoutMs": 30000,
|
|
"RejectTimeoutMs": 60000,
|
|
"Clients": [
|
|
{
|
|
"EndpointUrl": "opc.tcp://client.example.com:4841",
|
|
"Timeout": 30000,
|
|
"MaxSessionCount": 1,
|
|
"Enabled": true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Equivalent code-only configuration:
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddServer(o =>
|
|
{
|
|
o.ReverseConnect = new ServerReverseConnectOptions();
|
|
o.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions
|
|
{
|
|
EndpointUrl = "opc.tcp://client.example.com:4841",
|
|
Enabled = true
|
|
});
|
|
});
|
|
```
|
|
|
|
For a loaded XML file or stream, its `ServerConfiguration.ReverseConnect`
|
|
is authoritative; use `ConfigureLoadedConfiguration` to change it rather
|
|
than the options-building shortcuts. Directly constructed `StandardServer`
|
|
instances do not gain reverse-connect behavior. See
|
|
[Reverse Connect](ReverseConnect.md#server-side-dependency-injection)
|
|
for the hosted lifecycle and a loaded-configuration example.
|
|
|
|
### Operation limits
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddServer(o =>
|
|
{
|
|
o.OperationLimits = new OperationLimitsOptions
|
|
{
|
|
MaxNodesPerRead = 1000,
|
|
MaxNodesPerWrite = 1000,
|
|
MaxNodesPerBrowse = 1000,
|
|
MaxMonitoredItemsPerCall = 5000
|
|
};
|
|
});
|
|
```
|
|
|
|
Bindable from `OpcUa:Server:OperationLimits`. Any value left at zero is
|
|
treated as "unlimited" by the OPC UA server stack.
|
|
|
|
### User token policies
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddServer(o =>
|
|
{
|
|
o.UserTokenPolicies.Add(new OpcUaUserTokenPolicy
|
|
{
|
|
TokenType = UserTokenType.UserName
|
|
});
|
|
o.UserTokenPolicies.Add(new OpcUaUserTokenPolicy
|
|
{
|
|
TokenType = UserTokenType.Certificate
|
|
});
|
|
});
|
|
```
|
|
|
|
Bindable from `OpcUa:Server:UserTokenPolicies`. When the list is empty
|
|
the hosted service falls back to a single `Anonymous` policy. If the
|
|
identity defaults disable anonymous access (`EnableAnonymous = false`),
|
|
that implicit `Anonymous` policy is not advertised; the endpoints list
|
|
one policy per token type of the registered authenticators instead
|
|
(OPC 10000-4 §7.14: the user identity tokens the server accepts).
|
|
|
|
### Identity (server)
|
|
|
|
> Full identity surface is documented in
|
|
> [Identity Providers](IdentityProviders.md). This section covers only
|
|
> the dependency injection bindings.
|
|
|
|
`builder.AddServer(...)` exposes identity-related extension methods on
|
|
`IOpcUaServerBuilder`:
|
|
|
|
| Extension | Purpose |
|
|
|---|---|
|
|
| `ConfigureRoles(Action<RoleConfigurationOptions>)` / `(IConfiguration)` | Configures roles and identity-mapping rules on the default role manager. Well-known roles are configured when the manager is resolved; custom role nodes are materialized when the server address space starts. |
|
|
| `AddIdentityAuthenticator<TAuth>()` | Registers a single custom `IUserTokenAuthenticator` implementation. The hosted service adds it to `IServerInternal.IdentityRegistry` on startup. |
|
|
| `AddIdentityAuthenticator(IUserTokenAuthenticator)` | Registers an already-constructed authenticator instance. |
|
|
| `AddIdentityAuthenticator(Func<IServiceProvider, ICertificateValidatorEx?, IUserTokenAuthenticator>)` | Creates an authenticator during startup with injected services and the effective certificate validator, which may be `null`. |
|
|
| `AddIdentityAugmenter<TAugmenter>()` | Registers a single custom `IIdentityAugmenter` implementation. The hosted service runs it after accepted authentications. |
|
|
| `AddDefaultIdentityAuthenticators(Action<DefaultAuthenticatorOptions>)` / `(IConfiguration)` | Registers the four in-box authenticators (Anonymous, UserNamePassword, X509, Jwt) with toggles per type plus the JWT audience / clock-skew settings. |
|
|
| `AddJwtIssuer(Action<JwtIssuerOptions>)` / `(IConfiguration)` | Registers a trusted JWT issuer. Multiple calls coexist; each contributes a `StaticIssuerKeyResolver` and / or `JwksIssuerKeyResolver` keyed by `IssuerUri`. |
|
|
| `WithKeyCredentialPush(Action<KeyCredentialPushOptions>?)` | Enables the Part 12 §8 resource-server Push binding and registers an `IKeyCredentialStore` if none is supplied. |
|
|
| `AddSessionlessInvocation(Action<SessionlessInvocationOptions>?)` | Enables Session-less Service invocation (OPC 10000-4 §6.3) with an Access Token in the RequestHeader; anonymous callers only with `AllowAnonymous`. See [Session-less invocation](SessionlessInvocation.md). |
|
|
|
|
Configuration binding under `OpcUa:Server:Identity`:
|
|
|
|
```json
|
|
{
|
|
"OpcUa": {
|
|
"Server": {
|
|
"Identity": {
|
|
"Defaults": {
|
|
"EnableAnonymous": true,
|
|
"EnableUserNamePassword": true,
|
|
"EnableX509": true,
|
|
"EnableJwt": true,
|
|
"ExpectedAudience": "urn:my-server",
|
|
"ClockSkewTolerance": "00:01:00",
|
|
"UserCertificateTrustList": "Users"
|
|
},
|
|
"Issuers": [
|
|
{
|
|
"IssuerUri": "https://login.microsoftonline.com/{tenant}/v2.0",
|
|
"JwksUri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys",
|
|
"Algorithms": [ "RS256" ]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The `AddServer(IConfiguration)` overload walks this layout and (in
|
|
addition to binding `OpcUaServerOptions.Identity`) registers each
|
|
`Issuers[]` entry through `AddJwtIssuer(...)`, binds the `Roles`
|
|
sub-section into `RoleConfigurationOptions`, and enables the bridge
|
|
that creates the four default authenticators from the bound
|
|
`Identity:Defaults` flags. The `AddServer(Action<OpcUaServerOptions>)`
|
|
code-only overload does NOT auto-wire any of these — call the fluent
|
|
extensions explicitly when configuring from code.
|
|
|
|
The `IUserDatabase` / `IUserManagement` services needed by the
|
|
UserNamePassword authenticator must be registered separately — the
|
|
hosted service skips that authenticator if neither is in dependency injection. See
|
|
[Role-Based Security](RoleBasedUserManagement.md) for the role-mapping
|
|
layer.
|
|
|
|
The instance and factory overloads complement the existing generic
|
|
registration. Factories receive the validator after configuration and
|
|
certificate setup; a validator-dependent authenticator should fail
|
|
clearly if the nullable validator is absent. Register matching
|
|
`UserTokenPolicies` as well — an authenticator does not advertise its
|
|
token policy automatically. See
|
|
[custom authenticator registrations](IdentityProviders.md#custom-authenticator-registrations)
|
|
for an example using the configured certificate stores.
|
|
|
|
### Fluent shortcuts and one-shot presets
|
|
|
|
The server hosting surface exposes granular extension points and one-shot presets:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddSecureServer(options =>
|
|
{
|
|
options.ApplicationName = "PlantServer";
|
|
options.ApplicationUri = "urn:localhost:PlantServer";
|
|
options.ProductUri = "urn:example:PlantServer";
|
|
options.EndpointUrls.Add("opc.tcp://localhost:4840/PlantServer");
|
|
})
|
|
.AddOpcTcpTransport()
|
|
.AddReverseConnect(options =>
|
|
{
|
|
options.Clients.Add(new ServerReverseConnectClientOptions
|
|
{
|
|
EndpointUrl = "opc.tcp://client.example.com:4841"
|
|
});
|
|
})
|
|
.ConfigureOperationLimits(options => options.MaxNodesPerRead = 1000)
|
|
.ConfigureRoles(options => options.Roles.Add(new RoleDefinitionOptions
|
|
{
|
|
Name = BrowseNames.WellKnownRole_Observer,
|
|
Identities =
|
|
{
|
|
new RoleIdentityMappingOptions
|
|
{
|
|
CriteriaType = IdentityCriteriaType.UserName,
|
|
Criteria = "operator"
|
|
}
|
|
}
|
|
}));
|
|
```
|
|
|
|
Both `AddSecureServer` and `AddReferenceServer` honor `ConfigureRoles` and
|
|
previously registered `RoleConfigurationOptions`. They preserve an explicitly
|
|
registered `IRoleManager`; options do not silently replace or mutate that custom
|
|
manager. Configuration remains deferred until service resolution and startup.
|
|
|
|
Use `AddRoleManager(IRoleManager)` or `AddRoleManager<T>()` to explicitly replace the default role manager.
|
|
`AddServer<TServer>()` remains available for custom server implementations.
|
|
Custom types that need session, subscription, or durable-subscription DI
|
|
hooks must derive from `DependencyInjectionStandardServer`. The same
|
|
requirement applies to DI-only metadata, resource, and alias settings
|
|
configured through `ConfigureServerProperties`, `ConfigureResources`,
|
|
and `ConfigureAliasNames`. A non-DI custom server rejects these settings
|
|
clearly during startup instead of silently ignoring them.
|
|
|
|
Fluent node managers can be registered without a factory class:
|
|
|
|
```csharp
|
|
const string namespaceUri = "urn:example:line";
|
|
|
|
services.AddOpcUa()
|
|
.AddServer(options => { /* endpoint and application options */ })
|
|
.AddNodeManager(namespaceUri, nodes =>
|
|
{
|
|
ushort namespaceIndex =
|
|
(ushort)nodes.Context.NamespaceUris.GetIndex(namespaceUri);
|
|
nodes.CreateInstance(
|
|
new QualifiedName("Line", namespaceIndex),
|
|
parent => new FolderState(parent))
|
|
.Configure(node => node.UnderObjectsFolder());
|
|
});
|
|
```
|
|
|
|
The callback owns the complete address space contributed by this node manager.
|
|
`AddNodeManager(namespaceUri, build)` does not create an implicit root folder.
|
|
|
|
`AddHistorianFileStore(provider, path)` combines the historian provider registration with a Part 20
|
|
file-system mount for demo and lab servers.
|
|
|
|
### Positioning
|
|
|
|
Use `AddPositioningServer()` when Positioning owns its node manager. Use
|
|
`AddPositioningFor<TNodeManager>()` when an existing companion node manager
|
|
loads and owns the RSL/GPOS models:
|
|
|
|
```csharp
|
|
IPositioningServerBuilder positioning = services
|
|
.AddOpcUa()
|
|
.AddServer(options => { /* endpoint and application options */ })
|
|
.AddPositioningServer();
|
|
|
|
positioning
|
|
.AddGeoLocationProvider<MyGpsProvider>()
|
|
.AddRelativeSpatialLocationProvider<MyRelativeLocationProvider>()
|
|
.ConfigurePositioningFor<PositioningNodeManager>(context =>
|
|
{
|
|
// Build and register RSL/GPOS instances through context.AddressSpace.
|
|
return default;
|
|
});
|
|
```
|
|
|
|
Client factories compose the generated proxies over the managed session:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddClient(options => { /* endpoint and application options */ })
|
|
.AddPositioningClient();
|
|
```
|
|
|
|
See [Relative Spatial Location and Global Positioning](Positioning.md).
|
|
|
|
### Robotics
|
|
|
|
`AddRobotics()` registers the stock `RoboticsNodeManager`, the built-in DI/IA/
|
|
Robotics model provider, and the ordered Robotics configuration pipeline. It
|
|
owns the DI namespace, so it cannot be combined with `AddOpcUaDi()`.
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddServer(options => { /* endpoint and application options */ })
|
|
.AddRobotics(options =>
|
|
options.InstanceNamespaceUri = "urn:example:robot-cell")
|
|
.AddRoboticsModel<MyExtraModelProvider>()
|
|
.ConfigureRobotics(async context =>
|
|
{
|
|
await context.AddMotionDeviceSystemAsync("RobotCell", system =>
|
|
{
|
|
// Add controllers, motion devices, axes, power trains, and
|
|
// safety states through the fluent Robotics builders.
|
|
}, context.CancellationToken);
|
|
});
|
|
```
|
|
|
|
Use `ConfigureRobotics<TConfigurator>()` for a dependency-injected class-based
|
|
code-behind implementing `IRoboticsConfigurator`, and
|
|
`ConfigureRoboticsFor<TNodeManager>(…)` when the application already owns a
|
|
compatible `DiNodeManager`. Configurators run in registration order and share
|
|
one `IRoboticsBuildContext` per manager startup.
|
|
|
|
See the [Robotics developer guide](Robotics.md).
|
|
|
|
Client factories compose the Robotics client over the managed session.
|
|
`AddRoboticsClient()` also registers the DI client services because the Robotics
|
|
client extends `DiTopologyClient`:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddClient(options => { /* endpoint and application options */ })
|
|
.AddRoboticsClient();
|
|
```
|
|
|
|
## Client feature
|
|
|
|
`builder.AddClient(opt => …)` registers a lazy `ManagedSession` factory
|
|
delegate. The factory caches the connected session — subsequent awaits
|
|
return the same instance.
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Opc.Ua.Client;
|
|
|
|
services
|
|
.AddOpcUa()
|
|
.AddClient(opt =>
|
|
{
|
|
opt.ApplicationName = "MyClient";
|
|
opt.ApplicationUri = "urn:localhost:MyClient";
|
|
opt.ProductUri = "uri:example.com:MyClient";
|
|
opt.Session = new ManagedSessionOptions
|
|
{
|
|
Endpoint = endpoint,
|
|
ReconnectPolicy = new ReconnectPolicyOptions
|
|
{
|
|
Strategy = BackoffStrategy.Exponential
|
|
}
|
|
};
|
|
});
|
|
|
|
// Resolve and connect on first use:
|
|
var sessionFactory = sp.GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
|
|
ManagedSession session = await sessionFactory(ct);
|
|
```
|
|
|
|
`AddClient` also registers `ITelemetryContext`, `ISessionFactory` (a
|
|
`DefaultSessionFactory` configured with the V2 subscription engine),
|
|
`ManagedSessionFactory`, `IManagedSessionFactory`, and the top-level
|
|
`OpcUaClientOptions`. The cached delegate is convenient for a single fixed
|
|
endpoint; resolve `IManagedSessionFactory` when endpoints are selected at
|
|
runtime:
|
|
|
|
```csharp
|
|
var managedSessions = sp.GetRequiredService<IManagedSessionFactory>();
|
|
ManagedSession dynamicSession = await managedSessions.ConnectAsync(endpoint, ct);
|
|
```
|
|
|
|
Misconfiguration is validated through `IValidateOptions<OpcUaClientOptions>` when the host starts and again before a DI-created session connects. Supply either an explicit `Configuration`, the application identity fields directly on `OpcUaClientOptions` (as shown above), an existing configuration XML document (`ConfigurationFile` / `ConfigurationStream`, see below), or the shared `ConfigureApplication(...)` options for a combined client/server host (see [Shared application configuration](#shared-application-configuration)). `Session.Endpoint` is required by the cached fixed-endpoint delegate; `IManagedSessionFactory.ConnectAsync(endpoint, ...)` supplies it at runtime.
|
|
|
|
### Client: using an existing configuration XML file
|
|
|
|
The client offers the same migration path as
|
|
[the server](#migrating-with-an-existing-configuration-xml-file):
|
|
applications that already own a classic client configuration file
|
|
(`*.Config.xml`) can pass it to `AddClient` and keep every setting in it
|
|
— security configuration, certificate stores, transport quotas, client
|
|
configuration:
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddClient("MyClient.Config.xml", opt =>
|
|
{
|
|
opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
|
|
});
|
|
```
|
|
|
|
The same file can be referenced from configuration
|
|
(`OpcUa:Client:ConfigurationFile`), and a stream works for documents
|
|
that are not files on disk, e.g. embedded resources
|
|
(`OpcUaClientOptions.ConfigurationStream`; read once and disposed after
|
|
loading):
|
|
|
|
```csharp
|
|
Stream stream = typeof(Program).Assembly
|
|
.GetManifestResourceStream("MyApp.MyClient.Config.xml")!;
|
|
services.AddOpcUa().AddClient(stream);
|
|
```
|
|
|
|
The document loads lazily through
|
|
`ApplicationInstance.LoadApplicationConfigurationAsync` on first use —
|
|
the first session connect, reverse-connect startup, or an explicit
|
|
`GetAsync` on `OpcUaClientOptions.ConfigurationProvider` — which also
|
|
runs the optional `ConfigureLoadedConfiguration` override callback and
|
|
ensures the application-instance certificate, exactly like the shared
|
|
`ConfigureApplication(...)` path. The supplied document is
|
|
authoritative: it takes precedence over a shared application registered
|
|
with `ConfigureApplication(...)`, and combining it with an explicit
|
|
`Configuration`, with the application identity properties, or setting
|
|
both file and stream is rejected with a clear
|
|
`InvalidOperationException` at registration. DI-registered
|
|
`ICertificateManager` / `ICertificatePasswordProvider` are honored.
|
|
|
|
### Client: loading the configuration eagerly
|
|
|
|
Hosted applications whose user interface needs the
|
|
`ApplicationConfiguration` before any session exists — a WinForms or WPF
|
|
client whose main form takes the configuration in its constructor, for
|
|
instance — can opt into an eager load with
|
|
`OpcUaClientOptions.LoadConfigurationOnStart` (bindable from
|
|
`OpcUa:Client:LoadConfigurationOnStart`). It registers an
|
|
`IHostedService` that awaits the load while the host starts, the
|
|
client-side twin of the reverse-connect hosted service:
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddClient("MyClient.Config.xml", opt => opt.LoadConfigurationOnStart = true);
|
|
|
|
using IHost host = builder.Build();
|
|
await host.StartAsync();
|
|
|
|
// The document is loaded and validated and the application instance
|
|
// certificate is ensured; a failure has already failed host.StartAsync().
|
|
ApplicationConfiguration configuration = host.Services
|
|
.GetRequiredService<OpcUaClientOptions>()
|
|
.ConfigurationProvider!
|
|
.Configuration;
|
|
```
|
|
|
|
`OpcUaClientOptions.ConfigurationProvider` is the public
|
|
`IOpcUaApplicationConfigurationProvider` resolved for the client — the
|
|
supplied-document provider when `ConfigurationFile` / `ConfigurationStream`
|
|
was set, and otherwise the shared `ConfigureApplication(...)` provider.
|
|
It is available on the options instance resolved from the service
|
|
provider, not on the instance passed to the `AddClient(...)` callback,
|
|
and it exposes everything a hosted client needs: `GetAsync(ct)` as an
|
|
explicit load trigger without connecting a session, `Configuration` to
|
|
read the loaded document back, and `Application`
|
|
(`IApplicationInstance`) for applications that manage their own instance
|
|
certificate. Applications that do not use the Generic Host can call
|
|
`GetAsync` directly instead of setting `LoadConfigurationOnStart`; both
|
|
routes share the provider's single-flight load, so the configuration is
|
|
never loaded twice.
|
|
|
|
`LoadConfigurationOnStart` applies to every provider source and is a
|
|
no-op when an explicit `Configuration` was supplied, which is already
|
|
complete. Because it decides a service registration, set it within the
|
|
`AddClient(...)` callback or bind it from the configuration section;
|
|
setting it on the resolved options afterwards has no effect.
|
|
|
|
### Fluent shortcuts
|
|
|
|
The returned `IOpcUaClientBuilder` can compose client features without
|
|
returning to the root builder:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddClient(options =>
|
|
{
|
|
options.Configuration = configuration;
|
|
options.Session = options.Session with { Endpoint = endpoint };
|
|
})
|
|
.AddSubscriptions()
|
|
.AddManagedClientPool()
|
|
.AddWotConClient()
|
|
.AddGdsClient()
|
|
.AddCertificateManagement();
|
|
```
|
|
|
|
Reverse-connect one-shot sessions can be built through DI:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddReverseConnectClient(options =>
|
|
{
|
|
options.Configuration = configuration;
|
|
options.Session = options.Session with { Endpoint = endpoint };
|
|
}, new Uri("urn:server"));
|
|
```
|
|
|
|
Discovery-and-connect resolves `IOpcUaDiscoveryService`, selects an endpoint by security policy and mode,
|
|
then connects through `IManagedSessionFactory`:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddClient(options => options.Configuration = configuration)
|
|
.AddDiscoveryAndConnect(options =>
|
|
{
|
|
options.DiscoveryUrl = "opc.tcp://localhost:4840";
|
|
options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
|
|
options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
|
|
});
|
|
```
|
|
|
|
Client security checklist:
|
|
|
|
- Select `MessageSecurityMode.SignAndEncrypt` and an approved policy such as
|
|
`Basic256Sha256` (or a supported stronger policy) during discovery. Do not
|
|
fall back to `SecurityPolicy#None` outside isolated lab environments.
|
|
- Keep `AutoAcceptUntrustedCertificates = false`; provision the server or issuer
|
|
certificate in the client trust store and retain URI/hostname, chain,
|
|
revocation, key-size, and nonce validation. See
|
|
[Certificates](Certificates.md).
|
|
- Register an `IClientIdentityProvider` appropriate for the selected endpoint's
|
|
user-token policy. Keep passwords and tokens in the secret/provider
|
|
infrastructure rather than embedding them in configuration. See
|
|
[Identity Providers](IdentityProviders.md).
|
|
|
|
### Identity (client)
|
|
|
|
> Full identity surface is documented in
|
|
> [Identity Providers](IdentityProviders.md). This section covers only
|
|
> the dependency injection bindings.
|
|
|
|
`builder.AddClient(...)` exposes:
|
|
|
|
| Extension | Purpose |
|
|
|---|---|
|
|
| `AddIdentityProvider<TProvider>()` | Registers a custom `IClientIdentityProvider` type. All registered providers are composed into a single resolver by the session builder. |
|
|
| `AddIdentityProvider(Action<CompositeClientIdentityProviderBuilder>)` | Fluent composite-builder with shortcuts: `.AddAnonymous()`, `.AddUserName(...)`, `.AddX509(...)`, `.AddIssuedToken(...)`. |
|
|
| `AddIdentityProvider(IConfiguration section)` | Binds `OpcUaClientIdentityOptions` and rolls a `CompositeClientIdentityProvider` from the bound entries. |
|
|
| `AddAccessTokenProvider<T>()` / `(instance)` / `(factory)` | Registers an `IAccessTokenProvider` keyed by `AuthorityUri`. Issued-token identity providers select the matching provider at runtime via the URI. |
|
|
|
|
Configuration binding under `OpcUa:Client:Identity`:
|
|
|
|
```json
|
|
{
|
|
"OpcUa": {
|
|
"Client": {
|
|
"Identity": {
|
|
"EnableAnonymous": true,
|
|
"UserName": {
|
|
"UserName": "alice",
|
|
"SecretName": "alice-password",
|
|
"SecretStoreType": "InMemory"
|
|
},
|
|
"X509": {
|
|
"StoreType": "X509Store",
|
|
"StorePath": "CurrentUser/My",
|
|
"SubjectName": "CN=Alice"
|
|
},
|
|
"IssuedToken": {
|
|
"ProfileUri": "http://opcfoundation.org/UA/UserToken#JWT",
|
|
"AuthorityUri": "https://issuer.example"
|
|
},
|
|
"Order": [ "IssuedToken", "UserName", "X509", "Anonymous" ]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
When `Order` is non-empty the composite presents providers in that
|
|
preference; otherwise registration order is preserved. The `IssuedToken`
|
|
entry resolves the registered `IAccessTokenProvider` whose
|
|
`AuthorityUri` matches.
|
|
|
|
> **How the section is consumed.** `AddClient(IConfiguration)` binds
|
|
> `OpcUa:Client:Identity` into `OpcUaClientOptions.Identity`. At
|
|
> session-factory resolution time, if no `IClientIdentityProvider`
|
|
> service is explicitly registered, the factory builds a composite
|
|
> from the bound options provided at least one non-default field is
|
|
> set (any of `UserName`, `X509`, `IssuedToken`, `Order`, or
|
|
> `EnableAnonymous = false`). Setting only `EnableAnonymous = true`
|
|
> (the default) does NOT register any provider — call
|
|
> `.AddIdentityProvider(configuration.GetSection("OpcUa:Client:Identity"))`
|
|
> if you want an explicit eager registration regardless of the option
|
|
> values, or use the
|
|
> `AddIdentityProvider(Action<CompositeClientIdentityProviderBuilder>)`
|
|
> overload for code-only configuration. The composite-builder
|
|
> shortcuts (`.AddUserName(configure, registry)`,
|
|
> `.AddX509(configure, provider, passwords)`, and
|
|
> `.AddIssuedToken(configure, provider)`) each take the supporting
|
|
> service (an `ISecretRegistry`, `ICertificateProvider` +
|
|
> `ICertificatePasswordProvider`, or `IAccessTokenProvider`) as a
|
|
> second positional argument.
|
|
|
|
`ManagedSessionOptions.Identity` (eager) is `[Obsolete]` — set
|
|
`ManagedSessionOptions.IdentityProvider` instead for lazy / refreshable
|
|
identities. The `ManagedSession` proactive-refresh scheduler keys off
|
|
`IClientIdentityProvider.ExpiresAt`.
|
|
|
|
## Complex types
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddClient(/* … */).AddComplexTypes();
|
|
```
|
|
|
|
`AddComplexTypes()` can be chained directly after `AddClient(...)`. It
|
|
registers a `ComplexTypeSystemFactory` (singleton) that can be resolved
|
|
and used to build a `ComplexTypeSystem` for a connected session:
|
|
|
|
```csharp
|
|
var factory = sp.GetRequiredService<ComplexTypeSystemFactory>();
|
|
ComplexTypeSystem cts = factory.Create(session);
|
|
await cts.LoadAsync(...);
|
|
```
|
|
|
|
For a managed client that should load complex types automatically after
|
|
connect, use either `ManagedSessionBuilder.WithLoadComplexTypes()` or set
|
|
`ManagedSessionOptions.LoadComplexTypes = true`. The one-shot
|
|
`AddManagedClient(...)` helper registers client services, reconnect defaults,
|
|
and complex-type loading together:
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddManagedClient(opt =>
|
|
{
|
|
opt.Configuration = applicationConfiguration;
|
|
opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
|
|
});
|
|
```
|
|
|
|
The direct constructor fallback remains available:
|
|
|
|
```csharp
|
|
var cts = new ComplexTypeSystem(session, telemetry);
|
|
await cts.LoadAsync(ct: ct);
|
|
```
|
|
|
|
## Programs
|
|
|
|
Register the managed Part 10 server host with
|
|
`AddServer(...).AddPrograms(definitions)` or the overload that resolves
|
|
definitions/providers from `IServiceProvider` per server start. Registration
|
|
does not create or start workloads. Client registration is available through
|
|
`AddClient(...).AddProgramClient()`.
|
|
|
|
The direct alternatives are `ProgramNodeManagerFactory`, `ProgramRuntime.AttachAsync`
|
|
and `ProgramClient`. They use the same execution/lifecycle and generated proxy
|
|
paths, not independent implementations. See [Programs](Programs.md) for provider
|
|
contracts, explicit lifetime limits, typed client examples and compatibility rules.
|
|
|
|
## Alarms and conditions
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddClient(/* … */).AddAlarms();
|
|
```
|
|
|
|
Registers a singleton `AlarmClientFactory` so dependency-injection-hosted client
|
|
applications can obtain a Part 9 `AlarmClient` per connected session
|
|
without `new`-ing one manually. Source-generated event records
|
|
(`ConditionTypeRecord`, `AlarmConditionTypeRecord`,
|
|
`DialogConditionTypeRecord`, all subtypes including vendor extensions)
|
|
and the streaming extensions (`SubscribeAlarmsAsync`,
|
|
`SubscribeConditionsAsync`, `SubscribeDialogsAsync`) require no
|
|
registration — they compose naturally with the
|
|
`ManagedSession.DefaultStreaming` surface that `AddClient(...)`
|
|
already wires up.
|
|
|
|
```csharp
|
|
ManagedSession session = await sessionFactory(ct);
|
|
var factory = sp.GetRequiredService<AlarmClientFactory>();
|
|
AlarmClient alarms = factory.Create(session);
|
|
|
|
// Acknowledge an alarm:
|
|
await alarms.AcknowledgeAsync(conditionId, eventId,
|
|
new LocalizedText("en", "Acknowledged"), ct);
|
|
|
|
// Stream typed records via the session's default streaming subscription:
|
|
await foreach (ConditionTypeRecord record in session.DefaultStreaming
|
|
.SubscribeAlarmsAsync(ObjectIds.Server, ct: ct))
|
|
{
|
|
/* … */
|
|
}
|
|
```
|
|
|
|
The non-dependency-injection path (`session.GetAlarmClient()` extension and the public
|
|
`AlarmClient` constructor) remains available for callers that do not
|
|
use the dependency injection infrastructure. See
|
|
[Alarms and Conditions](AlarmsAndConditions.md) for the full
|
|
developer guide.
|
|
|
|
### Client-side reverse connect
|
|
|
|
When `OpcUaClientOptions.ReverseConnect` is set, the dependency injection container
|
|
registers a singleton `ReverseConnectManager` that opens the configured
|
|
listener endpoints on first resolution. Inbound reverse-hello messages
|
|
are surfaced via
|
|
`ReverseConnectManager.WaitForConnectionAsync(endpointUrl, serverUri, ct)`,
|
|
and the values are also mirrored into
|
|
`ApplicationConfiguration.ClientConfiguration.ReverseConnect`.
|
|
|
|
```jsonc
|
|
{
|
|
"OpcUa": {
|
|
"Client": {
|
|
"ReverseConnect": {
|
|
"HoldTimeMs": 15000,
|
|
"WaitTimeoutMs": 20000,
|
|
"ClientEndpointUrls": [
|
|
"opc.tcp://0.0.0.0:4841"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Equivalent code-only:
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddClient(opt =>
|
|
{
|
|
opt.Configuration = applicationConfiguration;
|
|
opt.ReverseConnect = new ClientReverseConnectOptions();
|
|
opt.ReverseConnect.ClientEndpointUrls.Add("opc.tcp://0.0.0.0:4841");
|
|
});
|
|
|
|
// Resolve the manager and await an inbound reverse-hello connection:
|
|
var reverseConnect = sp.GetRequiredService<ReverseConnectManager>();
|
|
ITransportWaitingConnection connection =
|
|
await reverseConnect.WaitForConnectionAsync(endpointUrl, serverUri: null, ct);
|
|
// pass `connection` to Session.Create / DefaultSessionFactory.RecreateAsync
|
|
```
|
|
|
|
Note: the `Func<CancellationToken, Task<ManagedSession>>` delegate
|
|
registered by `AddClient` does **not** automatically consume the
|
|
reverse-connect manager for initial connection — it still dials the
|
|
configured endpoint outbound. Use the `ReverseConnectManager` directly
|
|
when the server initiates the session. The
|
|
`ReverseConnect` configuration is also consumed by `Session` /
|
|
`SessionReconnectHandler` for reconnect-via-reverse-hello scenarios.
|
|
|
|
## Channel manager
|
|
|
|
`AddClient(...)` automatically registers an `IClientChannelManager`
|
|
singleton (`ClientChannelManager` implementation) built from
|
|
`OpcUaClientOptions.Configuration`. The
|
|
`Func<CancellationToken, Task<ManagedSession>>` delegate wires this
|
|
manager into every new `ManagedSession` it creates, so multiple
|
|
`ManagedSession` instances resolved from the same DI container will
|
|
share underlying transport channels per
|
|
`ConfiguredEndpoint`. Reconnect is coalesced and notified to attached
|
|
sessions transparently. On .NET 8 and later, HTTPS transport channels
|
|
also get the named `IHttpClientFactory` client (`Opc.Ua.Client`) and its
|
|
standard resilience handler through `AddClient(...)`. See
|
|
[Sessions and reconnect § 4](Sessions.md#4-iclientchannelmanager--centralised-channel-sharing-and-reconnect)
|
|
for details on identity, retry policy, HTTPS request resilience, and the
|
|
shared retry budget between channel-manager reconnect and
|
|
`ManagedSession`'s outer `IReconnectPolicy`.
|
|
|
|
> **Security trade-off — OPC UA TLS validation vs the DI HttpClient pipeline.**
|
|
> When an OPC UA `CertificateValidator` is configured for a channel
|
|
> (the normal case for any non-`SecurityPolicies.None` HTTPS profile),
|
|
> `HttpsTransportChannel` always takes the direct construction path so
|
|
> the OPC UA trust list, OPC UA mutual TLS, the redirect lock, and the
|
|
> message-size quotas are guaranteed. The named `Opc.Ua.Client`
|
|
> HttpClient pipeline (Polly resilience handler, etc.) is **not**
|
|
> applied to those channels and the channel emits a one-time
|
|
> `LogWarning` to make the bypass visible. To use both the Polly
|
|
> resilience handler AND OPC UA cert validation on the same channel,
|
|
> register the named client with a `ConfigurePrimaryHttpMessageHandler`
|
|
> that wires the OPC UA `ServerCertificateCustomValidationCallback` and
|
|
> the OPC UA application instance certificate yourself. See
|
|
> [Sessions and reconnect § HTTPS factory + OPC UA cert validation: secure-by-default fallback](Sessions.md#https-factory--opc-ua-cert-validation-secure-by-default-fallback).
|
|
|
|
To override the default channel manager (e.g. to inject a custom
|
|
`IChannelReconnectPolicy`):
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddClient(opt => { ... });
|
|
services.Replace(ServiceDescriptor.Singleton<IClientChannelManager>(sp =>
|
|
new ClientChannelManager(
|
|
sp.GetRequiredService<OpcUaClientOptions>().Configuration!,
|
|
sp.GetRequiredService<ITelemetryContext>(),
|
|
reconnectPolicy: new ExponentialBackoffChannelReconnectPolicy
|
|
{
|
|
MaxAttempts = 5
|
|
})));
|
|
```
|
|
|
|
## Application instance (advanced)
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddApplicationInstance();
|
|
```
|
|
|
|
Registers an `IApplicationInstanceFactory` singleton. Hosted server
|
|
services (regular, GDS, LDS, WotCon) call the factory to create their
|
|
own per-host `IApplicationInstance` instead of sharing a process-wide
|
|
singleton — this avoids configuration overwrite and certificate-lifecycle
|
|
conflicts when multiple servers coexist in one process. `AddServer` /
|
|
`AddGdsServer` / `AddLdsServer` register the factory automatically, so
|
|
calling `AddApplicationInstance()` explicitly is only needed when you
|
|
want the factory available outside the hosted-server context.
|
|
|
|
## GDS Client
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddGdsClient(opt =>
|
|
{
|
|
opt.SessionTimeout = TimeSpan.FromMinutes(2);
|
|
opt.MaxConnectAttempts = 10;
|
|
});
|
|
```
|
|
|
|
Requires an `ApplicationConfiguration` registered in the container.
|
|
Resolves `GlobalDiscoveryServerClient` and `ServerPushConfigurationClient`
|
|
singletons.
|
|
|
|
## GDS Server
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddGdsServer(o =>
|
|
{
|
|
o.ApplicationName = "MyGds";
|
|
o.ApplicationUri = "urn:localhost:MyOrg:MyGds";
|
|
o.ProductUri = "uri:myorg:mygds";
|
|
o.AutoAcceptUntrustedCertificates = false;
|
|
o.EndpointUrls.Add("opc.tcp://localhost:58810/GlobalDiscoveryServer");
|
|
o.AuthoritiesStorePath = "%LocalApplicationData%/OPC Foundation/pki/CA";
|
|
})
|
|
.AddApplicationsDatabase<MyApplicationsDatabase>()
|
|
.AddCertificateGroup<MyCertificateGroup>()
|
|
.AddCertificateRequest<MyCertificateRequest>()
|
|
.AddUserDatabase<MyUserDatabase>();
|
|
.AddInMemoryStores()
|
|
// Optionally:
|
|
// .AddAuthorizationService<MyTokenIssuer>(o => o.IssuerUri = "urn:my-gds")
|
|
// .AddAccessTokenProvider<MyAccessTokenProvider>()
|
|
// .AddKeyCredentialRequestStore<MyKeyCredentialRequestStore>()
|
|
// .AddConfigurationDataStore<MyConfigurationDataStore>();
|
|
```
|
|
|
|
Throws on a second `.AddGdsServer(...)`. Auto-registers an internal
|
|
`GdsHostedServer : GlobalDiscoverySampleServer` and its
|
|
`ApplicationsNodeManager`. The pluggable services (`IApplicationsDatabase`,
|
|
`ICertificateGroup`, `ICertificateRequest`, etc.) are resolved from the
|
|
container at startup.
|
|
|
|
### Identity (GDS server)
|
|
|
|
`IGdsServerBuilder` forwards every identity-related extension to the
|
|
underlying server builder so a GDS host configures identity the same
|
|
way as a regular server:
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddGdsServer(opt => opt.ApplicationName = "MyGds")
|
|
.AddDefaultIdentityAuthenticators(opt =>
|
|
{
|
|
opt.EnableAnonymous = false;
|
|
opt.EnableUserNamePassword = true;
|
|
})
|
|
.AddJwtIssuer(opt =>
|
|
{
|
|
opt.IssuerUri = "https://login.microsoftonline.com/{tenant}/v2.0";
|
|
opt.JwksUri = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys";
|
|
});
|
|
```
|
|
|
|
`Action<>` and `IConfiguration` overloads are available for
|
|
`AddDefaultIdentityAuthenticators` and `AddJwtIssuer`; `ConfigureRoles`
|
|
takes an `IConfiguration` section. The role manager it registers maps
|
|
identities to Roles on the GDS, unless a regular server (`AddServer`)
|
|
shares the container, which then owns it.
|
|
|
|
UserName and X.509 tokens are handled by the GDS's own authenticators,
|
|
not the generic `UserNamePasswordAuthenticator` and `X509Authenticator`,
|
|
also when an `IUserManagement` is registered or a regular server shares
|
|
the container. The UserName authenticator grants the Roles
|
|
`IUserDatabase` assigns (DiscoveryAdmin, CertificateAuthorityAdmin, ...;
|
|
OPC 10000-12 §6.2, §7.2). The X.509 authenticator validates the user
|
|
certificate against the configured user trust list and grants
|
|
AuthenticatedUser; it does not read Roles from `IUserDatabase`. Use
|
|
`ConfigureRoles` identity mapping rules to give certificate users a GDS
|
|
Role. The `EnableAnonymous`,
|
|
`EnableUserNamePassword`, `EnableX509`, `UserDatabase` and
|
|
`UserCertificateTrustList` options select and configure them. An
|
|
authenticator added with `AddIdentityAuthenticator<T>()` replaces the
|
|
built-in for its token type.
|
|
`AddIdentityAugmenter<T>()` registers post-authentication identity
|
|
augmenters; `AddGdsApplicationSelfAdminProvider()` registers the built-in
|
|
OPC 10000-12 §7.2 SelfAdmin provider and is also wired by the GDS
|
|
`AddDefaultIdentityAuthenticators(...)` helper.
|
|
`AddAuthorizationService(...)` enables the default `CertificateJwtIssuer`;
|
|
`AddAuthorizationService<TIssuer>(...)` registers a custom `ITokenIssuer`
|
|
for cloud KMS, HSM, or external token-service signing.
|
|
|
|
`GdsServerHostedService` consumes these forwarded registrations during
|
|
startup and adds them to the same identity registry used by regular OPC
|
|
UA hosted servers, before the endpoints open.
|
|
|
|
`GdsServerOptions.UserTokenPolicies` (bindable from
|
|
`OpcUa:Gds:Server:UserTokenPolicies`) lists the user token types the GDS
|
|
endpoints advertise. When it is empty the GDS advertises Anonymous and
|
|
UserName, leaving out a type `AddDefaultIdentityAuthenticators` disables:
|
|
Anonymous lets a registered application pull its certificates (OPC
|
|
10000-12 §7.6), while `RegisterApplication` needs DiscoveryAdmin or the
|
|
ApplicationAdmin Privilege (§6.5.6), so an administrator logs in with a
|
|
user name.
|
|
|
|
### Startup tasks (GDS server)
|
|
|
|
The hosted GDS runs startup tasks the way the regular hosted server does:
|
|
pre-startup tasks while the server starts, and startup tasks in
|
|
registration order once it is running, where a failing task stops the
|
|
startup. A startup task is the supported signal that the GDS is
|
|
listening. `GdsReadiness` below stands for a readiness type of your own.
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddGdsServer(opt => opt.ApplicationName = "MyGds")
|
|
.AddPreStartupTask<MyGdsPreStartupTask>()
|
|
.AddStartupTask<MyGdsReadinessTask>()
|
|
.AddStartupTask((sp, context, cancellationToken) =>
|
|
{
|
|
// e.g. flip a readiness probe once the GDS is listening
|
|
sp.GetRequiredService<GdsReadiness>().MarkReady();
|
|
return ValueTask.CompletedTask;
|
|
});
|
|
```
|
|
|
|
Tasks registered through `IGdsServerBuilder` run on the GDS only. Tasks
|
|
registered directly as `IServerStartupTask` or `IServerPreStartupTask`
|
|
run on the GDS only when no regular server (`AddServer`) shares the
|
|
container, so a task meant for the regular server does not run twice.
|
|
|
|
See [Identity Providers](IdentityProviders.md) for the full reference.
|
|
|
|
## LDS Server
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddLdsServer(o =>
|
|
{
|
|
o.ApplicationName = "MyLds";
|
|
o.ApplicationUri = "urn:localhost:MyOrg:MyLds";
|
|
o.ProductUri = "uri:myorg:mylds";
|
|
o.EndpointUrls.Add("opc.tcp://localhost:4840/UADiscovery");
|
|
o.EnableMulticast = true; // LDS-ME multicast advertisement
|
|
o.MulticastLoopbackOnly = false; // set true for in-process tests
|
|
o.ServerCapabilities.Add("DA"); // extra capabilities (LDS / LDS-ME are always implicit)
|
|
});
|
|
```
|
|
|
|
Throws on a second `.AddLdsServer(...)`. The server is advertised as
|
|
`ApplicationType.DiscoveryServer` (the LDS class promotes the type after
|
|
`ApplicationConfiguration.CreateAsync`).
|
|
|
|
`MulticastLoopbackOnly` restricts the mDNS announcer to the loopback NIC
|
|
— intended for in-process tests where LDS-ME traffic must stay local.
|
|
`ServerCapabilities` is additive — `LDS` is always included, plus
|
|
`LDS-ME` when `EnableMulticast` is `true`.
|
|
|
|
## WoT Connectivity Server
|
|
|
|
The WoT Connectivity server lives inside a regular `StandardServer`, so
|
|
`AddWotConServer` must be combined with `AddServer`:
|
|
|
|
```csharp
|
|
services
|
|
.AddOpcUa()
|
|
.AddServer(o => /* regular server options */)
|
|
.AddNodeManager<MyNodeManagerFactory>() // your domain node managers
|
|
.Services
|
|
.AddOpcUa() // returns the same builder
|
|
.AddWotConServer(o =>
|
|
{
|
|
o.AssetNamespaceUri = "http://myorg/UA/WoT-Con/Assets/";
|
|
o.ThingDescriptionStorageFolder = "tds";
|
|
})
|
|
.AddAssetProvider<MyAssetProviderFactory>()
|
|
.AddDiscoveryProvider<MyDiscoveryProvider>();
|
|
```
|
|
|
|
`AddWotConServer` registers the WoT `WotConnectivityNodeManagerFactory`
|
|
as an `OpcUaServerNodeManagerRegistration` so it attaches to the regular
|
|
server feature. `IWotAssetProviderFactory` and `IWotAssetDiscoveryProvider`
|
|
services registered in dependency injection are picked up automatically.
|
|
If `AddWotConServer` is registered without a preceding `AddServer`, startup
|
|
fails with a clear configuration error instead of silently dropping the node
|
|
manager registration.
|
|
|
|
## WoT Connectivity Client
|
|
|
|
```csharp
|
|
services.AddOpcUa().AddClient(/* … */).AddWotConClient();
|
|
|
|
// Resolve and connect on first use:
|
|
var wotClient = sp.GetRequiredService<Func<CancellationToken, Task<WotConnectivityClient>>>();
|
|
WotConnectivityClient client = await wotClient(ct);
|
|
```
|
|
|
|
The WoT client reuses the connected `ManagedSession` registered by
|
|
`AddClient(...)`.
|
|
|
|
## Combined hosts
|
|
|
|
You can run a regular server, a GDS server, and an LDS server inside a
|
|
single Generic Host. Each owns its own:
|
|
|
|
- `OpcUaServerOptions` / `GdsServerOptions` / `LdsServerOptions`
|
|
- PKI root and certificate
|
|
- Endpoint URLs and ports
|
|
- `IApplicationInstance` (created from the shared
|
|
`IApplicationInstanceFactory`)
|
|
- `BackgroundService` (the registrations don't collide)
|
|
|
|
```csharp
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer(o => /* regular server on 51210 */)
|
|
.AddNodeManager<MyNodeManagerFactory>()
|
|
.Services
|
|
.AddOpcUa()
|
|
.AddLdsServer(o => /* LDS on 4840 */)
|
|
.Services
|
|
.AddOpcUa()
|
|
.AddGdsServer(o => /* GDS on 58810 */)
|
|
.AddApplicationsDatabase<MyApplicationsDatabase>()
|
|
.AddCertificateGroup<MyCertificateGroup>()
|
|
.AddCertificateRequest<MyCertificateRequest>()
|
|
.AddUserDatabase<MyUserDatabase>();
|
|
```
|
|
|
|
Node managers registered under a feature wrapper (e.g.
|
|
`OpcUaServerNodeManagerRegistration` for the regular server) are
|
|
isolated: the GDS / LDS hosted services do not see them.
|
|
|
|
## Native AOT
|
|
|
|
Every `.AddXxx(...)` overload — both the `Action<TOptions>` shape and
|
|
the `IConfiguration` / `IConfigurationSection` shapes — is AOT-safe.
|
|
The .NET 8+ Configuration Binding Source Generator is enabled on every
|
|
library that performs configuration binding
|
|
(`<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>`),
|
|
so the reflection-based binder is replaced by statically-generated
|
|
[C# 12 interceptors](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-12#interceptors)
|
|
at compile time.
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddServer(builder.Configuration.GetSection("OpcUa:Server")) // bind from appsettings.json — AOT-safe
|
|
.AddNodeManager<MyAotNodeManagerFactory>();
|
|
```
|
|
|
|
`Action<TOptions>` (code-only) and `IConfiguration` overloads can be
|
|
mixed freely; consumers no longer need to choose between them for AOT
|
|
compatibility.
|
|
|
|
Notes:
|
|
|
|
- The source generator targets net8.0+. On older TFMs (net48) the generator is a no-op and the
|
|
reflection-based binder is used — those TFMs don't support
|
|
PublishAot anyway.
|
|
- Options properties whose type is an interface or a non-default-
|
|
constructible class (e.g. `ApplicationConfiguration`,
|
|
`IUserIdentity`, `ISubscriptionEngineFactory`) are silently skipped
|
|
by the generator with an informational `SYSLIB1100` / `SYSLIB1101`
|
|
diagnostic. Those properties are runtime-only — set them in code,
|
|
not in `appsettings.json`. The affected libraries suppress those
|
|
diagnostics in their csproj.
|
|
- The `tests/Opc.Ua.Aot.Tests` project verifies the end-to-end AOT
|
|
path: build + AOT publish produce **zero** `IL2026` / `IL3050`
|
|
warnings from any dependency injection extension.
|
|
|
|
For AOT consumers, configure options through code or configuration —
|
|
both are supported:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddServer(o =>
|
|
{
|
|
o.ApplicationName = "AotServer";
|
|
o.ApplicationUri = "urn:host:AotServer";
|
|
o.EndpointUrls.Add("opc.tcp://localhost:51210/AotServer");
|
|
o.AutoAcceptUntrustedCertificates = false;
|
|
})
|
|
.AddNodeManager<MyAotNodeManagerFactory>();
|
|
```
|
|
|
|
## Telemetry
|
|
|
|
`AddOpcUa()` registers an `ITelemetryContext` that resolves the host's
|
|
`ILoggerFactory` on first use. To override:
|
|
|
|
```csharp
|
|
services.AddSingleton<ITelemetryContext>(myCustomTelemetry);
|
|
services.AddOpcUa(); // TryAddSingleton — custom one wins
|
|
```
|
|
|
|
To configure logging fluently:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information))
|
|
.AddServer(/* … */);
|
|
```
|
|
|
|
## See also
|
|
|
|
- [Sessions](Sessions.md) — `ManagedSession`, reconnect, subscription engines.
|
|
- [Source Generated NodeManagers](NodeManagers.md#source-generated-node-managers) — `IAsyncNodeManagerFactory` from a model design XML.
|
|
- [Native AOT](NativeAoT.md) — publishing applications and the AOT test harness.
|
|
- [GDS Developer Guide](GDS.md) — GDS service interfaces and provider patterns.
|
|
- [Robotics](Robotics.md) — OPC 40010 hosting, model providers, and topology builders.
|
|
- [WoT Connectivity](WoTConnectivity.md) — OPC 10100-1 information model.
|
|
- [Diagnostics](Diagnostics.md) — `ITelemetryContext` end-to-end.
|
|
- [CryptoProvider](CryptoProvider.md) — `AddCryptoProvider`, hardware-held private keys, FIPS posture and audit.
|