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