# 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.
93 KiB
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; 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) — seePubSub.mdfor 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
- Root:
services.AddOpcUa() - Shared application configuration
- Options binding
- Server feature
- Server metadata
- Node-manager factories
- Post-startup tasks
- Alias-name stores and standard browse nodes
- Localized resources
- Migrating with an existing configuration XML file
- Server security and resource controls
- First-class server options
- Committed session bindings
- Server-side reverse connect
- Operation limits
- User token policies
- Identity (server)
- Fluent shortcuts and one-shot presets
- Positioning
- Robotics
- Client feature
- Complex types
- Alarms and conditions
- Programs
- Client-side reverse connect
- Channel manager
- Application instance (advanced)
- GDS Client
- GDS Server
- LDS Server
- WoT Connectivity Server
- WoT Connectivity Client
- Combined hosts
- Native AOT
- Telemetry
- 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 |
Opc.Ua.Redundancy.Kubernetes |
serverBuilder.UseKubernetesRaftConsensus(...) |
IOpcUaServerBuilder |
yes (via AddServer) |
see 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 (client), and Identity (GDS server) sections below and the full Identity Providers 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:
- Registers
ITelemetryContextas aServiceProviderTelemetryContextsingleton (viaTryAddSingleton, so any prior user registration wins). - Registers the default
IBufferManagerFactoryandBufferManagerFactoryOptionssingletons (also viaTryAddSingleton). - Returns an
IOpcUaBuilderwhose.Servicesproperty exposes the underlyingIServiceCollectionfor advanced scenarios.
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:
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, 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:
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():
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) without a separate ConfigureApplication(...) call.
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 for trust-store deployment and validation, and Certificate Manager for injectable certificate lifecycle management.
Discovery servers expose the same transport and reverse-connect shortcuts as the regular server builder:
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:
// 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
(<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>),
which replaces the reflection-based binder with statically-generated
C# 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.
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.
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:
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:
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:
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:
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:
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 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:
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 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:
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:
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:
{ "OpcUa": { "Server": { "ConfigurationFile": "MyServer.Config.xml" } } }
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, andPkiRoot- Security-policy toggles and transport quotas
ReverseConnectandConfigureBuilder
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:
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:
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.
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. SetIncludeUnsecurePolicyNone = trueonly for an isolated lab endpoint with no sensitive data or credentials. - Enable
IncludeEccPoliciesonly when the deployment certificates and clients support the advertised ECC policies. - Configure
UserTokenPoliciestogether with matching authenticators. An empty list advertisesAnonymous(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 and Role-Based User Management. - Keep certificate auto-accept disabled and provision trust lists. For
advanced validation, use the shared
ConfigureApplication(...).ConfigureSecuritycallback described above. - Bound transport and service work for the deployment.
MaxByteStringLength,MaxArrayLength,MaxMessageSize, andOperationTimeoutMscontrol transport quotas;OperationLimitsbounds nodes processed by individual services. - Set deployment-specific channel, session, and failed-authentication ceilings
through
ConfigureBuilderto 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.
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 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). |
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 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
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 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
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.
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:
{
"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:
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
for the hosted lifecycle and a loaded-configuration example.
Operation limits
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
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. 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. |
Configuration binding under OpcUa:Server:Identity:
{
"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 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
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:
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:
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:
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:
services.AddOpcUa()
.AddClient(options => { /* endpoint and application options */ })
.AddPositioningClient();
See Relative Spatial Location and Global Positioning.
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().
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.
Client factories compose the Robotics client over the managed session.
AddRoboticsClient() also registers the DI client services because the Robotics
client extends DiTopologyClient:
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.
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:
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). 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:
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:
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):
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:
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:
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:
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:
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.SignAndEncryptand an approved policy such asBasic256Sha256(or a supported stronger policy) during discovery. Do not fall back toSecurityPolicy#Noneoutside 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. - Register an
IClientIdentityProviderappropriate 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.
Identity (client)
Full identity surface is documented in Identity Providers. 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:
{
"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)bindsOpcUa:Client:IdentityintoOpcUaClientOptions.Identity. At session-factory resolution time, if noIClientIdentityProviderservice is explicitly registered, the factory builds a composite from the bound options provided at least one non-default field is set (any ofUserName,X509,IssuedToken,Order, orEnableAnonymous = false). Setting onlyEnableAnonymous = 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 theAddIdentityProvider(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 (anISecretRegistry,ICertificateProvider+ICertificatePasswordProvider, orIAccessTokenProvider) 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
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:
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:
services.AddOpcUa().AddManagedClient(opt =>
{
opt.Configuration = applicationConfiguration;
opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
});
The direct constructor fallback remains available:
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 for provider
contracts, explicit lifetime limits, typed client examples and compatibility rules.
Alarms and conditions
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.
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 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.
{
"OpcUa": {
"Client": {
"ReverseConnect": {
"HoldTimeMs": 15000,
"WaitTimeoutMs": 20000,
"ClientEndpointUrls": [
"opc.tcp://0.0.0.0:4841"
]
}
}
}
}
Equivalent code-only:
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
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
CertificateValidatoris configured for a channel (the normal case for any non-SecurityPolicies.NoneHTTPS profile),HttpsTransportChannelalways 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 namedOpc.Ua.ClientHttpClient pipeline (Polly resilience handler, etc.) is not applied to those channels and the channel emits a one-timeLogWarningto 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 aConfigurePrimaryHttpMessageHandlerthat wires the OPC UAServerCertificateCustomValidationCallbackand the OPC UA application instance certificate yourself. See Sessions and reconnect § HTTPS factory + OPC UA cert validation: secure-by-default fallback.
To override the default channel manager (e.g. to inject a custom
IChannelReconnectPolicy):
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)
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
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
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:
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.
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 for the full reference.
LDS Server
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:
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
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 sharedIApplicationInstanceFactory)BackgroundService(the registrations don't collide)
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
at compile time.
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 informationalSYSLIB1100/SYSLIB1101diagnostic. Those properties are runtime-only — set them in code, not inappsettings.json. The affected libraries suppress those diagnostics in their csproj. - The
tests/Opc.Ua.Aot.Testsproject verifies the end-to-end AOT path: build + AOT publish produce zeroIL2026/IL3050warnings from any dependency injection extension.
For AOT consumers, configure options through code or configuration — both are supported:
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:
services.AddSingleton<ITelemetryContext>(myCustomTelemetry);
services.AddOpcUa(); // TryAddSingleton — custom one wins
To configure logging fluently:
services.AddOpcUa()
.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information))
.AddServer(/* … */);
See also
- Sessions —
ManagedSession, reconnect, subscription engines. - Source Generated NodeManagers —
IAsyncNodeManagerFactoryfrom a model design XML. - Native AOT — publishing applications and the AOT test harness.
- GDS Developer Guide — GDS service interfaces and provider patterns.
- Robotics — OPC 40010 hosting, model providers, and topology builders.
- WoT Connectivity — OPC 10100-1 information model.
- Diagnostics —
ITelemetryContextend-to-end. - CryptoProvider —
AddCryptoProvider, hardware-held private keys, FIPS posture and audit.