mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
b0d8ca0d3a
Marc answered the five design questions (2026-10-05). This update
implements the answers; see "Design decisions" at the end.
# Description
OPC 10000-4 §6.3 lets a client invoke a subset of the services without
CreateSession/ActivateSession. The request then carries either an Access
Token in `RequestHeader.authenticationToken` or no token at all. The
server today only offers the low-level
`ISessionManager.ValidateSessionLessRequest` event. Without a handler,
every request whose token names no session is answered with
`Bad_SessionIdInvalid`, including a request without a token. That is the
code a client of an expired session expects; for a session-less request,
§6.3.1 has a server that does not support the invocation answer
`Bad_ServiceUnsupported`.
This PR adds built-in session-less invocation behind an opt-in and keeps
the existing event as the way to take over completely.
## Changes
### `AddSessionlessInvocation()`
(`src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs`)
```csharp
services.AddOpcUa()
.AddServer(options => { /* … */ })
.AddSessionlessInvocation(o => o.AllowAnonymous = false) // the default
.AddJwtIssuer(/* … */); // validates the Access Tokens
```
- Registers `SessionlessInvocationOptions`.
`DependencyInjectionStandardServer` applies them to the `SessionManager`
in use, including one created by a registered session manager factory,
unless that manager already has options.
- Without dependency injection: `SessionManager.SessionlessInvocation =
new SessionlessInvocationOptions()`.
- Without either, nothing changes apart from the
`Bad_ServiceUnsupported` answer below.
### `SessionlessInvocationOptions` (new, `src/Opc.Ua.Server/Session/`)
- `AcceptAccessTokens` (default `true`): an Access Token in
`RequestHeader.authenticationToken`, carried as a String NodeId, is
handed to the server's identity registry (`IServerIdentityRegistry`, the
one ActivateSession uses) as an issued JWT token (OPC 10000-6
§7.6.5.2.3). The resulting identity runs the request. §6.3.1 requires an
encrypted channel for the token, so it is only accepted over
SignAndEncrypt or HTTPS.
- `AllowAnonymous` (default `false`): a request without a token runs as
an anonymous user. Without it, such a request is rejected with
`Bad_IdentityTokenInvalid`.
- `MaxConcurrentRequests` (default 64) and
`MaxConcurrentRequestsPerChannel` (default 16): the budget for
session-less requests, see below. Zero does not limit; a negative value
is rejected.
### Budget for session-less requests (`SessionlessRequestBudget`, new,
internal)
#4541 bounds channels and incomplete messages. A session-less request
has no Session to be accounted for, but it occupies a worker and keeps
its channel active. It gets a budget of its own:
- **Two limits**, checked and taken together under one
`System.Threading.Lock`: concurrent requests on the server, and on one
channel. A request refused by the channel limit takes no server-wide
place. The channel is the SecureChannel on opc.tcp. The HTTPS bindings
use one SecureChannel id per listener, so their requests are grouped by
peer address (IPv4-mapped IPv6 addresses count as IPv4).
- **Answer**: `Bad_ServerTooBusy` ("The Server does not have the
resources to process the request at this time."), the code #4541's
request queue uses when it has no room. `Bad_TooManyOperations` means
too many operations in one request, so it does not fit.
- **Lifetime**: the place is taken before the identity is checked, which
also bounds the validations that run at the same time. The lease is
attached to the `OperationContext` and returned when the context is
disposed, which every service does with `using`, however the request
ends (completed, failed, cancelled). A rejected token, a handler error
or exception, and a cancelled validation return it at once. A lease
releases once, however often it is disposed. Channel entries are removed
with their last request.
- **Scope**: it applies to every session-less request on a manager that
has `SessionlessInvocation` set, including requests a
`ValidateSessionLessRequest` handler decides. A handler without
`AddSessionlessInvocation` runs without a limit, as before. Requests
with a Session token and services outside the §6.3.1 set are not
counted.
- The limits are read for each request, so a change of the options
applies to the next request.
### `SessionManager`
- **Service sets:** a request without a session is limited to the
service sets §6.3.1 allows: View (without
RegisterNodes/UnregisterNodes), Attribute, Method, NodeManagement and
Query. Every other service keeps answering `Bad_SessionIdInvalid`.
`IsSessionlessService(RequestType)` is public.
- **Request without a token:** with session-less invocation not enabled
and no `ValidateSessionLessRequest` handler, it is answered with
`Bad_ServiceUnsupported` (§6.3.1). A token that names no session stays
`Bad_SessionIdInvalid`.
- **Precedence:** a registered `ValidateSessionLessRequest` handler
still decides first, unchanged apart from the service-set limit.
`ValidateSessionlessRequestAsync` is `protected virtual` for servers
that need their own rules.
- **Roles:** the identity of a session-less request goes through the
role resolution ActivateSession applies to the effective identity of a
Session (the `ResolveRoles` part of `AddMandatoryRoles`: the
`MustChangePassword` restriction, `IRoleManager.ResolveGrantedRoles`, so
mapped Access Token claims and the built-in AuthenticatedUser role).
This includes an identity a `ValidateSessionLessRequest` handler sets.
There is no client application certificate without a Session, so no
TrustedApplication role and no application-based mapping.
- **Cancellation:** cancelling an Access Token validation propagates as
`OperationCanceledException` so the endpoint maps it to the status of
the request lifetime (previously it became `Bad_UnexpectedError`). The
budget place is still returned.
- **Log:** a rejected Access Token is logged with a dedicated
`[LoggerMessage]` (`ServerEventIds.SessionManager + 18`, the next free
id after #4632).
### Documentation
- `docs/SessionlessInvocation.md` (new, linked from `docs/README.md`):
which services, enabling it, caller identity, the answers without the
feature, the request limits, the handler, calling the server over
opc.tcp and the REST binding.
- `docs/ResourceIsolation.md`: a short section "Requests without a
Session" pointing to the budget.
- `docs/DependencyInjection.md`: `AddSessionlessInvocation` in the
identity extension tables.
## Tests
- `Opc.Ua.Server.Tests/SessionlessInvocationTests` (mocked server
internals):
- `SessionlessRequestIsUnsupportedUnlessEnabled`,
`AnUnknownSessionTokenStaysBadSessionIdInvalid`,
`ServicesOutsideTheSessionlessSetStillNeedASession` (7 services),
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler`,
`AValidateSessionLessRequestHandlerTakesPrecedenceAsync`;
-
`SessionlessRequestWithoutIdentityIsRejectedUnlessAnonymousIsAllowedAsync`,
`SessionlessAccessTokenIsValidatedByTheIdentityRegistryAsync`,
`AnAccessTokenNoAuthenticatorHandlesIsRejected`,
`SessionlessAccessTokenRequiresAnEncryptedChannel` (None, Sign),
`SessionlessAccessTokenIsAcceptedOverSignAndEncryptAsync`,
`AccessTokensCanBeDisabled`,
`IsSessionlessServiceFollowsTheServiceSetsOfPartFour` (21 cases),
`ValidateSessionlessRequestRejectsMissingArguments`;
- wiring: `AddSessionlessInvocationRegistersTheConfiguredOptions`,
`AddSessionlessInvocationRejectsANullBuilder`,
`TheHostedServerAppliesTheOptionsToItsSessionManager`,
`TheHostedServerAppliesTheOptionsToAFactoryCreatedSessionManager`,
`TheHostedServerKeepsOptionsTheSessionManagerAlreadyHas`.
- End to end against the reference server,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests` (opc.tcp,
`SessionClient` on a channel without CreateSession):
- `ReadWithAnAccessTokenNeedsNoSessionAsync` (SignAndEncrypt),
`AnAccessTokenOnAnUnencryptedChannelIsRejectedAsync`,
`ARequestWithoutAnIdentityIsRejectedAsync`,
`SubscriptionsStillNeedASessionAsync`;
- one sample per service set: `ViewServicesRunWithoutASessionAsync`
(Browse, TranslateBrowsePathsToNodeIds),
`AttributeServicesRunWithoutASessionAsync` (Write, HistoryRead),
`MethodServicesRunWithoutASessionAsync` (Call),
`NodeManagementServicesRunWithoutASessionAsync` (AddNodes). They check
that the service runs and reports its operation result, not a missing
session or an internal error, since #4632 hardened only
Browse/BrowseNext against a missing session. Query is not sampled:
`StandardServer` does not implement QueryFirst/QueryNext.
- REST binding,
`HttpsWebApiIntegrationTests.SessionlessReadOverRestRunsWithAnAccessTokenAsync`
(Compact and Verbose): `POST /read` with the Access Token as String
NodeId (`"AuthenticationToken": "s=<token>"`), and the same request
without a token.
- **Budget**, `Opc.Ua.Server.Tests/SessionlessRequestBudgetTests` (22
tests):
- the budget itself: server limit and release, channel limit that takes
nothing from the server, server limit checked first, zero does not
limit, a lease releases once, HTTPS grouped by peer address (IPv4-mapped
form too), 200 concurrent callers against the limits (exactly the limit
granted, none above the channel limit, parallel release leaves zero and
no tracked channel), 16 callers acquiring and releasing in a loop
(high-water mark never above the limit);
- through `SessionManager.ValidateRequestAsync`: `Bad_ServerTooBusy`
over the server and the channel limit, disposing a context twice
releases once, places returned after a rejected Access Token, an
unencrypted channel, a missing identity, a cancelled validation (while
it waits, the place is taken), a handler error and a handler exception;
the budget also limits requests a handler decides, and a handler-only
server has none; requests outside the service set and requests a
disabled server refuses take no place; changed limits apply to the next
request and new options start a new budget;
- options: finite defaults, negative values rejected.
- **Budget end to end**,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests.SessionlessRequestsRunInABudgetOfTheirOwnAsync`
(opc.tcp, requests held in the Access Token validation): the third
request over a limit of two gets `Bad_ServerTooBusy`; after release,
five sequential reads and five reads of an unknown node (operation
result bad) all pass; a channel limit of one refuses the second request
on that channel while another channel's request is admitted.
`ARequestWithoutATokenIsUnsupportedWhileTheFeatureIsOffAsync`:
`Bad_ServiceUnsupported` for a request without token on a server without
the feature, `Bad_SessionIdInvalid` for a token that names no Session.
- **Fail without the change**: the places-returned tests fail when the
budget does not release on failure (4 of 22 fail with the release in
`SessionManager` removed; the other budget tests use API this change
adds). `SessionlessRequestIsUnsupportedUnlessEnabled` and
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler` are
unchanged from the first version.
- **Review follow-up (`e9a35980b`)**:
`SessionlessAccessTokenReceivesTheRolesMappedFromItsClaimsAsync`,
`SessionlessAccessTokenWithoutTheMappedClaimIsNotGrantedTheRoleAsync`,
`AnAnonymousSessionlessRequestIsGrantedTheAnonymousRoleOnlyAsync`,
`TheIdentityOfAValidateSessionLessRequestHandlerReceivesTheMappedRolesAsync`,
`AValidateSessionLessRequestHandlerThatSetsNoIdentityKeepsNoIdentityAsync`;
end to end
`ARoleMappedNodeIsReadWithTheRolesMappedFromTheAccessTokenAsync` (the
ServerDiagnosticsSummary, readable for SecurityAdmin only, is denied for
a token whose claim is not mapped, readable once the rule maps it,
denied for another token) and
`ACancelledAccessTokenValidationAnswersTheTimeoutAndReturnsItsPlaceAsync`.
The anonymous and no-identity tests guard unchanged behavior. With the
`SessionManager.cs` change removed the three claim-mapping tests, the
end-to-end role test (`Bad_UserAccessDenied`), the end-to-end
cancellation test (`Bad_UnexpectedError` instead of `Bad_Timeout`) and
`ACancelledValidationReturnsItsPlaceAsync` fail. Lines of this commit
were not re-measured for patch coverage; the figure below is for
`496f84fce`.
- **`ClientTest`**: the three assertions that accepted
`Bad_ServiceUnsupported` too are back to master (`Bad_SessionIdInvalid`
only), so the PR no longer touches that file. Since #4646 a closed
`Session` refuses a request without authenticationToken itself; the
server code is not reached. The three tests pass with the single code
(12 cases on net10.0).
- Results on macOS: `Opc.Ua.Server.Tests` full run on net10.0 8076
passed / 9 skipped / 1 failed
(`ACancelledValidationReturnsItsPlaceAsync`, which asserted the old
`Bad_UnexpectedError` mapping; updated to the propagated cancellation
and passing); the session-less, SessionManager and role filters 368
passed / 0 failed on net10.0 and on net8.0, and the session-less
invocation and budget tests 73 passed on net10.0 after the last test was
added. `Opc.Ua.Sessions.Tests` with the
HTTPS/WebApi/WSS/OpenApi/Sessionless/ClientTest filter: 400 passed / 241
skipped / 0 failed on net10.0 and on net8.0 (the earlier host-name
failures were an environment issue of the test machine and are gone).
- Builds: `Opc.Ua.Server`, `Opc.Ua.Server.Tests` and
`Opc.Ua.Sessions.Tests` for net48, net8.0, net9.0 and net10.0 and
`Opc.Ua.Server` in Release net10.0: 0 errors, no new warnings (the
CA2213 in `SamplingGroup.cs` and the CA1861/CA2025 in other test files
are on master too).
- `dotnet format` (whitespace, style, analyzers `--severity info`, one
`--include` per changed file): no finding on a changed line. What
remains is on master lines (`SessionManager.cs` 552, 1374, 2205 and
`DependencyInjectionStandardServer.cs` 228) and the line-ending finding
of a macOS checkout.
- Coverage (`.azurepipelines/check-coverage.ps1` against master, only
the session-less tests of `Opc.Ua.Server.Tests` on net10.0): patch
105/116 changed lines (90.5 %). The 11 uncovered lines are Session-bound
lines of `ValidateRequestAsync` and the lockout logger that moved with
the change; the rest of the suite covers them. The project floor is not
meaningful for this subset.
- Fork CI and CodeQL: previous head `0fba9f21d` (master incl. #4674
merged): [CI run
37307824769](https://github.com/biancode/UA-.NETStandard/actions/runs/37307824769)
green (one net48 timing test, `KeepAliveHandlerV2Async`, failed in the
first attempt and passed on the job rerun; the PR does not touch
subscriptions), [CodeQL run
37307829138](https://github.com/biancode/UA-.NETStandard/actions/runs/37307829138)
green. Current head `e9a35980b`: [CI run
37364178979](https://github.com/biancode/UA-.NETStandard/actions/runs/37364178979),
[CodeQL run
37364182697](https://github.com/biancode/UA-.NETStandard/actions/runs/37364182697)
(results pending).
## Design decisions
Marc's answers (2026-10-05) and what the PR does:
1. **Built in** when `AddSessionlessInvocation` was used. Done.
2. **Service-set limit for all paths**: it applies before a
`ValidateSessionLessRequest` handler too. Done, unchanged.
3. **`Bad_ServiceUnsupported`** for a request without a token on a
server without the feature. Done, unchanged. A token that names no
Session stays `Bad_SessionIdInvalid`.
4. **Identity model**: an Access Token in the RequestHeader, validated
as an issued JWT token through the identity registry, anonymous only by
opt-in. Unchanged. The REST authentication is #4674, now in master.
5. **Separate budget** for session-less requests: added, see "Budget for
session-less requests". Open: no limit per identity yet (identities are
claims-defined, so the keys would be unbounded); say if you want one.
## Related Issues
- Fixes #4658
- Relates to #4657 (the REST binding's session-less service set), #4646
(a closed `Session` no longer sends requests without token), #4541
(resource isolation) and #4674 (REST authentication)
## Checklist
- [x] 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.
- [x] 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. (Affected
suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered
by the fork CI's Windows net48 legs.)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
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 `ISessionBindingProvider` 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.
|