Files
UA-.NETStandard/docs/DependencyInjection.md
T
Marc Schier 397db603db Add OPC UA for Asset Administration Shell: the published OPC 30270 and v3 draft (#4244)
# Description

Implements the **Asset Administration Shell** companion specifications
on top of `marcschier/wot-type-binding`, in two halves that a Server
chooses between:

* **AAS V3** — the
[draft](https://github.com/marcschier/opcua-drafts/tree/main/companion-specs/AAS)
(`http://opcfoundation.org/UA/I4AAS/v3/`), all sixteen conformance
units: the metamodel, the registry built on the abstract xRegistry base,
generational materialization, federation, packages with digest-bound
integrity, the Digital Product Passport vocabulary, and the Annex F
bridge to WoT Thing Descriptions.
* **AAS V2** — the published [OPC
30270](https://reference.opcfoundation.org/specs/OPC-30270/full)
(`http://opcfoundation.org/UA/I4AAS/`), which maps the AAS V2.0.1
metamodel: object model, ingestion of XML, JSON and AASX,
materialization, server and client.

The two are different metamodel generations rather than two revisions of
one model — V2 keeps `Asset` as a first-class Identifiable,
`identification.idType`, Views and ordered collections, none of which
survive into V3 — so they are **alternatives**. A Server hosts one or
the other; this is documented rather than enforced at startup. Each is
named for the generation it maps (`Opc.Ua.Aas.V2`, `Opc.Ua.Aas.V3`,
`AddAasV2Server`, `AddAasV3Server`), and what genuinely serves both
carries no marker and sits at the root of `Opc.Ua.Aas`: the
deterministic NodeId encoding and idShortPath conventions,
`AasOptional<T>`, the digest rule, the materialization diagnostics, the
AASX container walk, and the value and operation providers, which are
keyed on `NodeId` and `Variant` and so carry no version at all.

## What is in here

**New libraries**: `Opc.Ua.Aas`, `Opc.Ua.Aas.Server`,
`Opc.Ua.Aas.Client`, `Opc.Ua.Aas.WoT`, plus `samples/Aas` and
`docs/Aas.md`.

**Enabling changes to existing code**, each needed by the above:

* The OPC UA `Decimal` DataType added to `Opc.Ua.Types`, with its Part 6
§5.1.10 wire encoding. `BinaryDecoder` gained one internal member,
`TryReadRemainingBodyBytes`, because a trailing field with no
self-describing length cannot otherwise be read; `Decimal.Decode` is its
only caller. Part 6 Table 3 types the unscaled `Value` as an
**OctetString whose byte count is `Length` − 2**, not a length-prefixed
`ByteString`, so it carries no length of its own —
`DecimalEncodingTests` pins the body byte-for-byte against that mistake,
and against the `Length ≤ 2` values the same table calls invalid.
* The xRegistry projection engine extracted into
`Opc.Ua.XRegistry.Server` and `Opc.Ua.WotCon.Server` migrated onto it
(−848/+387 lines), with the WoT Connectivity tests passing **unedited**.
* Two source-generator fixes, both of which made a model impossible to
build (see below).

## Defects found and fixed along the way

Running the stack end to end, rather than trusting per-area test suites,
surfaced defects that no unit test caught. The ones worth a maintainer's
attention:

* **The generator emitted non-terminating constructors.** A structure
field with no declared default was initialized with `new T()`, so a
self-referential DataType (`AASReferenceDataType.referredSemanticId`)
recursed until the stack overflowed and `AddOpcUaAas` could not load the
model at all. The test host *died* rather than failing. Only the field
that closes a cycle is now left null.
* **The generator omitted `new` on a redeclared placeholder.**
`AASOrderedSubmodelElementCollectionType` redeclares the
`SubmodelElement` slot of its base type, and the generated `Add` method
hid the base one without saying so, so the V2 model would not compile.
No existing model could have hit either defect — both are build
failures, so anything that builds today is unaffected.
* **The metamodel half had never worked outside its unit tests**: the
emitted NodeSet declared no reference-type aliases and was rejected by
the loader; the Mandatory `Invoke` Method was never emitted, because a
NodeSet is imported as written rather than instantiated from its type;
the runtime bound at the authored namespace index instead of the one the
Server assigns; and `MethodDeclarationId` was addressed in the wrong
namespace. Every materializer test inspected the in-memory object graph,
which cannot see whether the graph is loadable — two tests now import it
exactly as `RuntimeNodeSetNodeManager` does.
* **The client could not see any Property member.** It browsed
`HasComponent` with subtypes, but `HasProperty` is a sibling under
`Aggregates`, so reading a value failed and invoking failed argument
validation.
* **Every repeated registry write forked a duplicate group and
resource** instead of adding a version, because the identifier was
re-derived against a sibling set that already contained the entity
itself. This also made the persistence bounds unreachable.
* `.gitignore`'s `**/packages/*` silently swallowed the whole
package-integrity implementation and its tests, which would have
vanished on a fresh clone.

## Specification findings

Nine findings against the V3 draft are fixed in
[marcschier/opcua-drafts#75](https://github.com/marcschier/opcua-drafts/pull/75).
The substantive one: clause 6.1.5's absent-versus-empty rule was
**unsatisfiable** for five container fields that materialize as parent
components with no node of their own, which made `AAS-LosslessRoundTrip`
unachievable.

OPC 30270 contradicts itself twice. Both are recorded in
`AasV2ConformanceUnits.cs` and the documentation rather than smoothed
over: Table 83 spells the unit `I4AAS MultiLangaugeProperty` where Table
85 spells it correctly, and Table 85 lists an `I4AAS Security` unit that
Table 83 never defines. Table 84, which would assign profile URIs, is
empty, so `ServerProfileArray` gains no I4AAS entry.

## Known limitation

Calling an `Operation` **with arguments** answers `BadTooManyArguments`.
Method resolution works from both the instance NodeId and the
`MethodDeclarationId`, and the argument definitions are published, but
`NodeState.AddChild` does not route through `MethodState.FindChild`, so
a Method imported from a NodeSet leaves its typed `InputArguments` null.
Closing this means changing how the shared NodeSet import claims typed
children, which affects every consumer of runtime NodeSets and is out of
scope here. It is documented in `docs/Aas.md` §11, the conformance
matrix marks `AAS-OperationInvoke` partial rather than claiming it, and
`samples/Aas` exercises the path and reports the status rather than
working around it. V2 is unaffected — its `Operation` is an
OptionalPlaceholder with no declared arguments.

## Related Issues

- No tracking issue yet. Opening as a **draft** for design discussion
first, per the ADR guidance below; happy to open one and link it once
the shape is agreed.

## 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.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Verification

| Check | Result |
|---|---|
| AAS tests | **670 passed, 0 failed** on net472, net48, net8.0, net9.0,
net10.0 |
| Coverage | Client 80.8 %, Server 81.2 %, `Opc.Ua.Aas` 82.0 %, WoT 82.4
% — all above the 80 % gate |
| Libraries | `Opc.Ua.Aas`, `.Server`, `.Client`, `.WoT` build clean on
every target framework, zero warnings |
| Regression | `Opc.Ua.Types` 8677, `Opc.Ua.WotCon` 1094,
`Opc.Ua.XRegistry` 198, `Opc.Ua.Server` 4050 — all green |
| Sample | `samples/Aas` builds warning-free and runs end to end |
| Merge validation | Solution builds clean on net10.0 and net48; WoT
Types 504, WotCon 1111, AAS 670, XRegistry 198 tests passed |
| CI | All 99 checks green on `ff9298e44`; only the CLA signature status
is outstanding |

### Review pass

A full review against both specifications, and for API consistency and
correctness, produced twelve
fixes. Each one has a test that fails without it.

| Defect | Where | Why it mattered |
|---|---|---|
| V2 projection aborted on **every** `File` read from a document |
`AasV2EnvironmentRuntime` | The runtime bound `File/Value`
unconditionally, the materializer emitted it only with content, and the
builder throws `BadNotFound` for an unknown NodeId — aborting the whole
registration. No reader ever populates `AasFile.File`, so no AAS V2
document could be hosted at all. |
| `aas-suppl` anchored on the wrong part | `AasxPackageReaderCore`,
`AasxPackageWriter` | IDTA and the Package Explorer anchor it on the
environment part; we used the origin part. Every real `.aasx` returned
zero supplementary files, silently. Affects both halves. |
| Federation failed closed on pooled connections | `AasFederation` |
`ConnectCallback` fires only for new connections, so the second fetch to
an origin — and all of net472/net48/netstandard2.1 — failed with a
security-flavoured error. The peer is now validated *inside* the
callback, which makes reuse safe by construction. |
| Malformed `Decimal` escaped as `ArgumentOutOfRangeException` |
`BinaryDecoder` | A declared body shorter than `Scale` produced a
negative read length. Two of the three test cases had been passing for
the wrong reason. |
| V2 and V3 cross-wired their value providers |
`OpcUaAasServerBuilderExtensions` | Both registered `IAasValueProvider`;
whichever was added first served both AddressSpaces. V2 now has its own
contract, so co-hosting is harmless rather than silently wrong.
`EnvironmentFolder` was also bound and ignored on the V2 path. |
| Export filter skipped three nesting kinds | `AasEnvironmentExporter` |
Content under `Entity`, `AnnotatedRelationshipElement` and `Operation`
was exported unevaluated *and* stamped with a Digest presenting it as
complete (clause 6.5.10). List authorization paths also shifted after a
denial. |
| Concealment held at the Method but not in the projection |
`AasRegistryModel` | `GetSubmodel` answered `BadNotFound` while an
anonymous Browse revealed the node, its identifier, semanticId and
digest (clause 6.5.7). |
| File handles were unsynchronized and unowned | `AasElementFileManager`
| The async node manager holds no lock, so concurrent Calls raced a bare
`Dictionary`; sequential handles let any Session read, seek or commit
another's. Writes were also unbounded. |
| Projection host was unimplementable outside the stack |
`AasEnvironmentProjection` | The handle demanded a type with an internal
constructor — our own tests used
`FormatterServices.GetUninitializedObject` to get round it. Now mirrors
WotCon's interface-typed registration. |
| Embedded `FileType` omitted 7 of 10 Mandatory members |
`AasEnvironmentMaterializer` (V2) | OPC 10000-3 6.4.4 requires them all;
without `Size` and `OpenCount` a Client cannot size a read or detect an
abandoned handle. |
| Round-trip oracle ignored most of the model | `AasRoundTripComparer` |
`Blob`, `File`, `ReferenceElement` and `BasicEventElement` had no case
arm, so their fields compared equal by falling off the switch;
`valueType`, `semanticId`, `entityType` and `Key.type` were never read.
The clause 6.4 losslessness claim rested on it. |

Known, unchanged: calling an `Operation` with arguments answers
`BadTooManyArguments`, because
`NodeState.AddChild` does not route through `MethodState.FindChild`.
Documented in `docs/Aas.md` §11.

Not addressed in this pass, and worth a maintainer decision: OPC 30270
§9.2 says all NodeIds of
Nodes not defined in the companion specification **shall not** use the
standard namespaces, and
Table 87 marks a vendor-specific instance namespace Mandatory. Both
halves currently mint instances
in the companion namespace. Separating them touches NodeId minting, the
runtime and the client
together, so it is better done deliberately than folded into a review
pass.
This is a large change and touches shared code (`Opc.Ua.Types`, the
source generator, the xRegistry projection). It is deliberately a draft:
the enabling changes are separable if maintainers would prefer them
split into their own PRs. The latest `master` is merged into this branch
in `0dd0ff0e1`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Copilot-Session: c3d88950-4a65-4ff6-87d2-1f545a875c9b
Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782
2026-08-18 08:11:47 -07:00

64 KiB

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 AAS V3 server (src/Opc.Ua.Aas.Server)
  • The AAS V3 client (src/Opc.Ua.Aas.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 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.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.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.Aas.Server builder.AddAasV3Server(opt => …) IAasServerBuilder yes (via AddServer) OpcUa:Aas:Server
Opc.Ua.Aas.Client builder.AddAasV3Client(opt => …) / AddAasV3RegistryClient(opt => …) IOpcUaBuilder — OpcUa:Aas:Client
Opc.Ua.PubSub builder.AddPubSub(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (publish-only) builder.AddPubSubPublisher(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (subscribe-only) builder.AddPubSubSubscriber(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (SKS client) builder.AddPubSubSecurityKeyServiceClient(...) IPubSubBuilder — OpcUa:PubSub:Sks:Client
Opc.Ua.PubSub (SKS server) builder.AddPubSubSecurityKeyServiceServer(...) IPubSubBuilder yes OpcUa:PubSub:Sks:Server
Opc.Ua.PubSub.Udp pubSub.AddUdpTransport() IPubSubBuilder — —
Opc.Ua.PubSub.Mqtt pubSub.AddMqttTransport() IPubSubBuilder — —
Opc.Ua.PubSub.Server serverBuilder.AddPubSubAddressSpace(...) IPubSubServerBuilder yes OpcUa:PubSub:AddressSpace
Opc.Ua.Redundancy.Server serverBuilder.UseDistributedAddressSpace(...) / UseReplicatedAddressSpace(...) / UseActiveActiveRedundancy(...) / UsePeerDiscovery(...) / AddServerRedundancy(...) IOpcUaServerBuilder yes (via AddServer) see HighAvailability.md
Opc.Ua.Redundancy.Kubernetes serverBuilder.UseKubernetesRaftConsensus(...) IOpcUaServerBuilder yes (via AddServer) see Kubernetes.md

Identity-provider extensions hang off IOpcUaServerBuilder, IOpcUaClientBuilder, and IGdsServerBuilder:

Extension Builder Section
AddDefaultIdentityAuthenticators(...) server, gds OpcUa:Server:Identity:Defaults
AddIdentityAuthenticator<T>() server, gds —
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 (client), and Identity (GDS server) sections below and the full Identity Providers guide.

Server features marked Hosted? = yes register an IHostedService so the .NET Generic Host (Host.CreateApplicationBuilder(args)) owns their lifetime, certificate setup, and Ctrl+C / SIGTERM handling.

Root: services.AddOpcUa()

services.AddOpcUa() is the only entry point. It does three things:

  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.
using Microsoft.Extensions.DependencyInjection;

services.AddOpcUa();

AddOpcUa is idempotent. The returned IOpcUaBuilder derives from the older IDependencyInjectionBuilder so existing builder.AddLogging() / builder.AddMetrics() extensions still compile unchanged. New AddLogging(this IOpcUaBuilder) / AddMetrics(this IOpcUaBuilder) overloads return IOpcUaBuilder for fluent chaining into feature methods:

services.AddOpcUa()
    .AddLogging(b => b.AddConsole())
    .AddMetrics()
    .AddServer(o => /* … */)
    .AddNodeManager<MyNodeManagerFactory>();

Buffer managers

Transport listeners and channels resolve IBufferManagerFactory from dependency injection. The default factory selects FastBufferManager in Release builds, CookieBufferManager in Debug builds, and TracingBufferManager when the stack is compiled with TRACK_MEMORY.

Register options before AddOpcUa() to select an implementation explicitly or apply a process-wide outstanding-buffer budget:

services.AddSingleton(new BufferManagerFactoryOptions
{
    ImplementationKind = BufferManagerImplementationKind.Fast,
    MaxOutstandingBytesPerProcess = 256L * 1024 * 1024
});

services.AddOpcUa()
    .AddOpcTcpTransport()
    .AddHttpsTransport();

When MaxOutstandingBytesPerProcess is positive, the singleton factory wraps every manager it creates with LimitingBufferManager and shares one BufferManagerMemoryLimiter across them. A 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():

services.AddSingleton<IBufferManagerFactory, MyBufferManagerFactory>();
services.AddOpcUa();

The existing new BufferManager(name, maxBufferSize, telemetry) path remains available for direct creation. BufferManager is a compatibility facade over IBufferManager; FastBufferManager, CookieBufferManager, TracingBufferManager, and LimitingBufferManager can also be created directly for advanced scenarios.

Shared application configuration

ConfigureApplication(...) defines the application identity, PKI root, and common certificate-validation settings once. AddClient(...), AddServer(...), or both contribute their feature-specific sections to one ApplicationConfiguration; the provider calls ApplicationConfigurationBuilder.CreateAsync, validates the completed configuration, and ensures the application instance certificate exists before a client connects or a hosted server starts. Use ConfigureApplication(...) for combined client/server hosts, or when several AddClient(...)/AddServer(...) registrations must share one application identity and certificate lifecycle. A client-only host can instead set the same identity fields directly on OpcUaClientOptions inside AddClient(...) (see Client feature) without a separate ConfigureApplication(...) call.

IOpcUaBuilder opcUa = services.AddOpcUa()
    .ConfigureApplication(options =>
    {
        options.ApplicationName = "MyApplication";
        options.ApplicationUri = "urn:localhost:MyApplication";
        options.ProductUri = "uri:example.com:MyApplication";
        options.SubjectName = "CN=MyApplication, O=Example, DC=localhost";
        options.PkiRoot = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Example",
            "MyApplication",
            "pki");
        options.AutoAcceptUntrustedCertificates = false;
        options.RejectSHA1SignedCertificates = true;
        options.MinimumCertificateKeySize = 2048;

        // Code-only advanced settings, applied after the values above.
        options.ConfigureSecurity = security => security
            .SetMaxRejectedCertificates(20)
            .SetRejectUnknownRevocationStatus(true)
            .SetUseValidatedCertificates(false)
            .SetSendCertificateChain(true);
    });

opcUa.AddClient(options =>
{
    options.Session = new ManagedSessionOptions
    {
        SessionName = "MyClient"
    };
});

opcUa.AddServer(options =>
{
    options.EndpointUrls.Add("opc.tcp://localhost:4840/MyApplication");
});

When both features are registered, the shared configuration has ApplicationType.ClientAndServer and one application-certificate lifecycle. An explicitly supplied OpcUaClientOptions.Configuration still wins for that client registration.

Application identity and certificate defaults

Setting Behavior when omitted
ApplicationName Required after client/server feature defaults are applied. The server feature defaults to OpcUaServer; client-only applications should set it explicitly.
ApplicationUri Generated from the host name and application name during validation. Set a stable URI for deployed applications.
ProductUri Uses the contributing feature value. Set a stable product URI for deployed applications.
SubjectName CN={ApplicationName}, O=OPC Foundation, DC=localhost; DC=localhost is replaced with the host name.
PkiRoot A per-application OPC Foundation/{ApplicationName}/pki directory below the process temporary directory. 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 starts with secure defaults: unknown certificates are not auto-accepted, the application certificate is not copied into a shared trusted store, SHA-1 certificates and unknown revocation status are rejected, nonce validation errors are not suppressed, certificate chains are sent, the minimum RSA key size is 2048, and at most five rejected certificates are retained. Validated-certificate caching is off unless enabled explicitly.

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, 0 keeps all, and a negative value keeps no history.
SetAutoAcceptUntrustedCertificates(...) Accepts otherwise-valid unknown peer certificates; default false. Prefer AutoAcceptUntrustedCertificates. Use true only in an isolated lab.
SetAddAppCertToTrustedStore(...) Adds a newly created application certificate to a shared trusted store; default false.
SetRejectSHA1SignedCertificates(...) Rejects SHA-1-signed certificates; default true. Prefer RejectSHA1SignedCertificates.
SetRejectUnknownRevocationStatus(...) Rejects chains when CA revocation status cannot be determined; default true.
SetUseValidatedCertificates(...) Reuses previously validated certificates without repeating the full validation path; default false. Leave disabled unless the reduced revalidation is explicitly acceptable for the deployment.
SetSuppressNonceValidationErrors(...) Suppresses zero/weak nonce errors; default false. Enabling it weakens user-token protection and is only for unavoidable legacy interoperability.
SetSendCertificateChain(...) Sends the chain with a CA-signed application certificate; default true.
SetMinimumCertificateKeySize(...) Sets the minimum accepted RSA key size; default 2048. Prefer MinimumCertificateKeySize.
AddCertificatePasswordProvider(...) Supplies an ICertificatePasswordProvider for protected private keys; no provider is registered by default.

See Certificates for trust-store deployment and validation, and Certificate Manager for injectable certificate lifecycle management.

Discovery servers expose the same transport and reverse-connect shortcuts as the regular server builder:

services.AddOpcUa()
    .AddInMemoryGdsServer(o => o.EndpointUrls.Add("opc.tcp://localhost:58810/GDS"))
    .AddOpcTcpTransport()
    .AddHttpsTransport()
    .AddReverseConnect(o => o.Clients.Add(new ServerReverseConnectClientOptions
    {
        EndpointUrl = "opc.tcp://client.example.com:4841"
    }));

services.AddOpcUa()
    .AddLdsServer(o => o.EndpointUrls.Add("opc.tcp://localhost:4840/LDS"))
    .AddRegistrationStore<MyRegisteredServerStore>()
    .AddMulticastDiscovery<MyLdsMeFactory>();

services.AddOpcUa()
    .AddWotConnectivityServer(
        server => server.EndpointUrls.Add("opc.tcp://localhost:4840/WoT"),
        wot => wot.AssetNamespaceUri = WotConnectivityServerOptions.DefaultAssetNamespaceUri);

Options binding

Every feature .AddXxx(...) has three overloads:

// 1. Action — AOT-safe, recommended for all consumers.
builder.AddServer(o => { o.ApplicationName = "MyServer"; /* … */ });

// 2. IConfiguration — bind from a configuration root's default section.
builder.AddServer(builder.Configuration);

// 3. IConfigurationSection — bind from an explicit section.
builder.AddServer(builder.Configuration.GetSection("MyApp:Server"));

The default section names are:

Feature Section
Server OpcUa:Server
Client OpcUa:Client
GDS Client OpcUa:Gds:Client
GDS Server OpcUa:Gds:Server
LDS Server OpcUa:Lds
WoT Connectivity Server OpcUa:WotCon:Server
WoT Connectivity Client OpcUa:WotCon:Client
AAS V3 Server OpcUa:Aas:Server
AAS V3 Client OpcUa:Aas:Client

The IConfiguration / IConfigurationSection overloads are AOT-safe on every library. Each dependency-injection-emitting library opts into the .NET 8+ Configuration Binding Source Generator (<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>), which replaces the reflection-based binder with statically-generated C# 12 interceptors. The tests/Opc.Ua.Aot.Tests project verifies that dotnet publish under PublishAot=true produces zero IL2026 / IL3050 warnings from the dependency injection surface.

Server feature

builder.AddServer(o => …) 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.

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<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.

The regular hosted server also registers INodeManagerLifecycle as a singleton forwarding provider. After the server reaches Running, application services can inject it to add, reload, or remove lifecycle-owned NodeManagers. Calls made before startup or after shutdown fail explicitly.

public sealed class RuntimeModelService(INodeManagerLifecycle lifecycle)
{
    public ValueTask<NodeManagerRegistration> AddAsync(CancellationToken ct)
    {
        return lifecycle.AddRuntimeNodeSetAsync(
            new RuntimeNodeSetOptions
            {
                Sources = [RuntimeNodeSetSource.FromFile("Models/Line.NodeSet2.xml")]
            },
            callerContext: null,
            ct);
    }
}

Applications that construct StandardServer directly use server.NodeManagerLifecycle instead. Both paths use the same lifecycle implementation and generation-aware registration handles.

Advanced server services can be supplied through the same fluent builder:

builder.Services
    .AddOpcUa()
    .AddServer<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.

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; adding a token policy alone does not authenticate it. See Identity Providers and Role-Based User Management.
  • Keep certificate auto-accept disabled and provision trust lists. For advanced validation, use the shared ConfigureApplication(...).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.

services.AddOpcUa()
    .ConfigureApplication(application =>
    {
        application.ApplicationName = "MyServer";
        application.AutoAcceptUntrustedCertificates = false;
        application.RejectSHA1SignedCertificates = true;
        application.MinimumCertificateKeySize = 2048;
        application.ConfigureSecurity = security => security
            .SetRejectUnknownRevocationStatus(true);
    })
    .AddServer(server =>
    {
        server.IncludeSignAndEncryptPolicies = true;
        server.IncludeUnsecurePolicyNone = false;
        server.IncludeEccPolicies = true;
        server.ConfigureBuilder = configuration => configuration
            .SetMaxFailedAuthenticationAttempts(5)
            .SetMaxSessionCount(100)
            .SetMaxChannelCount(200)
            .SetAuditingEnabled(true)
            .SetHttpsMutualTls(true);
        server.ConfigureRateLimits = limits =>
        {
            limits.ConnectionsPerSecond = 200;
            limits.ConnectionBurst = 400;
            limits.MaxConcurrentSessionEstablishment = 64;
        };
    });

SetAuditingEnabled(true) enables OPC UA audit-event reporting. SetHttpsMutualTls(true) requests and validates a client certificate when one is supplied on HTTPS endpoints; it does not by itself require every client to present one. Enforce certificate-only Web API access through the corresponding authentication and authorization setup. Server admission rate limiting is enabled by default with conservative connection and concurrent session-establishment limits. Tune it with ConfigureRateLimits, or register a custom IServerRateLimiterProvider; do not disable it without an equivalent upstream control. See Rate Limiting rather than duplicating the full algorithm and deployment guidance here.

First-class server options

In addition to the basic application-identity / endpoint / PKI knobs covered above, OpcUaServerOptions exposes the following first-class properties (bindable from IConfiguration or set via the Action<OpcUaServerOptions> overload). Certificate options added by AddSecurityConfiguration are instead reached through OpcUaApplicationOptions.ConfigureSecurity.

Property Underlying builder call Purpose
IncludeSignAndEncryptPolicies AddSignAndEncryptPolicies() Add the standard sign-and-encrypt policies. On by default.
IncludeUnsecurePolicyNone AddUnsecurePolicyNone() Advertise an unsecured endpoint. Off by default; lab use only.
IncludeEccPolicies AddEccSignAndEncryptPolicies() Add ECC sign-and-encrypt security policies. Off by default.
UserTokenPolicies AddUserTokenPolicy(UserTokenType) List of user-token policies advertised on every endpoint. Defaults to Anonymous when empty.
MaxByteStringLength SetMaxByteStringLength(int) Transport byte-string quota; defaults to 4 MiB.
MaxArrayLength SetMaxArrayLength(int) Transport array-element quota; defaults to 1 Mi elements.
MaxMessageSize SetMaxMessageSize(int) Transport quota in bytes; null keeps the stack default.
OperationTimeoutMs SetOperationTimeout(int) Transport operation timeout in ms; null keeps the stack default.
RejectSHA1Certificates SetRejectSHA1SignedCertificates(bool) Security hardening — defaults to true.
MinCertificateKeySize SetMinimumCertificateKeySize(ushort) Security hardening — defaults to 2048. Set to 0 to keep the stack default.
RegistrationEndpointUrl SetRegistrationEndpoint(EndpointDescription) LDS/GDS endpoint URL the server registers itself with on startup.
ReverseConnect SetReverseConnect(ReverseConnectServerConfiguration) Server-side reverse-connect clients (see below).
OperationLimits SetOperationLimits(OperationLimits) Per-service node limits (max nodes per read/write/browse/...).
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.

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:

{
  "OpcUa": {
    "Server": {
      "ReverseConnect": {
        "ConnectIntervalMs": 15000,
        "ConnectTimeoutMs": 30000,
        "RejectTimeoutMs": 60000,
        "Clients": [
          {
            "EndpointUrl": "opc.tcp://client.example.com:4841",
            "Timeout": 30000,
            "MaxSessionCount": 1,
            "Enabled": true
          }
        ]
      }
    }
  }
}

Equivalent code-only configuration:

services.AddOpcUa().AddServer(o =>
{
    o.ReverseConnect = new ServerReverseConnectOptions();
    o.ReverseConnect.Clients.Add(new ServerReverseConnectClientOptions
    {
        EndpointUrl = "opc.tcp://client.example.com:4841",
        Enabled = true
    });
});

Operation limits

services.AddOpcUa().AddServer(o =>
{
    o.OperationLimits = new OperationLimitsOptions
    {
        MaxNodesPerRead = 1000,
        MaxNodesPerWrite = 1000,
        MaxNodesPerBrowse = 1000,
        MaxMonitoredItemsPerCall = 5000
    };
});

Bindable from OpcUa:Server:OperationLimits. Any value left at zero is treated as "unlimited" by the OPC UA server stack.

User token policies

services.AddOpcUa().AddServer(o =>
{
    o.UserTokenPolicies.Add(new OpcUaUserTokenPolicy
    {
        TokenType = UserTokenType.UserName
    });
    o.UserTokenPolicies.Add(new OpcUaUserTokenPolicy
    {
        TokenType = UserTokenType.Certificate
    });
});

Bindable from OpcUa:Server:UserTokenPolicies. When the list is empty the hosted service falls back to a single Anonymous policy.

Identity (server)

Full identity surface is documented in Identity Providers. This section covers only the dependency injection bindings.

builder.AddServer(...) exposes identity-related extension methods on IOpcUaServerBuilder:

Extension Purpose
ConfigureRoles(Action<RoleConfigurationOptions>) / (IConfiguration) 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:

{
  "OpcUa": {
    "Server": {
      "Identity": {
        "Defaults": {
          "EnableAnonymous": true,
          "EnableUserNamePassword": true,
          "EnableX509": true,
          "EnableJwt": true,
          "ExpectedAudience": "urn:my-server",
          "ClockSkewTolerance": "00:01:00",
          "UserCertificateTrustList": "Users"
        },
        "Issuers": [
          {
            "IssuerUri": "https://login.microsoftonline.com/{tenant}/v2.0",
            "JwksUri":   "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys",
            "Algorithms": [ "RS256" ]
          }
        ]
      }
    }
  }
}

The AddServer(IConfiguration) overload walks this layout and (in addition to binding OpcUaServerOptions.Identity) registers each Issuers[] entry through AddJwtIssuer(...), binds the Roles sub-section into RoleConfigurationOptions, and enables the bridge that creates the four default authenticators from the bound Identity:Defaults flags. The AddServer(Action<OpcUaServerOptions>) code-only overload does NOT auto-wire any of these — call the fluent extensions explicitly when configuring from code.

The IUserDatabase / IUserManagement services needed by the UserNamePassword authenticator must be registered separately — the hosted service skips that authenticator if neither is in dependency injection. See Role-Based Security for the role-mapping layer.

Fluent shortcuts and one-shot presets

The server hosting surface exposes granular extension points and one-shot presets:

services.AddOpcUa()
    .AddSecureServer(options =>
    {
        options.ApplicationName = "PlantServer";
        options.ApplicationUri = "urn:localhost:PlantServer";
        options.ProductUri = "urn:example:PlantServer";
        options.EndpointUrls.Add("opc.tcp://localhost:4840/PlantServer");
    })
    .AddOpcTcpTransport()
    .AddReverseConnect(options =>
    {
        options.Clients.Add(new ServerReverseConnectClientOptions
        {
            EndpointUrl = "opc.tcp://client.example.com:4841"
        });
    })
    .ConfigureOperationLimits(options => options.MaxNodesPerRead = 1000)
    .ConfigureRoles(options => options.Roles.Add(new RoleDefinitionOptions
    {
        Name = BrowseNames.WellKnownRole_Observer,
        Identities =
        {
            new RoleIdentityMappingOptions
            {
                CriteriaType = IdentityCriteriaType.UserName,
                Criteria = "operator"
            }
        }
    }));

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:

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.

Positioning

Use AddPositioningServer() when Positioning owns its node manager. Use AddPositioningFor<TNodeManager>() when an existing companion node manager loads and owns the RSL/GPOS models:

IPositioningServerBuilder positioning = services
    .AddOpcUa()
    .AddServer(options => { /* endpoint and application options */ })
    .AddPositioningServer();

positioning
    .AddGeoLocationProvider<MyGpsProvider>()
    .AddRelativeSpatialLocationProvider<MyRelativeLocationProvider>()
    .ConfigurePositioningFor<PositioningNodeManager>(context =>
    {
        // Build and register RSL/GPOS instances through context.AddressSpace.
        return default;
    });

Client factories compose the generated proxies over the managed session:

services.AddOpcUa()
    .AddClient(options => { /* endpoint and application options */ })
    .AddPositioningClient();

See Relative Spatial Location and Global Positioning.

Robotics

AddRobotics() registers the stock RoboticsNodeManager, the built-in DI/IA/ Robotics model provider, and the ordered Robotics configuration pipeline. It owns the DI namespace, so it cannot be combined with AddOpcUaDi().

services
    .AddOpcUa()
    .AddServer(options => { /* endpoint and application options */ })
    .AddRobotics(options =>
        options.InstanceNamespaceUri = "urn:example:robot-cell")
    .AddRoboticsModel<MyExtraModelProvider>()
    .ConfigureRobotics(async context =>
    {
        await context.AddMotionDeviceSystemAsync("RobotCell", system =>
        {
            // Add controllers, motion devices, axes, power trains, and
            // safety states through the fluent Robotics builders.
        }, context.CancellationToken);
    });

Use ConfigureRobotics<TConfigurator>() for a dependency-injected class-based code-behind implementing IRoboticsConfigurator, and ConfigureRoboticsFor<TNodeManager>(…) when the application already owns a compatible DiNodeManager. Configurators run in registration order and share one IRoboticsBuildContext per manager startup.

See the Robotics developer guide.

Client factories compose the Robotics client over the managed session. AddRoboticsClient() also registers the DI client services because the Robotics client extends DiTopologyClient:

services.AddOpcUa()
    .AddClient(options => { /* endpoint and application options */ })
    .AddRoboticsClient();

Client feature

builder.AddClient(opt => …) registers a lazy ManagedSession factory delegate. The factory caches the connected session — subsequent awaits return the same instance.

using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Client;

services
    .AddOpcUa()
    .AddClient(opt =>
    {
        opt.ApplicationName = "MyClient";
        opt.ApplicationUri = "urn:localhost:MyClient";
        opt.ProductUri = "uri:example.com:MyClient";
        opt.Session = new ManagedSessionOptions
        {
            Endpoint = endpoint,
            ReconnectPolicy = new ReconnectPolicyOptions
            {
                Strategy = BackoffStrategy.Exponential
            }
        };
    });

// Resolve and connect on first use:
var sessionFactory = sp.GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
ManagedSession session = await sessionFactory(ct);

AddClient also registers ITelemetryContext, ISessionFactory (a DefaultSessionFactory configured with the V2 subscription engine), ManagedSessionFactory, IManagedSessionFactory, and the top-level OpcUaClientOptions. The cached delegate is convenient for a single fixed endpoint; resolve IManagedSessionFactory when endpoints are selected at runtime:

var managedSessions = sp.GetRequiredService<IManagedSessionFactory>();
ManagedSession dynamicSession = await managedSessions.ConnectAsync(endpoint, ct);

Misconfiguration is validated through IValidateOptions<OpcUaClientOptions> when the host starts and again before a DI-created session connects. Supply either an explicit Configuration, the application identity fields directly on OpcUaClientOptions (as shown above), or the shared ConfigureApplication(...) options for a combined client/server host (see Shared application configuration). Session.Endpoint is required by the cached fixed-endpoint delegate; IManagedSessionFactory.ConnectAsync(endpoint, ...) supplies it at runtime.

Fluent shortcuts

The returned IOpcUaClientBuilder can compose client features without returning to the root builder:

services.AddOpcUa()
    .AddClient(options =>
    {
        options.Configuration = configuration;
        options.Session = options.Session with { Endpoint = endpoint };
    })
    .AddSubscriptions()
    .AddManagedClientPool()
    .AddWotConClient()
    .AddGdsClient()
    .AddCertificateManagement();

Reverse-connect one-shot sessions can be built through DI:

services.AddOpcUa()
    .AddReverseConnectClient(options =>
    {
        options.Configuration = configuration;
        options.Session = options.Session with { Endpoint = endpoint };
    }, new Uri("urn:server"));

Discovery-and-connect resolves IOpcUaDiscoveryService, selects an endpoint by security policy and mode, then connects through IManagedSessionFactory:

services.AddOpcUa()
    .AddClient(options => options.Configuration = configuration)
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = "opc.tcp://localhost:4840";
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

Client security checklist:

  • Select MessageSecurityMode.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.
  • 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.

Identity (client)

Full identity surface is documented in Identity Providers. This section covers only the dependency injection bindings.

builder.AddClient(...) exposes:

Extension Purpose
AddIdentityProvider<TProvider>() Registers a custom IClientIdentityProvider type. All registered providers are composed into a single resolver by the session builder.
AddIdentityProvider(Action<CompositeClientIdentityProviderBuilder>) Fluent composite-builder with shortcuts: .AddAnonymous(), .AddUserName(...), .AddX509(...), .AddIssuedToken(...).
AddIdentityProvider(IConfiguration section) Binds OpcUaClientIdentityOptions and rolls a CompositeClientIdentityProvider from the bound entries.
AddAccessTokenProvider<T>() / (instance) / (factory) Registers an IAccessTokenProvider keyed by AuthorityUri. Issued-token identity providers select the matching provider at runtime via the URI.

Configuration binding under OpcUa:Client:Identity:

{
  "OpcUa": {
    "Client": {
      "Identity": {
        "EnableAnonymous": true,
        "UserName": {
          "UserName":        "alice",
          "SecretName":      "alice-password",
          "SecretStoreType": "InMemory"
        },
        "X509": {
          "StoreType":   "X509Store",
          "StorePath":   "CurrentUser/My",
          "SubjectName": "CN=Alice"
        },
        "IssuedToken": {
          "ProfileUri":   "http://opcfoundation.org/UA/UserToken#JWT",
          "AuthorityUri": "https://issuer.example"
        },
        "Order": [ "IssuedToken", "UserName", "X509", "Anonymous" ]
      }
    }
  }
}

When Order is non-empty the composite presents providers in that preference; otherwise registration order is preserved. The IssuedToken entry resolves the registered IAccessTokenProvider whose AuthorityUri matches.

How the section is consumed. AddClient(IConfiguration) 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

services.AddOpcUa().AddClient(/* … */).AddComplexTypes();

AddComplexTypes() can be chained directly after AddClient(...). It registers a ComplexTypeSystemFactory (singleton) that can be resolved and used to build a ComplexTypeSystem for a connected session:

var factory = sp.GetRequiredService<ComplexTypeSystemFactory>();
ComplexTypeSystem cts = factory.Create(session);
await cts.LoadAsync(...);

For a managed client that should load complex types automatically after connect, use either ManagedSessionBuilder.WithLoadComplexTypes() or set ManagedSessionOptions.LoadComplexTypes = true. The one-shot AddManagedClient(...) helper registers client services, reconnect defaults, and complex-type loading together:

services.AddOpcUa().AddManagedClient(opt =>
{
    opt.Configuration = applicationConfiguration;
    opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
});

The direct constructor fallback remains available:

var cts = new ComplexTypeSystem(session, telemetry);
await cts.LoadAsync(ct: ct);

Alarms and conditions

services.AddOpcUa().AddClient(/* … */).AddAlarms();

Registers a singleton AlarmClientFactory so dependency-injection-hosted client applications can obtain a Part 9 AlarmClient per connected session without new-ing one manually. Source-generated event records (ConditionTypeRecord, AlarmConditionTypeRecord, DialogConditionTypeRecord, all subtypes including vendor extensions) and the streaming extensions (SubscribeAlarmsAsync, SubscribeConditionsAsync, SubscribeDialogsAsync) require no registration — they compose naturally with the ManagedSession.DefaultStreaming surface that AddClient(...) already wires up.

ManagedSession session = await sessionFactory(ct);
var factory = sp.GetRequiredService<AlarmClientFactory>();
AlarmClient alarms = factory.Create(session);

// Acknowledge an alarm:
await alarms.AcknowledgeAsync(conditionId, eventId,
    new LocalizedText("en", "Acknowledged"), ct);

// Stream typed records via the session's default streaming subscription:
await foreach (ConditionTypeRecord record in session.DefaultStreaming
    .SubscribeAlarmsAsync(ObjectIds.Server, ct: ct))
{
    /* … */
}

The non-dependency-injection path (session.GetAlarmClient() extension and the public AlarmClient constructor) remains available for callers that do not use the dependency injection infrastructure. See Alarms and Conditions for the full developer guide.

Client-side reverse connect

When OpcUaClientOptions.ReverseConnect is set, the dependency injection container registers a singleton ReverseConnectManager that opens the configured listener endpoints on first resolution. Inbound reverse-hello messages are surfaced via ReverseConnectManager.WaitForConnectionAsync(endpointUrl, serverUri, ct), and the values are also mirrored into ApplicationConfiguration.ClientConfiguration.ReverseConnect.

{
  "OpcUa": {
    "Client": {
      "ReverseConnect": {
        "HoldTimeMs": 15000,
        "WaitTimeoutMs": 20000,
        "ClientEndpointUrls": [
          "opc.tcp://0.0.0.0:4841"
        ]
      }
    }
  }
}

Equivalent code-only:

services.AddOpcUa().AddClient(opt =>
{
    opt.Configuration = applicationConfiguration;
    opt.ReverseConnect = new ClientReverseConnectOptions();
    opt.ReverseConnect.ClientEndpointUrls.Add("opc.tcp://0.0.0.0:4841");
});

// Resolve the manager and await an inbound reverse-hello connection:
var reverseConnect = sp.GetRequiredService<ReverseConnectManager>();
ITransportWaitingConnection connection =
    await reverseConnect.WaitForConnectionAsync(endpointUrl, serverUri: null, ct);
// pass `connection` to Session.Create / DefaultSessionFactory.RecreateAsync

Note: the Func<CancellationToken, Task<ManagedSession>> delegate registered by AddClient does not automatically consume the reverse-connect manager for initial connection — it still dials the configured endpoint outbound. Use the ReverseConnectManager directly when the server initiates the session. The ReverseConnect configuration is also consumed by Session / SessionReconnectHandler for reconnect-via-reverse-hello scenarios.

Channel manager

AddClient(...) automatically registers an IClientChannelManager singleton (ClientChannelManager implementation) built from OpcUaClientOptions.Configuration. The Func<CancellationToken, Task<ManagedSession>> delegate wires this manager into every new ManagedSession it creates, so multiple ManagedSession instances resolved from the same DI container will share underlying transport channels per ConfiguredEndpoint. Reconnect is coalesced and notified to attached sessions transparently. On .NET 8 and later, HTTPS transport channels also get the named IHttpClientFactory client (Opc.Ua.Client) and its standard resilience handler through AddClient(...). See Sessions and reconnect § 4 for details on identity, retry policy, HTTPS request resilience, and the shared retry budget between channel-manager reconnect and ManagedSession's outer IReconnectPolicy.

Security trade-off — OPC UA TLS validation vs the DI HttpClient pipeline. When an OPC UA 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.

To override the default channel manager (e.g. to inject a custom IChannelReconnectPolicy):

services.AddOpcUa().AddClient(opt => { ... });
services.Replace(ServiceDescriptor.Singleton<IClientChannelManager>(sp =>
    new ClientChannelManager(
        sp.GetRequiredService<OpcUaClientOptions>().Configuration!,
        sp.GetRequiredService<ITelemetryContext>(),
        reconnectPolicy: new ExponentialBackoffChannelReconnectPolicy
        {
            MaxAttempts = 5
        })));

Application instance (advanced)

services.AddOpcUa().AddApplicationInstance();

Registers an IApplicationInstanceFactory singleton. Hosted server services (regular, GDS, LDS, WotCon) call the factory to create their own per-host IApplicationInstance instead of sharing a process-wide singleton — this avoids configuration overwrite and certificate-lifecycle conflicts when multiple servers coexist in one process. AddServer / AddGdsServer / AddLdsServer register the factory automatically, so calling AddApplicationInstance() explicitly is only needed when you want the factory available outside the hosted-server context.

GDS Client

services.AddOpcUa().AddGdsClient(opt =>
{
    opt.SessionTimeout = TimeSpan.FromMinutes(2);
    opt.MaxConnectAttempts = 10;
});

Requires an ApplicationConfiguration registered in the container. Resolves GlobalDiscoveryServerClient and ServerPushConfigurationClient singletons.

GDS Server

services
    .AddOpcUa()
    .AddGdsServer(o =>
    {
        o.ApplicationName = "MyGds";
        o.ApplicationUri = "urn:localhost:MyOrg:MyGds";
        o.ProductUri = "uri:myorg:mygds";
        o.AutoAcceptUntrustedCertificates = false;
        o.EndpointUrls.Add("opc.tcp://localhost:58810/GlobalDiscoveryServer");
        o.AuthoritiesStorePath = "%LocalApplicationData%/OPC Foundation/pki/CA";
    })
    .AddApplicationsDatabase<MyApplicationsDatabase>()
    .AddCertificateGroup<MyCertificateGroup>()
    .AddCertificateRequest<MyCertificateRequest>()
    .AddUserDatabase<MyUserDatabase>();
    .AddInMemoryStores()
    // Optionally:
    // .AddAuthorizationService<MyTokenIssuer>(o => o.IssuerUri = "urn:my-gds")
    // .AddAccessTokenProvider<MyAccessTokenProvider>()
    // .AddKeyCredentialRequestStore<MyKeyCredentialRequestStore>()
    // .AddConfigurationDataStore<MyConfigurationDataStore>();

Throws on a second .AddGdsServer(...). Auto-registers an internal GdsHostedServer : GlobalDiscoverySampleServer and its ApplicationsNodeManager. The pluggable services (IApplicationsDatabase, ICertificateGroup, ICertificateRequest, etc.) are resolved from the container at startup.

Identity (GDS server)

IGdsServerBuilder forwards every identity-related extension to the underlying server builder so a GDS host configures identity the same way as a regular server:

services
    .AddOpcUa()
    .AddGdsServer(opt => opt.ApplicationName = "MyGds")
    .AddDefaultIdentityAuthenticators(opt =>
    {
        opt.EnableAnonymous = false;
        opt.EnableUserNamePassword = true;
    })
    .AddJwtIssuer(opt =>
    {
        opt.IssuerUri = "https://login.microsoftonline.com/{tenant}/v2.0";
        opt.JwksUri   = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys";
    });

Action<> and IConfiguration overloads are available for 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 for the full reference.

LDS Server

services
    .AddOpcUa()
    .AddLdsServer(o =>
    {
        o.ApplicationName = "MyLds";
        o.ApplicationUri = "urn:localhost:MyOrg:MyLds";
        o.ProductUri = "uri:myorg:mylds";
        o.EndpointUrls.Add("opc.tcp://localhost:4840/UADiscovery");
        o.EnableMulticast = true;            // LDS-ME multicast advertisement
        o.MulticastLoopbackOnly = false;     // set true for in-process tests
        o.ServerCapabilities.Add("DA");      // extra capabilities (LDS / LDS-ME are always implicit)
    });

Throws on a second .AddLdsServer(...). The server is advertised as ApplicationType.DiscoveryServer (the LDS class promotes the type after ApplicationConfiguration.CreateAsync).

MulticastLoopbackOnly restricts the mDNS announcer to the loopback NIC — intended for in-process tests where LDS-ME traffic must stay local. ServerCapabilities is additive — LDS is always included, plus LDS-ME when EnableMulticast is true.

WoT Connectivity Server

The WoT Connectivity server lives inside a regular StandardServer, so AddWotConServer must be combined with AddServer:

services
    .AddOpcUa()
    .AddServer(o => /* regular server options */)
    .AddNodeManager<MyNodeManagerFactory>()  // your domain node managers
    .Services
    .AddOpcUa()                              // returns the same builder
    .AddWotConServer(o =>
    {
        o.AssetNamespaceUri = "http://myorg/UA/WoT-Con/Assets/";
        o.ThingDescriptionStorageFolder = "tds";
    })
    .AddAssetProvider<MyAssetProviderFactory>()
    .AddDiscoveryProvider<MyDiscoveryProvider>();

AddWotConServer registers the WoT WotConnectivityNodeManagerFactory as an OpcUaServerNodeManagerRegistration so it attaches to the regular server feature. IWotAssetProviderFactory and IWotAssetDiscoveryProvider services registered in dependency injection are picked up automatically. If AddWotConServer is registered without a preceding AddServer, startup fails with a clear configuration error instead of silently dropping the node manager registration.

WoT Connectivity Client

services.AddOpcUa().AddClient(/* … */).AddWotConClient();

// Resolve and connect on first use:
var wotClient = sp.GetRequiredService<Func<CancellationToken, Task<WotConnectivityClient>>>();
WotConnectivityClient client = await wotClient(ct);

The WoT client reuses the connected ManagedSession registered by AddClient(...).

Asset Administration Shell V3

The AAS metamodel server is hosted inside the regular OPC UA server. Register custom providers first; AddAasV3Server uses TryAddSingleton defaults for the document-backed value provider and operation handler.

services.AddSingleton<IAasValueProvider, MyAasValueProvider>();

services
    .AddOpcUa()
    .AddServer(options => { /* endpoint and application options */ })
    .Services
    .AddOpcUa()
    .AddAasV3Server(options =>
    {
        options.EnvironmentFolder = "aas-environments";
        options.RetirementPolicy = AasProjectionRetirementPolicy.Graceful;
    })
    .AddOperationHandler<MyAasOperationHandler>();

AddAasV3Client registers a metamodel client factory over the connected ManagedSession; AddAasV3RegistryClient registers the registry client factory that resolves Server/AASRegistry on first use:

services
    .AddOpcUa()
    .AddClient(options => { /* endpoint and application options */ })
    .AddAasV3Client(options =>
    {
        options.InstanceNamespaceUri = "urn:example:aas:instances";
    })
    .AddAasV3RegistryClient();

See OPC UA for Asset Administration Shell V3.

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)
builder.Services
    .AddOpcUa()
    .AddServer(o => /* regular server on 51210 */)
    .AddNodeManager<MyNodeManagerFactory>()
    .Services
    .AddOpcUa()
    .AddLdsServer(o => /* LDS on 4840 */)
    .Services
    .AddOpcUa()
    .AddGdsServer(o => /* GDS on 58810 */)
    .AddApplicationsDatabase<MyApplicationsDatabase>()
    .AddCertificateGroup<MyCertificateGroup>()
    .AddCertificateRequest<MyCertificateRequest>()
    .AddUserDatabase<MyUserDatabase>();

Node managers registered under a feature wrapper (e.g. OpcUaServerNodeManagerRegistration for the regular server) are isolated: the GDS / LDS hosted services do not see them.

Native AOT

Every .AddXxx(...) overload — both the Action<TOptions> shape and the IConfiguration / IConfigurationSection shapes — is AOT-safe. The .NET 8+ Configuration Binding Source Generator is enabled on every library that performs configuration binding (<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>), so the reflection-based binder is replaced by statically-generated C# 12 interceptors at compile time.

services.AddOpcUa()
    .AddServer(builder.Configuration.GetSection("OpcUa:Server"))  // bind from appsettings.json — AOT-safe
    .AddNodeManager<MyAotNodeManagerFactory>();

Action<TOptions> (code-only) and IConfiguration overloads can be mixed freely; consumers no longer need to choose between them for AOT compatibility.

Notes:

  • The source generator targets net8.0+. On older TFMs (net48 / 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:

services.AddOpcUa()
    .AddServer(o =>
    {
        o.ApplicationName = "AotServer";
        o.ApplicationUri = "urn:host:AotServer";
        o.EndpointUrls.Add("opc.tcp://localhost:51210/AotServer");
        o.AutoAcceptUntrustedCertificates = false;
    })
    .AddNodeManager<MyAotNodeManagerFactory>();

Telemetry

AddOpcUa() registers an ITelemetryContext that resolves the host's ILoggerFactory on first use. To override:

services.AddSingleton<ITelemetryContext>(myCustomTelemetry);
services.AddOpcUa();   // TryAddSingleton — custom one wins

To configure logging fluently:

services.AddOpcUa()
    .AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information))
    .AddServer(/* … */);

See also