mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
# Description
Adds `MinimalClient`, a compact console OPC UA application demonstrating
the unified dependency-injection surface, secure endpoint discovery,
managed sessions, fluent V2 subscriptions, and Alarms & Conditions
support.
## Changes
- Adds `ConfigureApplication(options => ...)` to build and validate one
shared `ApplicationConfiguration` for combined client/server hosts.
- Extends `OpcUaClientOptions` so the existing `AddClient(options =>
...)` callback accepts application identity, PKI, and
certificate-validation settings like `AddServer(...)`.
- Composes root `ConfigureApplication(...)` values with client defaults
in either registration order; shared values take precedence and client
values fill gaps.
- Keeps server samples on their established `AddServer(...)`
configuration pattern.
- Adds `MinimalClient` as a .NET 10 Native AOT console application using
top-level statements and `HostApplicationBuilder`.
- Discovers a `SignAndEncrypt` / `Basic256Sha256` endpoint by default;
`--insecure` and `--auto-accept` are explicit opt-ins with warnings.
- Demonstrates fluent `AddSubscription(...)`,
`TryAddMonitoredItem(...)`, and `AlarmClientFactory` usage.
- Adds targeted configuration, client, and combined client/server tests.
## Example
```csharp
builder.Services
.AddOpcUa()
.AddClient(options =>
{
options.ApplicationName = "MinimalClient";
options.ApplicationUri = "urn:localhost:OPCFoundation:MinimalClient";
options.ProductUri = "uri:opcfoundation.org:MinimalClient";
options.Session = new ManagedSessionOptions
{
SessionName = "MinimalClient",
SessionTimeout = TimeSpan.FromSeconds(60)
};
})
.AddDiscoveryAndConnect(options =>
{
options.DiscoveryUrl = discoveryUrl;
options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
})
.AddSubscriptions()
.AddAlarms();
```
## Related Issues
_If there is no issue yet, open one and link it here._
## Checklist
- [ ] 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.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
1140 lines
48 KiB
Markdown
1140 lines
48 KiB
Markdown
# Dependency Injection
|
|
|
|
This document describes the unified `Microsoft.Extensions.DependencyInjection`
|
|
surface for the OPC UA .NET Standard libraries. The surface is rooted in
|
|
a single `services.AddOpcUa()` call that returns an `IOpcUaBuilder` on
|
|
which every feature library hangs its own fluent `.AddXxx(...)` extension.
|
|
|
|
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.Common`)
|
|
- The GDS server (`src/Opc.Ua.Gds.Server.Common`)
|
|
- 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.
|
|
|
|
The non-dependency-injection public constructors and factories of every library
|
|
(`new ApplicationInstance(telemetry)`, `new StandardServer(telemetry)`,
|
|
`new LdsServer(telemetry)`, `new ManagedSession(...)` etc.) remain
|
|
unchanged. Use dependency injection when you want the .NET Generic Host to own application
|
|
lifetime, logging, and configuration; use the manual constructors when
|
|
you need finer control.
|
|
|
|
## 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.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.Gds.Client.Common` | `builder.AddGdsClient(opt => …)` | `IGdsClientBuilder` | — | `OpcUa:Gds:Client` |
|
|
| `Opc.Ua.Gds.Server.Common` | `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 | — |
|
|
| `AddIdentityAugmenter<T>()` | server, gds | — |
|
|
| `AddGdsApplicationSelfAdminProvider()` | gds | — |
|
|
| `AddJwtIssuer(...)` | server, gds | `OpcUa:Server:Identity:Issuers[]`|
|
|
| `WithAuthorizationService(...)` / `<TIssuer>()` | gds | — |
|
|
| `WithKeyCredentialPush(...)` | 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`.
|
|
|
|
Register options before `AddOpcUa()` to select an implementation explicitly or apply a process-wide 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 synchronous rent blocks without holding a manager lock until another buffer is returned. A single rent whose conservative expected size exceeds the budget fails immediately instead of waiting forever.
|
|
|
|
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.AutoAcceptUntrustedCertificates = 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.
|
|
|
|
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);
|
|
```
|
|
|
|
## 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 => …)` registers an OPC UA `StandardServer` as an
|
|
`IHostedService` via a private `OpcUaServerHostedService`. 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 = true;
|
|
o.EndpointUrls.Add("opc.tcp://localhost:51210/MyServer");
|
|
})
|
|
.AddNodeManager<MyNodeManagerFactory>() // IAsyncNodeManagerFactory
|
|
.AddSyncNodeManager<LegacyNodeManagerFactory>(); // INodeManagerFactory
|
|
|
|
await builder.Build().RunAsync();
|
|
```
|
|
|
|
`.AddNodeManager<T>()` and `.AddSyncNodeManager<T>()` register the
|
|
factory 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.
|
|
|
|
Advanced server services can be supplied through the same fluent builder:
|
|
|
|
```csharp
|
|
builder.Services
|
|
.AddOpcUa()
|
|
.AddServer<MyStandardServer>(o => /* … */) // optional StandardServer subclass
|
|
.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.
|
|
|
|
For advanced configuration (custom security policies, custom security
|
|
stores), set `OpcUaServerOptions.ConfigureBuilder` — it receives the
|
|
underlying `IApplicationConfigurationBuilderServerSelected` between the
|
|
default policy/quota steps and `CreateAsync`.
|
|
|
|
### 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). Anything not listed here remains
|
|
reachable through `ConfigureBuilder`.
|
|
|
|
| Property | Underlying builder call | Purpose |
|
|
|----------|-------------------------|---------|
|
|
| `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. |
|
|
| `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/...). |
|
|
|
|
### Server-side reverse connect
|
|
|
|
A server can dial back to clients via reverse-hello using
|
|
`OpcUaServerOptions.ReverseConnect`. 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
|
|
});
|
|
});
|
|
```
|
|
|
|
### 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.
|
|
|
|
### 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. |
|
|
| `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. |
|
|
|
|
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.
|
|
|
|
### 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.
|
|
Custom server types that need session, subscription, or durable-subscription DI hooks must derive from
|
|
`DependencyInjectionStandardServer`; otherwise startup fails fast instead of ignoring those hooks.
|
|
|
|
Fluent node managers can be registered without a factory class:
|
|
|
|
```csharp
|
|
services.AddOpcUa()
|
|
.AddReferenceServer()
|
|
.AddNodeManager("urn:example:line", nodes =>
|
|
{
|
|
nodes.Node("ReferenceServer");
|
|
});
|
|
```
|
|
|
|
`AddHistorianFileStore(provider, path)` combines the historian provider registration with a Part 20
|
|
file-system mount for demo and lab servers.
|
|
|
|
## 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), 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.
|
|
|
|
### 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;
|
|
});
|
|
```
|
|
|
|
### 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 = true;
|
|
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
|
|
`ConfigureRoles`, `AddDefaultIdentityAuthenticators`, and `AddJwtIssuer`.
|
|
`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.
|
|
|
|
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 /
|
|
netstandard2.0 / netstandard2.1) 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 = true;
|
|
})
|
|
.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](SourceGeneratedNodeManagers.md) — `IAsyncNodeManagerFactory` from a model design XML.
|
|
- [Native AOT](NativeAoT.md) — AOT testing setup.
|
|
- [GDS Developer Guide](GDS.md) — GDS service interfaces and provider patterns.
|
|
- [WoT Connectivity](WoTConnectivity.md) — OPC 10100-1 information model.
|
|
- [Diagnostics](Diagnostics.md) — `ITelemetryContext` end-to-end.
|