Files
UA-.NETStandard/docs/DependencyInjection.md
Marc Schier fa8d45f2cf Fix server/core validation, lifecycle and protocol regressions (#4477)
# Description

**Server/core audit fixes with regression tests. Latest review-fix
validation, merge validation and earlier acceptance results are
distinguished below.**

This addresses a 95-finding server/core report. Every finding was
reviewed with
the OPC UA specification knowledge base or, where the standard does not
prescribe a C# implementation, the corresponding ownership/configuration
contract. Incorrect or withdrawn subclaims remain qualified rather than
being
presented as additional fixes.

The original checkpoint covers session identity/certificate validation
and
revocation; certificate/key ownership and transactional updates; TCP
reconnect,
receive/admission and buffer lifetime; HTTP quotas; subscription
admission,
durability and pooled payloads; file-operation lifecycle; historian
aggregates;
fluent/state-machine behavior; and related utilities. It includes
targeted
regressions and directly related documentation.

The approved acceptance cleanup is separate in the commit history:

| Cleanup | Verified result |
| --- | --- |
| Invalid identity-policy and KeyCredential fixture inputs | Fixtures
explicitly advertise Username and use its generated policy ID.
Credential persistence uses valid clear mode inside the existing
encrypted admin context. Original permission, lockout and negative
encryption assertions remain. |
| Native GDS lifetime and concurrent connection failures | Retain
ApplicationInstance until client teardown. A deterministic native
reproducer proved the test-only channel cap of 10 was exhausted;
removing that override restores the existing bounded default of 1000. No
global serialization or production quota change. |
| Stress certificate ownership and close accounting | Balance two helper
references and the fake transport's server-certificate ownership.
Replace the invalid event-count bound with exactly-once physical
disposal, opens = closes, zero active entries and one logical entry
lifetime. |
| Participant deadlines | Use the injected TimeProvider with the
existing cross-framework WaitAsync overload. Tests pin one tick before,
exact expiry, terminal notification, recovery and timely completion. |
| Reproducibility and platform assertions | Preserve canonical WoT
bytes/hashes, explicit display-culture contracts and exact
numeric-vector assertions. Accept Apple's CryptographicException subtype
without weakening disposal balance. |
| Missing user-signature proof | Reject null/empty proofs before OS
crypto. Return BadUserSignatureInvalid per Part 4, 5.7.3.3. None-policy
and valid-signature controls remain. A CI-only test nullable-context
error was corrected without changing production behavior. |
| Durable sample restore and shared test storage | Dequeue waits for
actual batch residency instead of treating failed restore completion as
success, and restored files close before deletion. Five deterministic
regressions failed before the fix. The two fixtures that clear the
shared batch directory are isolated; all original 3,000-item assertions
remain unchanged. |

## Latest review follow-up

Commit `828856850219518970e8e2f580b8f94b3c1fc748` fixes the deferred
robotics
disposal reported in the latest review.
`RobotIntentNodeManager.DisposeAsync`
now joins a shared controller-host drain and then explicitly awaits base
node-manager disposal. A controller's bounded shutdown wait may defer
its
cleanup, but no longer lets the node-manager override finish early.
Concurrent
callers, including an asynchronous call after synchronous `Dispose`,
cannot
bypass the drain after the host list has been detached.

Synchronous disposal remains nonblocking for deferred work. Deferred
initialization failures are logged and retained, and base cleanup
failures
propagate to asynchronous callers. The existing controller-level bounded
disposal contract is unchanged; the node-manager contract is documented
in
`docs/Robotics.md`.

| Regression requirement | Evidence |
| --- | --- |
| Wait for deferred hosts; preserve synchronous compatibility and
concurrent joins | `NodeManagerAsyncDisposalWaitsForDeferredHostsAsync`:
four cases covering both entry-point orders and zero/positive host
shutdown waits. All four failed before the fix. |
| Wait through base cleanup and observe failures |
`NodeManagerDisposalWaitsForBaseCleanupAndReportsItsFailureAsync`: four
cases hold the actual base-resource semaphore, verify pending callers
after the host finishes, then check success or the same propagated
exception and exactly-once cleanup. |

The focused `IntentHostingCoverageTests` and
`IntentControllerHostTests.Dispose`
selection passed **30/30 on net10.0 and 30/30 on net48**, with no
failures or
skips. The final net10 solution build passed with zero errors and only
the four
previously documented CA2249 warnings in unchanged fuzz-test code. No
new
warnings, analyzer suppressions, dependencies, or weakened assertions
were added.
The review thread received a commit-specific reply and was explicitly
resolved.

This verification is scoped to the robotics feedback; the earlier full
Subscriptions-run limitation remains recorded in the merge section
below.


## Documentation review follow-up (2026-09-14)

Commit `f9a9686fcc5d3cfd19cbbc8c2d826e20f5c7e72f` addresses the six
follow-up
threads on documentation and implementation rationale. Each has an
in-thread
reply citing the commit and an explicit resolution.

Multiline XML documentation now covers the declarations introduced or
modified
throughout the entire PR, including internal contracts, samples, tests,
and
public members of related private helpers. A syntax-aware inventory
checked
**2,031 declarations across all 256 PR C# paths with zero documentation
gaps**.
A supplementary check also covers **75 previously unchanged public
members**
of private helpers involved in the changed code. Token comparison across
the
conditional-compilation branches proves this follow-up changes **no
executable
C# or test assertions**. Project-file changes are rationale comments
only.

The implementation questions are documented at their source:

| Question | Resolution |
| --- | --- |
| Certificate assembly friendship | Server Push configuration reuses the
internal explicit-subject CSR helper with the existing key; no public
crypto API was added. |
| Core.Types assembly friendship | Server builders use internal
transition callbacks/revision guards; Server.Tests checks the revision
deterministically without creating a public compatibility contract. |
| Always-single-reader notifications | One dequeuer preserves event
order; optional concurrency fans out within one event and joins before
the next event. |
| Asynchronous disposal completion | `DisposeAsync` already joins the
completion published after admitted operations and all cleanup attempts.
The compatibility `Dispose` remains nonblocking to avoid deadlocking an
admitted callback against its own drain. |

The requested detailed review disposition follows. It distinguishes
actual
fixes from confirmed policies and incorrect subclaims rather than
counting every
comment as a new runtime change.


### Detailed disposition of review 5191795284

The [SDK
review](https://github.com/OPCFoundation/UA-.NETStandard/pull/4477#pullrequestreview-5191795284)
was submitted against `1c4a9df6c`, before the subsequent merge and
feedback fixes.
Its **15 inline threads** have each received an action-specific reply
and an
explicit resolution. A fresh GraphQL query verified those replies and
resolution
states; the fixes in `b8a59990f` remain ancestors of the current branch.

| Review concern | Action and disposition | Concrete regression evidence
|
| --- | --- | --- |
| Injected certificate-store providers bypassed | **Fixed.**
Registration, configuration snapshots, and validation use the injected
providers. Explicit custom types remain authoritative; the built-in
Directory catch-all is deferred so native X509 paths and later custom
providers are not captured. Failed store opens dispose the created
store. | `InjectedProviderResolvesRegisteredAndConfiguredTrustStores`
covers default-provider ordering and both registration paths; provider
validation/open-failure cases and the unchanged
`TrustedCertificateInjectedViaIndependentX509StoreIsObservedAsync` cover
real use and independent-store visibility. |
| Unguarded timer-thread leadership callbacks | **Fixed.** Each
subscriber's exception is logged independently and cannot suppress
subsequent subscribers or fail lease operations. `IsLeader` reads the
deadline without invoking application callbacks. |
`ThrowingLeadershipSubscriberDoesNotFailAcquireOrDisposeAsync`,
`ExpiryNotifiesOtherSubscribersWhenOneThrowsAsync`, and
`IsLeaderDoesNotDispatchExpiryNotificationsAsync` were all demonstrated
failing before the fix. |
| Legacy pending-key provider accepts an unfinishable regenerated CSR |
**Fixed by explicit capability validation.** A provider implementing
only `IPendingCertificateKeyStore` receives `BadNotSupported` before key
generation or persistence when regeneration is requested. Existing-key
CSR/update remains supported. Regeneration requires matching-only
consumption; no unsafe destructive probing was introduced. |
`LegacyPendingProviderRejectsRegenerationBeforeCreatingOrSavingAKeyAsync`
checks no key or store mutation;
`LegacyPendingProviderCompletesExistingKeySigningAndUpdateAsync` proves
the supported flow completes. |
| Shutdown waits behind user deactivation | **Fixed.** Accepted
deactivations drain before the master's lifecycle gate is taken.
Configuration retains its binding until the shared drain completes,
including concurrent shutdown callers and direct master disposal. | Four
cases of `ShutdownDrainsDeactivationOutsideTheSessionClosingGateAsync`
exercise real configuration/binding/ServerInternalData/master reentry,
retained address space, exact session/subscription closure, and no
production cancellation shortcut. |
| Subclass cleanup runs under the operation-lifetime lock | **Fixed.**
Cleanup starts outside that lock. A pre-published completion joins all
disposers, and only the active asynchronous teardown context may use
guarded helpers. Captured contexts lose that privilege when the callback
exits. |
`SynchronousOwnedCleanupLetsSamplingWorkerReachClosedAdmissionAsync`,
`DisposeCoreCanUseGuardedMembersWithoutReopeningCallerAdmissionAsync`,
captured-context rejection, and exact cleanup-failure/ownership
assertions cover both synchronous and suspended cleanup. |
| Discovery requestId-zero chunk read after pool return | **Fixed by the
earlier merge.** Discovery validation precedes `SaveIntermediateChunk`;
no read occurs after that path can return the buffer. The ordering is
retained. | `DiscoveryChannelReadsTheFirstChunkBeforeItIsReturnedAsync`
uses a poisoning pool and the requestId-zero path. |
| T1.8 OPN cleanup misses post-decryption throws | **Fixed by the
earlier merge and pinned with new regressions.** Both sequence rejection
and certificate mismatch release the decrypted body and input ownership.
| `OpenSecureChannelRejectionAfterDecryptionReturnsEveryRentalAsync`
checks the exact audit status and pool conservation. Omitting either
corresponding release in an isolated snapshot made its case fail; no
mutation remains. |
| T1.2 mount-root protection depends on the provider | **Fixed at the
host boundary.** Separator-only aliases resolve to the protected root.
Traversal, empty/whitespace segments, backslashes, and drive/stream
qualifiers are rejected before arbitrary providers are invoked. Valid
nested paths and root destinations remain supported. | A deliberately
permissive provider is used by
`NonCanonicalObjectPathsCannotDeleteMoveOrCopyAsync` and invalid-target
cases; exact zero-call checks are paired with legitimate
nested-path/root-destination controls. |
| Inline trusted/issuer entries now face CRL checks | **Intentional
strict behavior, documented and tested.** A configured backing store
with strict unknown-status rejection requires current revocation
information. Explicit-only lists without a store retain their no-store
behavior. Actual revocation cannot be suppressed; a matching store
entry's policy takes precedence. |
`InlineIssuerWithStoreHonorsStrictUnknownRevocationPolicyAsync`,
`InlineIssuerRevocationIsNeverSuppressedAsync`, store-precedence cases,
and explicit-only controls preserve the missing-information versus
actual-revocation distinction in Part 4 6.1.3. |
| RSA minimum applied to unspecified ECC identifiers | **Fixed at the
application-certificate check.** The RSA minimum is compared only with
an actual RSA public key. An unspecified ECC identity is not replaced
simply because its key size is below an RSA threshold. |
`SilentCertificateCheckPreservesUnspecifiedEccIdentityAsync` checks DER,
thumbprint, store contents, and private-key retention; explicit RSA and
HTTPS minimum controls remain. |
| Failed directory refresh wedges corrective mutations | **Fixed.**
Recoverable automatic pre-refresh failures are logged and leave
reconciliation pending, but do not block corrective provider mutations.
Explicit refresh errors, capacity admission, caller cancellation, and
actual provider denials remain enforced. |
`CorrectiveDeleteRecoversAfterPersistentOverLimitRefreshFailureAsync`
converges an externally over-limit directory back to its exact allowed
contents. Capacity, cancellation/token, and six refresh-error controls
check negative paths. |
| Timed-transition rejection logs at 10 Hz | **Fixed.** Repeated
`(state, revision, status)` warnings are deduplicated without changing
the retry cadence. A different status or state entry permits another
warning. | `RepeatedTimedRejectionLogsOnceAndRecoversAsync` checks one
warning over eleven rejections and success on the next attempt.
Status-change and same/different-state reentry controls pin reset
behavior. |
| Bad unsubscribe strands retired-generation bookkeeping | **Fixed.**
Retired all-event bookkeeping completes even when unsubscribe reports
Bad; the first Bad is still returned to the caller. | Good and
BadCommunicationError cases of
`RetiredAllEventsDeletionCompletesBookkeepingAndReleasesGenerationAsync`
check event/snapshot removal, generation release, and exactly one
unsubscribe without finalization replay. |
| CSR subject parsing and fallback | **Fixed for existing and
regenerated keys.** Omitted subjects fall back to the actual stored
certificate. Regeneration resolves the public certificate's subject
before creating a key. Malformed or unresolvable subjects return
`BadInvalidArgument` before pending-key mutation. | Store-only fallback,
real-generator null/empty cases, and missing-subject/malformed-DN
controls verify the signed CSR, matching fresh pending key, unchanged
active identity, and no destructive side effects. |
| Committed RemoveUser change reported as failed | **Fixed for
RemoveUser and ModifyUser.** Accepted cleanup is still awaited and close
failures remain logged, but cannot replace the committed mutation
result. Precommit failures retain their original result and side
effects. |
`CommittedUserChangeKeepsResultAndPublishedUsersWhenCloseFailsAsync`
checks published Users, Good result, logging, and continued closes.
`FailedUserChangeKeepsPublishedUsersAndDoesNotCloseSessionsAsync` checks
BadNotFound with no publication or close. |
| Alleged pooled decoded-request aliasing | **No runtime redesign
needed; reviewer rebuttal confirmed.** The decoder copies the relevant
input data. Misleading ownership comments were corrected rather than
introducing a new dispatch model. |
`DecodedRequestRetainsValuesAfterInputBuffersAreReturnedAsync` checks
the exact decoded NodeId and ByteString after returned arrays are
poisoned. |

The current documentation-only follow-up rechecked **104 Core, 473
Server, and
4 Configuration cases** covering these concerns: **581 passed, zero
failed or
skipped**. The final net10 solution build also passed with **zero
warnings and
errors**. The complete earlier Core/Server/Configuration and net48
results remain
listed separately rather than being presented as rerun current-head
acceptance.

## Earlier review follow-up

Commit `b8a59990fe2e8e967a12ffd5d9158beed7831143` addresses all 19
review
threads. Each thread has an in-thread reply naming this commit and was
explicitly
resolved after verification; the final paginated query found 19 resolved
and
zero unresolved threads.

The follow-up fixes injected certificate-store selection (including
default
provider ordering and native X509 paths), actual-RSA minimum
enforcement, and
CSR capability/subject validation before key mutation. Node-manager
teardown
uses simpler scoped ownership and drains accepted user revocation
outside the
master gate; committed user results and retired-event cleanup remain
correct
when cleanup reports failure. File mutations can recover after failed
refresh
without bypassing capacity or cancellation, provider paths protect mount
roots,
and repeated timed rejections no longer flood logs. Leadership
subscriber
failures are isolated without callbacks from `IsLeader`.

Regressions also pin the already-merged OPN/discovery buffer fixes and
prove
decoded request values do not borrow returned input buffers. The strict
inline
issuer revocation policy is documented and tested, not weakened.
Existing tests
were changed only for the reviewed behavior: the incorrect
committed-user result
was corrected, and inline-issuer/root-alias controls were expanded.

### Review-fix validation

| Suite | Scope | Framework | Passed | Existing TRX NotExecuted |
| --- | --- | --- | ---: | ---: |
| Core | Complete project | net10.0 | 4,970 | 88 |
| Server | Complete project | net10.0 | 6,414 | 69 |
| Configuration | Complete project | net10.0 | 231 | 0 |
| Core | Certificate, lease and buffer compatibility slice | net48 | 153
| 0 |
| Server | Reviewed lifecycle, user, file, timer and CSR compatibility
slice | net48 | 692 | 0 |
| Configuration | Complete project | net48 | 231 | 0 |
| CSR provider regressions | Final portable-mock follow-up | net10.0 |
11 | 0 |

All runs exited successfully with normal concurrency and leak detection.
The Server console reports 5 skips; its TRX additionally records
existing
assumption/explicit cases, hence the 69-record count above. The
Framework build
caught two modern-only `ValueTask.CompletedTask` usages in new mock
setup; only
those expressions changed to the equivalent portable
`default(ValueTask)`.
The complete modern suites precede that test-only correction; all
production
files remained byte-identical, and the corrected fixture passed on both
targets.

The final net10 solution build succeeded with 0 errors and the same 4
CA2249
warnings in unchanged fuzz-test code. Diff checks and changed-line
whitespace
checks are clean; whole-file formatting still identifies 10 unchanged
baseline
lines, which were not reformatted outside the feedback scope. The
committed
35-file tree was hash-verified against the validation inputs.

This is the review-feedback verification scope, not a rerun or sign-off
of the
historical complete platform/native/soak acceptance campaign below.


## Latest upstream merge

Merge `9d9bf679f317164c5b2b90924cd3cdc27a86d451` integrates `master`
through `162fc5ebb` (#4486),
including the AddNodes child-name index, shared certificate-error
reporting,
CTT sample-data corrections, and the accompanying regressions and
documentation.

The CreateSession conflict preserves this branch's certificate
ownership,
required certificate, application-URI, and channel-certificate checks.
The
validation result now retains nested errors through `ThrowIfInvalid`,
allowing
the upstream shared reporting helper to mask trust, policy, and
revocation
failures consistently without losing audit detail.

The new indexed AddNodes regression initially attempted to reuse a
deterministic
NodeId after renaming its existing node. Renaming frees the BrowseName,
not that
NodeId. `IndexedBrowseNameReusePreservesExistingNodeIdentityAsync` now
proves
both contracts: the derived-ID collision leaves the original node
intact, then
a fresh explicitly requested ID can reuse the old name while the new
name
remains protected against duplicates. No production identity rule was
relaxed.
The incoming seeded random-index test uses the existing `UnsecureRandom`
wrapper
with the same seed and sequence; no new analyzer suppression was added.

| Local merge validation | Framework | Passed | Existing exclusions |
| --- | --- | ---: | ---: |
| Core, complete project | net10.0 | 5,081 | 88 |
| NodeState / BaseInstanceState | net10.0 | 579 | 0 |
| Server node management, certificate and session-security selection |
net10.0 | 112 | 0 |
| InformationModel node-management services | net10.0 | 29 | 1 |
| NodeState / BaseInstanceState | net48 | 578 | 0 |
| Core certificate validation, error reporting and TCP buffers | net48 |
111 | 0 |
| Server node management, certificate and session-security selection |
net48 | 112 | 0 |

All selected runs exited successfully at normal concurrency: **6,602
passing
test executions**. Modified test assemblies were explicitly rebuilt, and
the
combined name/identity regression was verified by name in both net10 and
net48
results. No skips or weakened assertions were introduced.

The final `dotnet build UA.slnx -c Release --no-restore -m:1` with
`CustomTestTarget=net10.0` succeeded with **zero warnings and zero
errors**.
The resolved source and adjusted tests pass whitespace verification, and
their
source hashes match the validation inputs.

This is merge-specific verification. Earlier review results and the
previously
reported incomplete full Subscriptions runs below remain scoped to their
own
commits; they were not reopened or relabeled as passing by this
operation.

## Previous upstream merge (2026-09-15 morning)

Merge `c4694e20ab089c555af233642042133ff3a03ac8` integrates `master`
through `484d6c887`, retaining
#4481's shared Like parser/GDS query fixes, #4488's RequestHandle
recovery in
ServiceFaults, and #4487's expired-session and durable-transfer
corrections.

Three conflict resolutions preserve both sides:

- Alias searches now use the shared `LikePattern` implementation,
including
upstream malformed-list validation. A timeout overload retains this
branch's
100 ms per-alias deadline and `BadTimeout` reporting without keeping a
second
wildcard translator. The shared matcher's original unlimited overload
remains
unchanged for existing GDS/ContentFilter callers. New regressions cover
bounded prefix/list scans, reuse after timeout, and the alias service
result.
- Expired activation closes outside the session-manager gate using
upstream's
once-only timeout claim, audit, and diagnostic accounting. This branch's
  original-channel and security-policy checks remain in place.
- TCP buffer tests retain this branch's ownership, poisoning-pool and
post-decryption checks alongside the upstream RequestHandle/fault
overload
  tests; neither set was discarded.

The upstream HTTPS binary behavior is intentionally retained: a decoder
`ServiceResultException` produces HTTP 200 with an OPC UA ServiceFault
carrying
the decoder status, response timestamp, and recoverable RequestHandle,
rather
than the previous HTTP 500 text response. Other exceptions retain their
existing HTTP error handling.

| Local validation | Framework | Passed | TRX NotExecuted |
| --- | --- | ---: | ---: |
| Core, complete project | net10.0 | 5,054 | 88 |
| Sessions, complete project | net10.0 | 780 | 610 |
| Server aliases, expiry/security and subscriptions | net10.0 | 288 | 0
|
| GDS database, paging and live directory calls | net10.0 | 263 | 0 |
| Shared Like patterns and bounded evaluation | net10.0 | 109 | 0 |
| Subscriptions transfer/durability/service selection | net10.0 | 23 | 1
|
| HTTPS WebApi, complete project | net10.0 | 346 | 0 |
| InformationModel aliases | net10.0 | 54 | 5 |
| Shared Like patterns and bounded evaluation | net48 | 109 | 0 |
| Core fault handles, TCP/HTTPS and filters | net48 | 268 | 0 |
| Server aliases, expiry/security and subscriptions | net48 | 288 | 0 |

These selected runs exited successfully with normal test concurrency:
**7,582 passing test executions**. Sessions' console reports 379 skips;
the
TRX also contains conditional/assumption exclusions, so the table uses
the
actual 610 NotExecuted records. No skips or weakened assertions were
added.
The new `EvaluationTimeoutBoundsWorkAndPreservesPatternReuse` and
`SharedPatternTimeoutRemainsAnAliasServiceError` tests passed on both
targets.

**Validation limitation:** two unfiltered Subscriptions runs did not
produce
a final result and were stopped, including a retry with a hang guard.
They
are not counted as passing. The isolated failover fixture passed 2/2,
and the
transfer/durability/service selection above passed. The stall's cause
was not
established; no unrelated code or test was changed to conceal it.

The final `dotnet build UA.slnx -c Release --no-restore -m:1` with
`CustomTestTarget=net10.0` succeeded with **0 warnings and 0 errors**.
Whitespace verification passed for all hand-reconciled files, and staged
source hashes match the validation inputs.

These are merge-specific checks, not a new full platform/native/soak
acceptance
sign-off. Earlier review and acceptance evidence below remains dated to
its
own commit.

## Previous upstream merge (2026-09-14)

Merge `c3e09553e4d1509502d4ba751960d58491bea2cf` integrates `master`
through `6313e38a0` (#4478),
including the CTT-driven Part 13 aggregate corrections and FindAlias
argument
validation. The upstream tests and CTT documentation are retained.

The single conflict in `AliasNameWildcardMatcher` is reconciled through
the
branch's shared regex compiler: upstream malformed-pattern rejection and
`IsValidPattern` coexist with OPC UA character escaping,
whole-string/newline
matching, the 100 ms timeout, and service-level error reporting. The
upstream
ReferenceTypeFilter checks and chronological aggregate behavior are
preserved.

Local merge verification:

| Suite / selection | Framework | Passed | Existing skips |
| --- | --- | ---: | ---: |
| Server aliases, aggregates and historian | net10.0 | 1,011 | 0 |
| Server aliases, aggregates and historian | net48 | 1,011 | 0 |
| InformationModel aliases | net10.0 | 54 | 5 |
| Features aliases | net10.0 | 11 | 0 |
| Client aliases | net10.0 | 28 | 0 |
| History aggregate/historical-access consumers | net10.0 | 265 | 0 |

All selected tests passed at normal concurrency. The final net10
solution
build passed with 0 errors and 0 warnings.
The resolved file's whitespace check and the staged diff check passed.
These
are merge-specific checks, not a new full platform/native/soak
acceptance claim.

## Previous upstream merges

Integrates `master` through `8e874d073`, including the Client, Types,
Core and
source-generator audit fixes in #4471, #4473, #4474 and #4475, plus the
event queue-size revision from #4480. The merge commits are `150ea0439`
(26 conflict resolutions) and `a0e17c07c` (the base update that arrived
during
validation).

The overlapping fixes are reconciled rather than replacing either
branch:

- **Rejected-certificate retention follows this branch, as explicitly
requested:**
zero means unlimited history; negative values disable new storage while
preserving existing backend trimming behavior (Directory clears prior
history;
shared key/value leaves it intact). The processor and manager policy are
not
  redefined, and documentation/tests describe the distinction.
- Preserve owned certificate snapshots and borrowed validation-core
lifetimes
  alongside upstream cache invalidation, store-first CRL checks and
  `BadCertificateRevocationUnknown` for unsupported revocation lookup.
- Combine exactly-once chunk ownership, receive-loop cancellation,
stale-epoch
rejection and detach-before-adoption with upstream
signature-before-padding
  verification and secret cleanup.
- Preserve pooled `HttpClient` per-request configuration together with
bounded
response reads and deadlines that remain active through body
consumption.
Regression cases exercise already-used clients and both timeout
precedence cases.
- Retain the binary-compatible channel-key constructor and normalize
omitted
profiles; preserve exact profile filtering and upstream binary-first
HTTPS/WSS
  endpoint ordering.
- Preserve monotonic lease expiry and stale-confirmation fencing while
adding
ordered, non-reentrant leadership notifications outside the state lock.

## Validation

Local validation of the conflict-resolution merge:

| Suite | Framework | Passed | Existing skips |
| --- | --- | ---: | ---: |
| Core | net10.0 | 4,940 | 88 |
| Client | net10.0 | 2,278 | 0 |
| Server | net10.0 | 6,088 | 5 |
| Types | net10.0 | 10,394 | 1 |
| SourceGeneration.Core | net10.0 | 3,940 | 8 |
| SourceGeneration | net10.0 | 185 | 0 |
| Core | net48 | 4,855 | 104 |

After the subsequent four-file #4480 update, the related Server slice
passed
811 tests with 5 existing skips, the real durable queue-size cases
passed 6/6,
and EventManager's 21 cases passed again after analyzer-only
expected-array
cleanup. The final `dotnet build UA.slnx -c Release` with
`CustomTestTarget=net10.0`
succeeded: 0 errors and 4 pre-existing CA2249 warnings in unchanged
fuzz-test
code. Whitespace verification passed for the resolved files and the
follow-up.

Merge-specific proof includes
`RequestTimeoutRemainsActiveWhileReadingTheOpenApiBodyAsync` on
already-used
shared clients with either the request or HttpClient timeout shorter,
the
existing ownership/expiry regressions from both branches, and both
branches'
retention tests adjusted only to the explicitly chosen zero/negative
contract.

Earlier acceptance at `1c4a9df6c` passed hosted Linux/macOS CI, CodeQL,
image
builds and the canonical coverage gate (87.15% line, 77.27% branch,
92.14% patch).
Both Framework project matrices, all three Windows native suites (183
cases),
and the unfiltered net10 stress project (230 cases, including full
60-minute
chaos and 30-minute memory soaks) also passed. Those are **pre-merge
results**,
not a claim that the full framework/platform/native/soak matrix has been
rerun
on this merge. The previous acceptance workflow remains separate from
this
review-feedback operation.

Security checks, meaningful assertions, leak detection, default native
concurrency, full soak durations and coverage thresholds remain intact.
The user-set ready-for-review state is unchanged. No architectural or
complete
merged-head acceptance sign-off is claimed.

## Related Issues

- Fixes #4476

## 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**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-09-16 04:42:09 +00:00

81 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)
  • The GDS server (src/Opc.Ua.Gds.Server)
  • The LDS server (src/Opc.Ua.Lds.Server)
  • The WoT Connectivity server (src/Opc.Ua.WotCon.Server)
  • The WoT Connectivity client (src/Opc.Ua.WotCon.Client)
  • The PubSub stack (src/Opc.Ua.PubSub, src/Opc.Ua.PubSub.Udp, src/Opc.Ua.PubSub.Mqtt, src/Opc.Ua.PubSub.Server) — see PubSub.md 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 builder.AddGdsClient(opt => …) IGdsClientBuilder — OpcUa:Gds:Client
Opc.Ua.Gds.Server builder.AddGdsServer(opt => …) IGdsServerBuilder yes OpcUa:Gds:Server
Opc.Ua.Lds.Server builder.AddLdsServer(opt => …) ILdsServerBuilder yes OpcUa:Lds
Opc.Ua.WotCon.Server builder.AddWotConServer(opt => …) IWotConServerBuilder yes (via AddServer) OpcUa:WotCon:Server
Opc.Ua.WotCon.Client builder.AddWotConClient(opt => …) IOpcUaBuilder — OpcUa:WotCon:Client
Opc.Ua.PubSub builder.AddPubSub(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (publish-only) builder.AddPubSubPublisher(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (subscribe-only) builder.AddPubSubSubscriber(opt => …) IPubSubBuilder yes OpcUa:PubSub
Opc.Ua.PubSub (SKS client) builder.AddPubSubSecurityKeyServiceClient(...) IPubSubBuilder — OpcUa:PubSub:Sks:Client
Opc.Ua.PubSub (SKS server) builder.AddPubSubSecurityKeyServiceServer(...) IPubSubBuilder yes OpcUa:PubSub:Sks:Server
Opc.Ua.PubSub.Udp pubSub.AddUdpTransport() IPubSubBuilder — —
Opc.Ua.PubSub.Mqtt pubSub.AddMqttTransport() IPubSubBuilder — —
Opc.Ua.PubSub.Server serverBuilder.AddPubSubAddressSpace(...) IPubSubServerBuilder yes OpcUa:PubSub:AddressSpace
Opc.Ua.Redundancy.Server serverBuilder.UseDistributedAddressSpace(...) / UseReplicatedAddressSpace(...) / UseActiveActiveRedundancy(...) / UsePeerDiscovery(...) / AddServerRedundancy(...) IOpcUaServerBuilder yes (via AddServer) see HighAvailability.md
Opc.Ua.Redundancy.Kubernetes serverBuilder.UseKubernetesRaftConsensus(...) IOpcUaServerBuilder yes (via AddServer) see Kubernetes.md

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

Extension Builder Section
AddDefaultIdentityAuthenticators(...) server, gds OpcUa:Server:Identity:Defaults
AddIdentityAuthenticator<T>() server, gds —
AddIdentityAuthenticator(instance) / (factory) server —
AddIdentityAugmenter<T>() server, gds —
AddGdsApplicationSelfAdminProvider() gds —
AddJwtIssuer(...) server, gds OpcUa:Server:Identity:Issuers[]
WithAuthorizationService(...) / <TIssuer>() gds —
WithKeyCredentialPush(...) server —
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. Zero keeps unlimited history; a negative value disables new storage. Directory prunes prior history at a negative limit; the shared key/value store retains it.
SetAutoAcceptUntrustedCertificates(...) Accepts otherwise-valid unknown peer certificates; default false. Prefer AutoAcceptUntrustedCertificates. Use true only in an isolated lab.
SetAddAppCertToTrustedStore(...) Adds a newly created application certificate to a shared trusted store; default false.
SetRejectSHA1SignedCertificates(...) Rejects SHA-1-signed certificates; default true. Prefer RejectSHA1SignedCertificates.
SetRejectUnknownRevocationStatus(...) Rejects chains when CA revocation status cannot be determined; default true.
SetUseValidatedCertificates(...) Reuses previously validated certificates without repeating the full validation path; default false. Leave disabled unless the reduced revalidation is explicitly acceptable for the deployment.
SetSuppressNonceValidationErrors(...) Suppresses zero/weak nonce errors; default false. Enabling it weakens user-token protection and is only for unavoidable legacy interoperability.
SetSendCertificateChain(...) Sends the chain with a CA-signed application certificate; default true.
SetMinimumCertificateKeySize(...) Sets the minimum accepted RSA key size; default 2048. Prefer MinimumCertificateKeySize.
AddCertificatePasswordProvider(...) Supplies an ICertificatePasswordProvider for protected private keys; no provider is registered by default.

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

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

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

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

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

AddHttpsTransport() is supplied by OPCFoundation.NetStandard.Opc.Ua.Bindings.Https. Add that package directly when enabling HTTPS, WSS, Kestrel-hosted OPC TCP, or the REST binding; the GDS server package does not reference it transitively.

Options binding

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

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

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

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

The default section names are:

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

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

Server feature

builder.AddServer(o => …) hosts a DependencyInjectionStandardServer via a private OpcUaServerHostedService. This implementation derives from ReverseConnectServer, so configured reverse connections work alongside the dependency-injection hooks without an application server subclass. The ordinary StandardServer and its direct-construction behavior are unchanged. Endpoints, PKI root, security policies, and the application instance certificate are all set up on host startup.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole();

builder.Services
    .AddOpcUa()
    .AddServer(o =>
    {
        o.ApplicationName = "MyServer";
        o.ApplicationUri = "urn:localhost:MyOrg:MyServer";
        o.ProductUri = "uri:myorg:myserver";
        o.AutoAcceptUntrustedCertificates = false;
        o.EndpointUrls.Add("opc.tcp://localhost:51210/MyServer");
    })
    .AddNodeManager<MyNodeManagerFactory>()       // IAsyncNodeManagerFactory
    .AddSyncNodeManager<LegacyNodeManagerFactory>(); // INodeManagerFactory

await builder.Build().RunAsync();

.AddNodeManager(...), .AddNodeManagers(...), and .AddSyncNodeManager<T>() register factories under an OpcUaServerNodeManagerRegistration wrapper that is scoped to the regular server feature. Node managers registered this way are not visible to the GDS / LDS hosted services running in the same container. See Combined hosts below.

.AddServer(...) throws InvalidOperationException on a second call: at most one regular server may be registered per service collection.

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

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

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

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

builder.Services
    .AddOpcUa()
    .AddServer(o => o.ApplicationName = "MyServer")
    .AddSessionManager<MySessionManager>()
    .AddSubscriptionManager<MySubscriptionManager>()
    .AddDurableSubscriptions(subscriptionStore, monitoredItemQueueFactory)
    .AddHistorian(historianProvider)
    .AddFileSystem(fileSystemProvider)
    .AddSecretStore(secretStore)
    .AddCertificateManager(certificateManager)
    .AddAliasNameStore(aliasNameStore);

If no identity authenticator is configured, the regular hosted server adds an anonymous authenticator matching its default anonymous user-token policy. If a non-anonymous user-token policy is configured without a corresponding authenticator, startup logs a warning.

The customization extensions below are methods on IOpcUaServerBuilder in Microsoft.Extensions.DependencyInjection; they return the same builder. They use the existing server and dependency-injection dependencies.

Adopting these hooks is optional. Existing direct server construction and subclass-based customization remain supported; upgrading does not require switching to dependency injection.

Server metadata

ConfigureServerProperties(Action<Opc.Ua.ServerProperties>) configures the metadata published in the server's build information:

services.AddOpcUa()
    .AddServer(options =>
    {
        options.ApplicationName = "PlantServer";
        options.ProductUri = "urn:example:PlantServer";
    })
    .ConfigureServerProperties(properties =>
    {
        properties.ManufacturerName = "Example Automation";
        properties.SoftwareVersion = "2.0.0";
        properties.BuildNumber = "42";
        properties.BuildDate = new DateTime(2026, 9, 1, 0, 0, 0, DateTimeKind.Utc);
    });

The callback configures IOptions<Opc.Ua.ServerProperties>. A directly registered Opc.Ua.ServerProperties service takes precedence over those options; the two sources are not merged.

Property left unspecified Default
ProductName The effective ApplicationConfiguration.ApplicationName.
ProductUri The effective ApplicationConfiguration.ProductUri.
SoftwareVersion The existing stack software-version helper's value.
BuildNumber The existing stack build-number helper's value.
ManufacturerName, BuildDate No publisher identity or build date is inferred; configure these explicitly.

The version and build-number fallbacks are stack defaults, not the publisher application's version. Supply the publisher's own metadata for released products. On the XML/stream path, the effective application identity comes from the loaded configuration, including any ConfigureLoadedConfiguration changes.

Node-manager factories

Use AddNodeManager<TFactory>() for an IAsyncNodeManagerFactory resolved from dependency injection, or the existing AddSyncNodeManager<TFactory>() for an INodeManagerFactory. Generic factories are resolved lazily, after the effective application configuration has been loaded or built. Already-constructed instances are also supported through AddNodeManager(IAsyncNodeManagerFactory) and AddNodeManager(INodeManagerFactory).

Configuration-dependent constructors should use AddNodeManagers(Func<IServiceProvider, ApplicationConfiguration, ArrayOf<IAsyncNodeManagerFactory>>). The callback receives the effective configuration, including loaded-file overrides, and can return zero, one, or many factories:

using Microsoft.Extensions.DependencyInjection;

services.AddOpcUa()
    .AddServer("PlantServer.Config.xml")
    .AddNodeManagers((sp, configuration) =>
    [
        ActivatorUtilities.CreateInstance<PlantNodeManagerFactory>(sp, configuration)
    ]);

Here PlantNodeManagerFactory is an application-defined IAsyncNodeManagerFactory whose constructor accepts ApplicationConfiguration and any other injected dependencies. Use [] when no factories are needed, or include more factories in the returned ArrayOf<IAsyncNodeManagerFactory>. The loaded configuration is supplied explicitly; it is not registered as a global ApplicationConfiguration service for constructor injection.

Post-startup tasks

AddStartupTask<TTask>(), where TTask : class, IServerStartupTask, registers an injected task implementing OnServerStartedAsync(IServerContext, CancellationToken). Alternatively, AddStartupTask(Func<IServiceProvider, IServerContext, CancellationToken, ValueTask>) registers an asynchronous delegate. IServerStartupTask is in Opc.Ua.Server.Hosting.

For example, register an application task, or delegate to an injected initializer whose InitializeAsync returns ValueTask:

serverBuilder.AddStartupTask<MyStartupTask>();
serverBuilder.AddStartupTask((sp, context, cancellationToken) =>
    sp.GetRequiredService<MyInitializer>().InitializeAsync(context, cancellationToken));

Type registration is idempotent: registering the same TTask again does not run it twice. Separate delegate registrations are distinct tasks. Tasks run sequentially in registration order, once after the server has started. An exception or cancellation aborts hosted startup and triggers server cleanup; subsequent tasks do not run.

StopAsync waits for execution cleanup even after a host shutdown timeout, so the host does not dispose dependencies that the server is still using. Slow cleanup can therefore outlast that timeout.

Use injected services for application dependencies and the existing typed node-manager lookup for server subsystems. For example, inside a task:

using System.Linq;
using Opc.Ua;
using Opc.Ua.Server;

IDiagnosticsNodeManager diagnostics =
    context.FindNodeManagers<IDiagnosticsNodeManager>().Single();
HistoryServerCapabilitiesState capabilities =
    await diagnostics.GetDefaultHistoryCapabilitiesAsync(cancellationToken)
        .ConfigureAwait(false);

No extra IServerContext members or server subclass are needed. Prefer the historian providers' automatic capabilities rollup when it already describes the server's history support, rather than manually overriding the published capabilities in a task.

Alias-name stores and standard browse nodes

AddAliasNameStore(store) and AddAliasNameStoreRegistry(registry) make registered stores available before address-space startup. Application IServerPreStartupTask registrations run first, in their registration order, so they can initialize source registries before the stores are copied into the server's registry. ConfigureAliasNames(Action<AliasNameServerOptions>) controls whether the normal ConfigurationNodeManager also materializes their aliases and declared optional capabilities under the standard TagVariables and Topics categories:

serverBuilder
    .AddAliasNameStore(aliasNameStore)
    .ConfigureAliasNames(options => options.MaterializeAliasNodes = true);

AliasNameServerOptions is in Opc.Ua.Server.AliasNames; its MaterializeAliasNodes defaults to false. Materialized browse nodes are a startup snapshot, not a live mirror of store mutations. This setting is independent of AliasNameNodeManagerOptions for custom categories, whose MaterializeAliasNodes default remains true. See Alias Names for capabilities, mutation behavior, and the lower-level materialization helper.

Localized resources

Use ConfigureResources(Action<Opc.Ua.Server.ResourceManager>) or ConfigureResources(Action<IServiceProvider, Opc.Ua.Server.ResourceManager>) to add translations or replace default status text. The callbacks run after the base resource manager has loaded the stack's default status text. In a Generic Host, the service-provider overload can also consume application configuration:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

serverBuilder
    .ConfigureResources(resources =>
        resources.Add("DeviceUnavailable", "de-DE", "Das Gerät ist nicht verfügbar."))
    .ConfigureResources((sp, resources) =>
    {
        string text = sp.GetRequiredService<IConfiguration>()["Messages:DeviceUnavailable"]
            ?? "The device is unavailable.";
        resources.Add("DeviceUnavailable", "en-US", text);
    });

The server creates, retains, and disposes its own resource manager. These callbacks do not register a live Opc.Ua.Server.ResourceManager service for injection. Resolve supporting services from sp and use the manager passed to the callback.

Translated text retains its original TranslationInfo fallback locale, template, and arguments. A retained result can therefore be translated for another session without losing its original fallback.

Migrating with an existing configuration XML file

Applications that already own a classic OPC UA application configuration file (*.Config.xml, previously loaded through ApplicationInstance.LoadApplicationConfigurationAsync) can pass that file to AddServer directly and keep every setting in it — base addresses, security policies, certificate stores, transport quotas, operation limits, user-token policies — while adopting the hosted dependency-injection surface:

builder.Services
    .AddOpcUa()
    .AddServer("MyServer.Config.xml")
    .AddNodeManager<MyNodeManagerFactory>();

The same file can be referenced from configuration instead (OpcUa:Server:ConfigurationFile), and a custom StandardServer subclass works too:

{ "OpcUa": { "Server": { "ConfigurationFile": "MyServer.Config.xml" } } }
builder.Services.AddOpcUa().AddServer<MyServer>("MyServer.Config.xml");

On this path the file is authoritative: the OpcUaServerOptions knobs that feed the configuration builder (ApplicationName, EndpointUrls, PkiRoot, policy toggles, transport quotas, ReverseConnect, ConfigureBuilder, ...) are not applied, and the file also takes precedence over a shared application registered with ConfigureApplication(...). Options that act on the hosted server itself (Identity, ConfigureRateLimits) and runtime registrations (AddNodeManager, AddNodeManagers, startup tasks, authenticators, metadata, resources, and alias settings) keep working. Configuration-building shortcuts such as AddReverseConnect and ConfigureOperationLimits do not override file settings. To override individual file settings from code, use ConfigureLoadedConfiguration, also exposed as the optional AddServer callback below. It runs after the file is loaded and validated but before certificates are checked and the server starts:

builder.Services
    .AddOpcUa()
    .AddServer("MyServer.Config.xml", configuration =>
    {
        configuration.ServerConfiguration.MaxSessionCount = 25;
    });

A configuration document that is not a file on disk — an embedded resource, a document fetched from a store — can be supplied as a Stream instead (OpcUaServerOptions.ConfigurationStream; mutually exclusive with ConfigurationFile). The stream must remain open until the host starts the server; it is read once and disposed by the hosted service during startup, so do not wrap it in a using yourself:

Stream stream = typeof(Program).Assembly
    .GetManifestResourceStream("MyApp.MyServer.Config.xml")!;

builder.Services
    .AddOpcUa()
    .AddServer(stream)   // read + disposed when the host starts the server
    .AddNodeManager<MyNodeManagerFactory>();

The certificate manager and certificate password provider registered in dependency injection are honored the same way as on the options path, and the unmatched user-token-policy startup warning is derived from the policies advertised by the file. The client offers the same migration path — see Client: using an existing configuration XML file.

Server security and resource controls

The hosted server is secure by default: sign-and-encrypt policies are included, SecurityPolicy#None is excluded, SHA-1 certificates are rejected, the minimum RSA key size is 2048, and unknown certificates are not auto-accepted. Review these controls explicitly for every deployment:

  • Keep IncludeSignAndEncryptPolicies = true. 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/...).
ConfigurationFile LoadApplicationConfigurationAsync(path) Load the configuration from an existing OPC UA XML configuration file instead of building it from the options (see Migrating with an existing configuration XML file).
ConfigurationStream LoadApplicationConfigurationAsync(stream) Code-only: load the configuration from a stream (e.g. an embedded resource); read once and disposed at startup. Mutually exclusive with ConfigurationFile.
ConfigureLoadedConfiguration Code-only callback Override individual settings of the configuration loaded from ConfigurationFile / ConfigurationStream.
ConfigureBuilder Code-only callback Pre-security server-policy and server-option escape hatch, including max failed authentication attempts, sessions, channels, auditing, and HTTPS mutual TLS.
ConfigureRateLimits Code-only callback Tunes the default connection and session-establishment admission controls.

Server-side reverse connect

A regular DI server can dial back to clients via reverse-hello using OpcUaServerOptions.ReverseConnect or the fluent AddReverseConnect(...) shortcut. Its ReverseConnectServer base runs the configured outbound connections while retaining the other DI customization hooks. The options projection into ServerConfiguration.ReverseConnect is unchanged. The data binds directly from OpcUa:Server:ReverseConnect:

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

Equivalent code-only configuration:

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

For a loaded XML file or stream, its ServerConfiguration.ReverseConnect is authoritative; use ConfigureLoadedConfiguration to change it rather than the options-building shortcuts. Directly constructed StandardServer instances do not gain reverse-connect behavior. See Reverse Connect for the hosted lifecycle and a loaded-configuration example.

Operation limits

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

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

User token policies

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

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

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.
AddIdentityAuthenticator(IUserTokenAuthenticator) Registers an already-constructed authenticator instance.
AddIdentityAuthenticator(Func<IServiceProvider, ICertificateValidatorEx?, IUserTokenAuthenticator>) Creates an authenticator during startup with injected services and the effective certificate validator, which may be null.
AddIdentityAugmenter<TAugmenter>() Registers a single custom IIdentityAugmenter implementation. The hosted service runs it after accepted authentications.
AddDefaultIdentityAuthenticators(Action<DefaultAuthenticatorOptions>) / (IConfiguration) Registers the four in-box authenticators (Anonymous, UserNamePassword, X509, Jwt) with toggles per type plus the JWT audience / clock-skew settings.
AddJwtIssuer(Action<JwtIssuerOptions>) / (IConfiguration) Registers a trusted JWT issuer. Multiple calls coexist; each contributes a StaticIssuerKeyResolver and / or JwksIssuerKeyResolver keyed by IssuerUri.
WithKeyCredentialPush(Action<KeyCredentialPushOptions>?) Enables the Part 12 §8 resource-server Push binding and registers an IKeyCredentialStore if none is supplied.

Configuration binding under OpcUa:Server:Identity:

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

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

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

The instance and factory overloads complement the existing generic registration. Factories receive the validator after configuration and certificate setup; a validator-dependent authenticator should fail clearly if the nullable validator is absent. Register matching UserTokenPolicies as well — an authenticator does not advertise its token policy automatically. See custom authenticator registrations for an example using the configured certificate stores.

Fluent shortcuts and one-shot presets

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

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

Use AddRoleManager(IRoleManager) or AddRoleManager<T>() to replace the default role manager. AddServer<TServer>() remains available for custom server implementations. Custom types that need session, subscription, or durable-subscription DI hooks must derive from DependencyInjectionStandardServer. The same requirement applies to DI-only metadata, resource, and alias settings configured through ConfigureServerProperties, ConfigureResources, and ConfigureAliasNames. A non-DI custom server rejects these settings clearly during startup instead of silently ignoring them.

Fluent node managers can be registered without a factory class:

const string namespaceUri = "urn:example:line";

services.AddOpcUa()
    .AddServer(options => { /* endpoint and application options */ })
    .AddNodeManager(namespaceUri, nodes =>
    {
        ushort namespaceIndex =
            (ushort)nodes.Context.NamespaceUris.GetIndex(namespaceUri);
        nodes.CreateInstance(
                new QualifiedName("Line", namespaceIndex),
                parent => new FolderState(parent))
            .Configure(node => node.UnderObjectsFolder());
    });

The callback owns the complete address space contributed by this node manager. AddNodeManager(namespaceUri, build) does not create an implicit root folder.

AddHistorianFileStore(provider, path) combines the historian provider registration with a Part 20 file-system mount for demo and lab servers.

Positioning

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

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

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

Client factories compose the generated proxies over the managed session:

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

See Relative Spatial Location and Global Positioning.

Robotics

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

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

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

See the Robotics developer guide.

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

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

Client feature

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

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

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

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

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

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

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

Client: using an existing configuration XML file

The client offers the same migration path as the server: applications that already own a classic client configuration file (*.Config.xml) can pass it to AddClient and keep every setting in it — security configuration, certificate stores, transport quotas, client configuration:

services
    .AddOpcUa()
    .AddClient("MyClient.Config.xml", opt =>
    {
        opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
    });

The same file can be referenced from configuration (OpcUa:Client:ConfigurationFile), and a stream works for documents that are not files on disk, e.g. embedded resources (OpcUaClientOptions.ConfigurationStream; read once and disposed after loading):

Stream stream = typeof(Program).Assembly
    .GetManifestResourceStream("MyApp.MyClient.Config.xml")!;
services.AddOpcUa().AddClient(stream);

The document loads lazily through ApplicationInstance.LoadApplicationConfigurationAsync on first use — the first session connect, reverse-connect startup, or an explicit GetAsync on OpcUaClientOptions.ConfigurationProvider — which also runs the optional ConfigureLoadedConfiguration override callback and ensures the application-instance certificate, exactly like the shared ConfigureApplication(...) path. The supplied document is authoritative: it takes precedence over a shared application registered with ConfigureApplication(...), and combining it with an explicit Configuration, with the application identity properties, or setting both file and stream is rejected with a clear InvalidOperationException at registration. DI-registered ICertificateManager / ICertificatePasswordProvider are honored.

Client: loading the configuration eagerly

Hosted applications whose user interface needs the ApplicationConfiguration before any session exists — a WinForms or WPF client whose main form takes the configuration in its constructor, for instance — can opt into an eager load with OpcUaClientOptions.LoadConfigurationOnStart (bindable from OpcUa:Client:LoadConfigurationOnStart). It registers an IHostedService that awaits the load while the host starts, the client-side twin of the reverse-connect hosted service:

services
    .AddOpcUa()
    .AddClient("MyClient.Config.xml", opt => opt.LoadConfigurationOnStart = true);

using IHost host = builder.Build();
await host.StartAsync();

// The document is loaded and validated and the application instance
// certificate is ensured; a failure has already failed host.StartAsync().
ApplicationConfiguration configuration = host.Services
    .GetRequiredService<OpcUaClientOptions>()
    .ConfigurationProvider!
    .Configuration;

OpcUaClientOptions.ConfigurationProvider is the public IOpcUaApplicationConfigurationProvider resolved for the client — the supplied-document provider when ConfigurationFile / ConfigurationStream was set, and otherwise the shared ConfigureApplication(...) provider. It is available on the options instance resolved from the service provider, not on the instance passed to the AddClient(...) callback, and it exposes everything a hosted client needs: GetAsync(ct) as an explicit load trigger without connecting a session, Configuration to read the loaded document back, and Application (IApplicationInstance) for applications that manage their own instance certificate. Applications that do not use the Generic Host can call GetAsync directly instead of setting LoadConfigurationOnStart; both routes share the provider's single-flight load, so the configuration is never loaded twice.

LoadConfigurationOnStart applies to every provider source and is a no-op when an explicit Configuration was supplied, which is already complete. Because it decides a service registration, set it within the AddClient(...) callback or bind it from the configuration section; setting it on the resolved options afterwards has no effect.

Fluent shortcuts

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

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

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

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

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

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

Client security checklist:

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

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