32 Commits

Author SHA1 Message Date
Marc Schier e78d36ce4b Client/server transfer, WSS, and hosting fixes (#4802)
# Description

Layer 2 of the upstream non-UaLens extraction, with core/types
prerequisite #4801 (`marcschier-extract-core-and-types`). The initial
extraction commit `86ff10d3994f132f7343a929abdae209bc2e69b7` started
from `8881e096fb5e4eaa36f3c13f8caa57733373bd19` and copied exactly 13
client/server paths from #4462 at frozen head
`390c4a7a51b71da40caec2d22ef74518bbe670ce`, using its original base
`5b03e9afa492cdb36049ee52ae844f630f89d8db`. Those initial blobs were
source-identical; the core/types layer was not duplicated or rewritten.

- Add bounded temporary-file copying with byte/request limits, session
ownership checks, and independent bounded cleanup.
- Refresh revised monitored-item parameters after snapshot transfer and
notify the target handler after successful handle synchronization.
- Apply WSS opening deadlines while preserving caller cancellation, and
cover the wire envelope and request timeout behavior.
- Make hosted server presets honor role configuration without replacing
an explicitly registered role manager; include the corresponding tests
and documentation.

**Review follow-up:** commit `a5c8fe0884ad3346106529deba71d93c5df20a9a`
intentionally corrects three defects in the extracted bounded-copy
implementation. It captures one `SessionRequestContext` for managed
transfers, calls the standard `FileType.Close` declaration without
metadata discovery, and retains valid generation handles before
rejecting non-Good status or malformed metadata. Read/Close remain
strictly validated, and transfer/cleanup failures remain independently
visible. The follow-up also adds 21 direct-client regression cases and
updates `docs/FileSystemClient.md`, bringing this PR to 15 changed
paths. The current bounded-copy implementation is therefore
intentionally no longer identical to the frozen source.

No UaLens projects/UI, AMB or Machinery implementations, samples,
CI/native renderer changes, release changes, or generic infrastructure
files are included. The follow-up does not merge/rebase a lower or upper
stack layer.

**Current review-fix validation:** normal analyzers and
warnings-as-errors remained enabled, using the existing SDK **10.0.401**
without changing `global.json`, installing an SDK, or adding
suppressions. The final file-system and session-context selection passed
**241 tests on net10.0 and 241 tests on net48**, with **0 failed and 0
skipped** on each. Both selections include all 21 new regressions.
Before the production fix, the first 20-case regression suite had 13
failures and 7 passes, reproducing the lost cleanup, cancellation,
session-recreation and Close-discovery defects. An additional
deterministic recovery-gate race case was then added and passed on both
frameworks. Informational-severity formatting/style/analyzer checks
passed for the changed C# files.

| Review requirement | Direct regression evidence |
| --- | --- |
| Pin the managed generation through copying and cleanup |
`CopyRejectsRecreatedManagedGenerationAsync`,
`CopyRejectsRecoveryBetweenGenerationAndReadDispatchAsync` |
| Preserve a generation response that returns after cancellation |
`ManagedCopyClosesHandleReturnedAfterCancellationAsync` with immediate
and delayed responses through a real managed transport lease |
| Do not spend the Close deadline discovering metadata |
`CopyUsesDeclarationCloseWithoutDiscoveryAsync` for ordinary and managed
sessions |
| Retain valid handles before rejecting generation status/metadata |
`CopyClosesHandleBeforeRejectingGenerationStatusAsync`,
`CopyClosesHandleFromMalformedGenerationResponseAsync` |
| Preserve error semantics and strict Read/Close validation |
`CopyRejectsGenerationWithoutInventingHandleAsync`,
`CopyPreservesGenerationAndCleanupFailuresAsync`,
`CopyKeepsReadAndCloseStatusValidationAsync` |

<details>
<summary>Exact review-fix validation commands and results</summary>

Commands ran from this layer's repository root. The following variables
expand the actual executable, filters and results paths. Console logs
and TRX reports were retained in the session artifacts.

```powershell
$dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe'
$env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk'
$env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj'
$filter = 'FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem|FullyQualifiedName~Opc.Ua.Client.Tests.SessionContexts'
$r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net10'
$r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net48'

# Before modifying production code, after the new tests compiled:
& $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-red.trx' --results-directory $r10 --verbosity quiet
# Expected regression failure: Passed: 7, Failed: 13, Skipped: 0.

# After the production fix, before adding the explicit queued-recovery case:
& $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-green.trx' --results-directory $r10 --verbosity quiet
# Passed: 20, Failed: 0, Skipped: 0.

# Final complete affected selections, including all 21 new cases:
& $dotnet test $client --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r48 --verbosity quiet
# Passed: 241, Failed: 0, Skipped: 0.

& $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r10 --verbosity quiet
# Passed: 241, Failed: 0, Skipped: 0.

$sourceFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\src\Opc.Ua.Client\FileSystem\TemporaryFileTransferClient.BoundedRead.cs'
$testFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\tests\Opc.Ua.Client.Tests\FileSystem\TemporaryFileTransferClientBoundedReadTests.cs'
$env:CustomTestTarget = 'net48'
& $dotnet format 'src\Opc.Ua.Client\Opc.Ua.Client.csproj' --include $sourceFile --no-restore --verify-no-changes --severity info --verbosity quiet
# Passed, exit 0.
$env:CustomTestTarget = 'net10.0'
& $dotnet format $client --include $testFile --no-restore --verify-no-changes --severity info --verbosity quiet
# Passed, exit 0.

git diff --check
# Passed. CRLF, final newlines and the 120-column C# limit were also checked.
```

Initial no-restore test attempts after switching frameworks exited
without executing tests and were not counted as passes. Diagnostic
`dotnet build ... --no-restore` runs reported `NETSDK1005` for the
missing framework assets; normal restore was then allowed.
Test-authoring compile errors were corrected before recording the red
run, without suppressions. Intermediate broader selections passed
240/240 on each framework before the queued-recovery case increased the
final count to 241.

</details>

**Initial-extraction validation (commit
`86ff10d3994f132f7343a929abdae209bc2e69b7`):** these results are
historical, not a claim that the broader initial selections were rerun
after the review fix. The server test assembly was built as a dependency
of the client project before its `--no-build` runs.

| Initial targeted scope | net10.0 | net48 |
| --- | --- | --- |
| Client subscriptions and file system | Passed: 546, Failed: 0,
Skipped: 0 | Passed: 542, Failed: 0, Skipped: 0 |
| Hosted roles, identity, composition, fluent APIs and builder coverage
| Passed: 164, Failed: 0, Skipped: 0 | Passed: 164, Failed: 0, Skipped:
0 |
| WebAPI WSS transport | Passed: 37, Failed: 0, Skipped: 0 | Not
supported: the repository excludes this .NET 8+ test project on net48 |
| V2 transfer and snapshot integration | Passed: 6, Failed: 0, Skipped:
0 | Passed: 6, Failed: 0, Skipped: 0 |

The first broader net48 hosting run had **9 startup timeouts and
certificate leaks** (155 passed). A single startup case reproduced the
failure. With only a shorter `NUnit.WorkDirectory`, the isolated case
and then all 164 hosting cases passed, with certificate leak tracking
enabled and no teardown leaks. This is consistent with the fixture's
documented .NET Framework PKI path-length constraint; no source or
diagnostic suppression was changed. Both failed runs and successful
reruns are recorded below.

**Deferred coverage:** the broader domain-level
`MachineryResultTransferTests*.cs` fixtures remain in Machinery layer
#4804. They were not imported or run here. This review follow-up adds
direct-client coverage for the three defects instead. The Machinery
owner has been informed that the old expectations for failed Close
discovery and zero cleanup after a returned non-Good generation response
must be reconciled when the lower fixes are propagated.

**Not run:** complete `UA.slnx` suites on net48/net10.0, a coverage
measurement, CI/CodeQL completion, native/fuzz qualifications, or
package publishing. This remains a draft pending full-stack integration
and CI evidence.

<details>
<summary>Initial-extraction validation command arguments and
outcomes</summary>

All commands ran from this layer's repository root. Variables below
expand the executable, project paths, filters and results directories
used by the actual commands; test output was also retained in
session-local log files.

```powershell
$dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe'
$env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk'
$env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
$r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net10'
$r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net48'
$client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj'
$server = 'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj'
$webapi = 'tests\Opc.Ua.Bindings.Https.WebApi.Tests\Opc.Ua.Bindings.Https.WebApi.Tests.csproj'
$subscriptions = 'tests\Opc.Ua.Subscriptions.Tests\Opc.Ua.Subscriptions.Tests.csproj'
$clientFilter = 'FullyQualifiedName~Opc.Ua.Client.Subscriptions|FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem'
$serverFilter = 'FullyQualifiedName~HostedRolePresetTests|FullyQualifiedName~IdentityHostingTests|FullyQualifiedName~ServerFluentApiHostingTests|FullyQualifiedName~ServerCompositionHostingTests|FullyQualifiedName~OpcUaServerBuilderExtensionsCoverageTests'
$subscriptionFilter = 'FullyQualifiedName~Opc.Ua.Subscriptions.Tests.TransferSubscriptionTests|FullyQualifiedName~Opc.Ua.Subscriptions.Tests.SubscriptionSnapshotTests'

& $dotnet --version
# Passed: 10.0.401.

& $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet
# Exit 0 but no tests executed: missing restore metadata. NOT counted as validation.

& $dotnet build $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --verbosity quiet
# Failed: NETSDK1004, missing obj\project.assets.json. Normal restore was then allowed in test commands.

& $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet
# Passed: 546, Failed: 0, Skipped: 0.

& $dotnet test $server --no-build --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $serverFilter --logger 'trx;LogFileName=server-net10.trx' --results-directory $r10 --verbosity quiet
# Passed: 164, Failed: 0, Skipped: 0.

& $dotnet test $webapi -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~WebApiWssTransportChannelTests' --logger 'trx;LogFileName=webapi-net10.trx' --results-directory $r10 --verbosity quiet
# Passed: 37, Failed: 0, Skipped: 0.

& $dotnet test $client -c Release -f net48 -p:CustomTestTarget=net48 --filter $clientFilter --logger 'trx;LogFileName=client-net48.trx' --results-directory $r48 --verbosity quiet
# Passed: 542, Failed: 0, Skipped: 0.

& $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48.trx' --results-directory $r48 --verbosity quiet
# Failed: Passed: 155, Failed: 9, Skipped: 0; startup timeouts and 9 undisposed certificates at teardown.

& $dotnet test $subscriptions -c Release -f net48 -p:CustomTestTarget=net48 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net48.trx' --results-directory $r48 --verbosity quiet
# Passed: 6, Failed: 0, Skipped: 0.

& $dotnet test $subscriptions -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net10.trx' --results-directory $r10 --verbosity quiet
# Passed: 6, Failed: 0, Skipped: 0.

& $dotnet msbuild $webapi -p:CustomTestTarget=net48 -p:TargetFramework=net48 -getProperty:TargetFramework,RestrictForLegacyTfm,_RestrictedToLegacyTfm,IsTestProject,EnableDefaultCompileItems
# Passed: net48 / true / true / false / false. Confirms the net48 project is an empty non-test shell.

# Each following diagnostic invocation had certificate allocation tracking enabled.
$env:OPCUA_CERTIFICATE_LEAK_TRACKING = '1'

& $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~HostedRolePresetTests' --logger 'trx;LogFileName=hosted-role-net48.trx' --results-directory $r48 --verbosity quiet
# Passed: 10, Failed: 0, Skipped: 0.

& $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48.trx' --results-directory $r48 --verbosity quiet
# Failed: Passed: 0, Failed: 1, Skipped: 0; 30-second startup timeout and one certificate leak.

# A fresh, dedicated short working directory was created for the following two runs.
& $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556'
# Passed: 1, Failed: 0, Skipped: 0; no certificate leak.

& $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556'
# Passed: 164, Failed: 0, Skipped: 0; no certificate leak or assembly-level teardown error.
# The dedicated short working directory and its generated PKI contents were removed afterward.
```

TRX inspection confirmed all initially added client (3), WSS (4), and
hosted-role (10) regression cases executed on supported frameworks. The
initial 13-path set and source blobs were verified before the extraction
commit. A separate mistyped SDK availability probe
(`uals-528-deep-repro` instead of `ualens-528-deep-repro`) failed before
launching any process; all actual builds/tests used the correct verified
SDK above.

</details>

## Related Issues

Related to #4461; extracted from source PR #4462. Neither is closed by
this PR. Prerequisite: #4801, the core/types layer.

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-10-15 19:29:42 +02:00
Marc Schier 15d0ce7bd5 Implement OPC UA Part 10 Programs (#4789)
# Description

Implement OPC UA Part 10 Programs across `Opc.Ua.Core`/`Core.Types`,
`Opc.Ua.Server`, and `Opc.Ua.Client`, reusing the existing
state-machine, source-generation, node-management, auditing, and
streaming-subscription infrastructure.

- Add asynchronous provider-backed execution, standard controls,
internal/autonomous transitions, generation checks, and nested-state
activation/resumption.
- Add checked creation/adoption, vendor Create adapters, deletion,
recycling and instance limits, auto-delete, DI, and direct-construction
paths.
- Maintain ProgramDiagnostic2 and retained final results; publish
captured intermediate results and concrete transition/audit events with
access controls.
- Add generated SDK models/proxies, typed client discovery and reads,
event streams, and subscribe-before-call readiness handling.
- Add unit, connected client/server, generated Annex A-style model, and
NativeAOT coverage. Document usage and migration in `docs/Programs.md`
and related guides.

### Compatibility and design decisions

The linked Part 10 document is v1.05.00; namespace-zero definitions
remain aligned with 1.05.07. Documentation distinguishes normative
requirements from conflicting specification text and SDK conventions.

- Correct state numbers to Halted=11, Ready=12, Running=13,
Suspended=14. Generated subtypes retain inherited namespace-zero
state/transition identifiers.
- Managed Reset is Halted-to-Ready only. Preserve the existing low-level
Reset-from-Suspended behavior for compatibility.
- Preserve UInt32 limit types; use `UInt32.MaxValue` as a documented SDK
unlimited convention. RecycleCount overflow reports BadOutOfRange
without stopping unlimited execution.
- In-memory default with injectable providers. Durable recovery,
distributed execution/failover, and a production Annex A download engine
are excluded.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Tests and focused coverage evidence are above; the combined
tests/coverage checkbox remains open because a whole-repository coverage
increase has not been established. CLA status, CI/CodeQL results,
tracking-issue approval, and PR feedback are not asserted.
2026-10-15 09:19:25 +02:00
Marc Schier 7c965fd636 Capture metadata images and generation-bound session clients (#4752)
# Description

- Add isolated NodeState reference images, TypeTable graph snapshots,
EncodeableFactory registration forks/views, StringTable mutation
versions/private copies, and ServiceMessageContext mapping-holder
generation tracking. Reference images are lazy for empty nodes;
selected-image clones own independent storage and publication ownership.
- Add the complete generic NodeBrowser continuation-dependency
capability. This does not implement server browse-history retention,
NodeManager read images, or server batch/readiness admission.
- Add optional client binding capture for SessionClient, native Session
and ManagedSession, with managed-lease/native-channel generation
composition. Bindings capture authenticated-session identity, mapping
holders/versions and owned endpoint/policy/configuration observations.
Replacement, reconnect, mapping ABA and disposal permanently invalidate
stale bindings; disposing a binding does not dispose its owner.
- Preserve cancellation and master pooled-response ownership: a rejected
pooled response is released exactly once; an accepted response remains
caller-owned. Keep native transport submission under its replacement
gate and completion outside the gate, without routing ordinary unbound
requests through binding machinery.
- Rename the existing server lookup interface and all actual consumers
with minimal hunks; add focused fixtures, generated FileTypeClient usage
guidance, and package/transport documentation.

**Breaking preview type identity:** the existing 2.0-preview server
lookup `Opc.Ua.ISessionBindingProvider` becomes
`Opc.Ua.IServerSessionBindingProvider`. Server lookup consumers must
update their referenced type and rebuild. This is neither
source-compatible nor binary-additive for that preview type. The new
client capability is optional and adds no mandatory members to existing
ISession/ISessionClient/ITransportChannel contracts. This
preview-to-preview rename is documented in SessionBindings/Transports,
not in the 1.5-to-2.0 MigrationGuide.

**Ownership boundary:** no RequestLifetime changes,
certificate-manager/channel-entry adoption, BaseVariableState changes,
Method-result feature extraction, NodeManager batch/readiness or
browse-history retention, session restoration, registry federation, WoT
conversion, or WoT runtime/planners/events.
DependencyInjection/ResourceIsolation/index changes are lookup/link
closure only. SessionManager's PR hunk changes only its implemented
lookup-interface name; newer master public diagnostic masking is not
replaced.

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Part of #4511: user-approved combined groups 5+6 Wave A extraction.
This PR does not close the umbrella or publish Wave B.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

### Review qualification

Review fixes are published at
**`6b0eb1d5c32a439300fbcaff974b9af8b909161b`**, changing eight
already-owned files. All five received review threads were replied to
and resolved after verification. Factory selector installation and
target-selection-plus-commit share the root publication gate, with
consistent root→image release/capture ordering. TypeTable selector
installation and final mutation routing share its existing gate. Native
bound channel validity uses volatile observations; the two owned XML
defects are corrected.

| Requirement | Evidence |
|---|---|
| Selector installation must not hide TypeTable mutations |
`SelectorInstalledDuringNodePreparationReceivesMutation`: old Add
routing failed on both TFMs; corrected routing passes on both within
**148/148** factory/TypeTable controls |
| Selector installation must not race factory commit routing | Static
common-root-gate/order proof plus baseline-GREEN
`BuilderCapturedBeforeOwnershipUsesSelectedFactory`,
`ViewOwnerReleasePreservesRegistrationsAndAllowsReuse`,
`ViewOwnerReleaseCanRetryAfterPendingPublication`,
`PublicationProtectsPendingAndRetiredFactoryImages` |
| Lock-free validity must observe owner transitions | Static
volatile-read reasoning plus native
`BoundRequestCannotDispatchWhileNativeReplacementIsPendingAsync`,
`BoundResponseRejectsInvalidationAfterNativeDispatchAsync`,
`BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync`:
**12/12 each TFM**; Core binding/pooled ownership controls **17/17 each
TFM** |
| Correct owned XML documentation | “an unmanaged” and one multiline
`ReferenceUpdate.Next` summary; scoped whitespace/INFO and build
evidence below |

Factory controls are **not** claimed as a deterministic factory RED, and
public transport controls are **not** claimed as a deterministic
weak-memory RED. No private-lock reflection, artificial production seam,
or source-name assertion was added. The TypeTable RED uses the real
`ILocalNode.NodeClass` callback during node preparation, and both
expected RED receipts/TRX were retained.

All accepted review runs use the unchanged admitted SDK wrapper, normal
project references, sole mutex, one node/no shared compiler, finite
resource guards and canonical short-C build outputs. Source hashes were
frozen per command and matched the committed bytes afterward. SDK
ownership was released **before** commit/push/review replies. No
automatic merge was requested.

Scoped whitespace passed across the four affected projects. Review INFO
classification is **0 owned findings / 13 untouched baseline findings
excluded** (whole-file formatter exits 1,1,0,0); no baseline
RCS1140/RCS0055 expansion or suppression. The final normal net10 public
Client/Core/Types trim/AOT-analyzer closure passed **0 warnings / 0
errors**. This is not a NativeAOT publish/run.

<details>
<summary>Accepted review command payloads and results</summary>

These exact argument payloads were executed through the unchanged
guarded SDK wrapper. `$proof` expands to the protected results-directory
below. Successful explicit builds of the same source preceded the
`--no-build` runs.

```powershell
$proof = 'C:\Users\mschier\.copilot\session-state\a3b74048-c52f-4c87-8773-7a46c9ab0087\files\qualification-review-4752'

# Exit 0: 148 passed, 0 failed
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -f net48 -p:CustomTestTarget=net48 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --no-restore --verbosity minimal --filter "FullyQualifiedName~EncodeableFactorySnapshotTests|FullyQualifiedName~TypeTableTests" --logger "trx;LogFileName=review-types-green-net48.trx" --results-directory $proof

# Exit 0: 148 passed, 0 failed
dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -f net10.0 --no-restore --no-build --filter "FullyQualifiedName~EncodeableFactorySnapshotTests|FullyQualifiedName~TypeTableTests" --logger "trx;LogFileName=review-types-green-net10.trx" --results-directory $proof -p:CustomTestTarget=net10.0 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal

# The following two per-TFM payloads each exited 0:
# Core: 17/17 net48 and 17/17 net10.0; Client: 12/12 net48 and 12/12 net10.0
foreach ($tfm in @('net48', 'net10.0')) {
    dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -f $tfm --no-restore --no-build --filter "FullyQualifiedName~SessionBindingResponseTests|FullyQualifiedName~SessionBindingContextTests" --logger "trx;LogFileName=review-actual-core-green-$tfm.trx" --results-directory $proof -p:CustomTestTarget=$tfm -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal
    dotnet test tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj -f $tfm --no-restore --no-build --filter "FullyQualifiedName~BoundRequestCannotDispatchWhileNativeReplacementIsPendingAsync|FullyQualifiedName~BoundResponseRejectsInvalidationAfterNativeDispatchAsync|FullyQualifiedName~BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync" --logger "trx;LogFileName=review-actual-client-green-$tfm.trx" --results-directory $proof -p:CustomTestTarget=$tfm -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal
}

# Exit 0: 0 warnings, 0 errors
dotnet build src\Opc.Ua.Client\Opc.Ua.Client.csproj -f net10.0 -p:CustomTestTarget=net10.0 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --no-restore --verbosity minimal
```

The six expanded GREEN commands, exact results/TRX hashes and two
expected RED commands are retained in
`qualification-review-4752\qualification-summary.json`, SHA256
`C5E71A4D8D37BAC8803F6DF38B6BCE2B70F3D0AFD5DEC8493FAE9DB440DCFB5A`.
Committed/qualified byte identity is in `publication-identity.json`,
SHA256
`CD9515FE96703F8B241F2B7B9B71F73EF0FF2626C2C8BCCE40307971A8F8E809`.
Per-command raw exits, guards, hashes and reports are retained outside
disposable build roots.

For each record in the protected `format-inputs.json` (Types/Core/Client
libraries and Types.Tests), the recorded Project/Include values expand
these exact payloads:

```powershell
# Workspace properties recorded with the formatter receipts:
$env:CustomTestTarget = 'net10.0'
$env:UseArtifactsOutput = 'true'
$env:ArtifactsPath = 'C:\cp56-a3b7-v2'

# All four exit 0
dotnet format whitespace $Project --no-restore --verify-no-changes --verbosity minimal --include $Include

# Exits 1,1,0,0 due only to untouched baseline findings; owned=0, baseline=13
dotnet format $Project --no-restore --severity info --verify-no-changes --verbosity minimal --report "$proof\formats\review-info-$i" --include $Include
```

Initial fresh matrix evidence remains separately identified and
unchanged: Types274/Core119/Client151/server137 net10;
Types274/Core119/Client151/server135 net48, all passing. Qualification
base remains `0dff94bd378e79b8cf1a47e1aaf340f9b4b40048`; the initial
published head was `9fee4583`. All 16 initial exact command
arrays/results/TRX hashes remain in protected
`qualification-v2\qualification-summary.json`, SHA256
`88E10240C20049B59A3401BE007C6528A85DF2F3DCFAF9D11D908CEF2946DA4E`.

Failures/incidents remain retained rather than relabeled as success:
original disappeared-root historical counts are invalidated; the
large-TRX controller DOM-parser failure is preserved separately from
accepted streaming receipts; the review argument-array
defect/default-D-output controls are preserved but excluded from
canonical review qualification. Correctly separated argument rechecks
produced the accepted short-C Core/Client receipts. The sole SDK wrapper
was not changed.

Full UA.slnx, measured coverage increase, CLA verification, NativeAOT
publication and current-head/master CI/CodeQL remain unclaimed. Actual
merge—not resolved review threads—unlocks Wave B.

</details>
2026-10-13 06:25:43 +02:00
Klaus Landsdorf b0d8ca0d3a Server: session-less service invocation (OPC 10000-4 §6.3) with AddSessionlessInvocation (#4659)
Marc answered the five design questions (2026-10-05). This update
implements the answers; see "Design decisions" at the end.

# Description

OPC 10000-4 §6.3 lets a client invoke a subset of the services without
CreateSession/ActivateSession. The request then carries either an Access
Token in `RequestHeader.authenticationToken` or no token at all. The
server today only offers the low-level
`ISessionManager.ValidateSessionLessRequest` event. Without a handler,
every request whose token names no session is answered with
`Bad_SessionIdInvalid`, including a request without a token. That is the
code a client of an expired session expects; for a session-less request,
§6.3.1 has a server that does not support the invocation answer
`Bad_ServiceUnsupported`.

This PR adds built-in session-less invocation behind an opt-in and keeps
the existing event as the way to take over completely.

## Changes

### `AddSessionlessInvocation()`
(`src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs`)

```csharp
services.AddOpcUa()
    .AddServer(options => { /* … */ })
    .AddSessionlessInvocation(o => o.AllowAnonymous = false)  // the default
    .AddJwtIssuer(/* … */);   // validates the Access Tokens
```

- Registers `SessionlessInvocationOptions`.
`DependencyInjectionStandardServer` applies them to the `SessionManager`
in use, including one created by a registered session manager factory,
unless that manager already has options.
- Without dependency injection: `SessionManager.SessionlessInvocation =
new SessionlessInvocationOptions()`.
- Without either, nothing changes apart from the
`Bad_ServiceUnsupported` answer below.

### `SessionlessInvocationOptions` (new, `src/Opc.Ua.Server/Session/`)

- `AcceptAccessTokens` (default `true`): an Access Token in
`RequestHeader.authenticationToken`, carried as a String NodeId, is
handed to the server's identity registry (`IServerIdentityRegistry`, the
one ActivateSession uses) as an issued JWT token (OPC 10000-6
§7.6.5.2.3). The resulting identity runs the request. §6.3.1 requires an
encrypted channel for the token, so it is only accepted over
SignAndEncrypt or HTTPS.
- `AllowAnonymous` (default `false`): a request without a token runs as
an anonymous user. Without it, such a request is rejected with
`Bad_IdentityTokenInvalid`.
- `MaxConcurrentRequests` (default 64) and
`MaxConcurrentRequestsPerChannel` (default 16): the budget for
session-less requests, see below. Zero does not limit; a negative value
is rejected.

### Budget for session-less requests (`SessionlessRequestBudget`, new,
internal)

#4541 bounds channels and incomplete messages. A session-less request
has no Session to be accounted for, but it occupies a worker and keeps
its channel active. It gets a budget of its own:

- **Two limits**, checked and taken together under one
`System.Threading.Lock`: concurrent requests on the server, and on one
channel. A request refused by the channel limit takes no server-wide
place. The channel is the SecureChannel on opc.tcp. The HTTPS bindings
use one SecureChannel id per listener, so their requests are grouped by
peer address (IPv4-mapped IPv6 addresses count as IPv4).
- **Answer**: `Bad_ServerTooBusy` ("The Server does not have the
resources to process the request at this time."), the code #4541's
request queue uses when it has no room. `Bad_TooManyOperations` means
too many operations in one request, so it does not fit.
- **Lifetime**: the place is taken before the identity is checked, which
also bounds the validations that run at the same time. The lease is
attached to the `OperationContext` and returned when the context is
disposed, which every service does with `using`, however the request
ends (completed, failed, cancelled). A rejected token, a handler error
or exception, and a cancelled validation return it at once. A lease
releases once, however often it is disposed. Channel entries are removed
with their last request.
- **Scope**: it applies to every session-less request on a manager that
has `SessionlessInvocation` set, including requests a
`ValidateSessionLessRequest` handler decides. A handler without
`AddSessionlessInvocation` runs without a limit, as before. Requests
with a Session token and services outside the §6.3.1 set are not
counted.
- The limits are read for each request, so a change of the options
applies to the next request.

### `SessionManager`

- **Service sets:** a request without a session is limited to the
service sets §6.3.1 allows: View (without
RegisterNodes/UnregisterNodes), Attribute, Method, NodeManagement and
Query. Every other service keeps answering `Bad_SessionIdInvalid`.
`IsSessionlessService(RequestType)` is public.
- **Request without a token:** with session-less invocation not enabled
and no `ValidateSessionLessRequest` handler, it is answered with
`Bad_ServiceUnsupported` (§6.3.1). A token that names no session stays
`Bad_SessionIdInvalid`.
- **Precedence:** a registered `ValidateSessionLessRequest` handler
still decides first, unchanged apart from the service-set limit.
`ValidateSessionlessRequestAsync` is `protected virtual` for servers
that need their own rules.
- **Roles:** the identity of a session-less request goes through the
role resolution ActivateSession applies to the effective identity of a
Session (the `ResolveRoles` part of `AddMandatoryRoles`: the
`MustChangePassword` restriction, `IRoleManager.ResolveGrantedRoles`, so
mapped Access Token claims and the built-in AuthenticatedUser role).
This includes an identity a `ValidateSessionLessRequest` handler sets.
There is no client application certificate without a Session, so no
TrustedApplication role and no application-based mapping.
- **Cancellation:** cancelling an Access Token validation propagates as
`OperationCanceledException` so the endpoint maps it to the status of
the request lifetime (previously it became `Bad_UnexpectedError`). The
budget place is still returned.
- **Log:** a rejected Access Token is logged with a dedicated
`[LoggerMessage]` (`ServerEventIds.SessionManager + 18`, the next free
id after #4632).

### Documentation

- `docs/SessionlessInvocation.md` (new, linked from `docs/README.md`):
which services, enabling it, caller identity, the answers without the
feature, the request limits, the handler, calling the server over
opc.tcp and the REST binding.
- `docs/ResourceIsolation.md`: a short section "Requests without a
Session" pointing to the budget.
- `docs/DependencyInjection.md`: `AddSessionlessInvocation` in the
identity extension tables.

## Tests

- `Opc.Ua.Server.Tests/SessionlessInvocationTests` (mocked server
internals):
- `SessionlessRequestIsUnsupportedUnlessEnabled`,
`AnUnknownSessionTokenStaysBadSessionIdInvalid`,
`ServicesOutsideTheSessionlessSetStillNeedASession` (7 services),
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler`,
`AValidateSessionLessRequestHandlerTakesPrecedenceAsync`;
-
`SessionlessRequestWithoutIdentityIsRejectedUnlessAnonymousIsAllowedAsync`,
`SessionlessAccessTokenIsValidatedByTheIdentityRegistryAsync`,
`AnAccessTokenNoAuthenticatorHandlesIsRejected`,
`SessionlessAccessTokenRequiresAnEncryptedChannel` (None, Sign),
`SessionlessAccessTokenIsAcceptedOverSignAndEncryptAsync`,
`AccessTokensCanBeDisabled`,
`IsSessionlessServiceFollowsTheServiceSetsOfPartFour` (21 cases),
`ValidateSessionlessRequestRejectsMissingArguments`;
- wiring: `AddSessionlessInvocationRegistersTheConfiguredOptions`,
`AddSessionlessInvocationRejectsANullBuilder`,
`TheHostedServerAppliesTheOptionsToItsSessionManager`,
`TheHostedServerAppliesTheOptionsToAFactoryCreatedSessionManager`,
`TheHostedServerKeepsOptionsTheSessionManagerAlreadyHas`.
- End to end against the reference server,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests` (opc.tcp,
`SessionClient` on a channel without CreateSession):
- `ReadWithAnAccessTokenNeedsNoSessionAsync` (SignAndEncrypt),
`AnAccessTokenOnAnUnencryptedChannelIsRejectedAsync`,
`ARequestWithoutAnIdentityIsRejectedAsync`,
`SubscriptionsStillNeedASessionAsync`;
- one sample per service set: `ViewServicesRunWithoutASessionAsync`
(Browse, TranslateBrowsePathsToNodeIds),
`AttributeServicesRunWithoutASessionAsync` (Write, HistoryRead),
`MethodServicesRunWithoutASessionAsync` (Call),
`NodeManagementServicesRunWithoutASessionAsync` (AddNodes). They check
that the service runs and reports its operation result, not a missing
session or an internal error, since #4632 hardened only
Browse/BrowseNext against a missing session. Query is not sampled:
`StandardServer` does not implement QueryFirst/QueryNext.
- REST binding,
`HttpsWebApiIntegrationTests.SessionlessReadOverRestRunsWithAnAccessTokenAsync`
(Compact and Verbose): `POST /read` with the Access Token as String
NodeId (`"AuthenticationToken": "s=<token>"`), and the same request
without a token.
- **Budget**, `Opc.Ua.Server.Tests/SessionlessRequestBudgetTests` (22
tests):
- the budget itself: server limit and release, channel limit that takes
nothing from the server, server limit checked first, zero does not
limit, a lease releases once, HTTPS grouped by peer address (IPv4-mapped
form too), 200 concurrent callers against the limits (exactly the limit
granted, none above the channel limit, parallel release leaves zero and
no tracked channel), 16 callers acquiring and releasing in a loop
(high-water mark never above the limit);
- through `SessionManager.ValidateRequestAsync`: `Bad_ServerTooBusy`
over the server and the channel limit, disposing a context twice
releases once, places returned after a rejected Access Token, an
unencrypted channel, a missing identity, a cancelled validation (while
it waits, the place is taken), a handler error and a handler exception;
the budget also limits requests a handler decides, and a handler-only
server has none; requests outside the service set and requests a
disabled server refuses take no place; changed limits apply to the next
request and new options start a new budget;
  - options: finite defaults, negative values rejected.
- **Budget end to end**,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests.SessionlessRequestsRunInABudgetOfTheirOwnAsync`
(opc.tcp, requests held in the Access Token validation): the third
request over a limit of two gets `Bad_ServerTooBusy`; after release,
five sequential reads and five reads of an unknown node (operation
result bad) all pass; a channel limit of one refuses the second request
on that channel while another channel's request is admitted.
`ARequestWithoutATokenIsUnsupportedWhileTheFeatureIsOffAsync`:
`Bad_ServiceUnsupported` for a request without token on a server without
the feature, `Bad_SessionIdInvalid` for a token that names no Session.
- **Fail without the change**: the places-returned tests fail when the
budget does not release on failure (4 of 22 fail with the release in
`SessionManager` removed; the other budget tests use API this change
adds). `SessionlessRequestIsUnsupportedUnlessEnabled` and
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler` are
unchanged from the first version.
- **Review follow-up (`e9a35980b`)**:
`SessionlessAccessTokenReceivesTheRolesMappedFromItsClaimsAsync`,
`SessionlessAccessTokenWithoutTheMappedClaimIsNotGrantedTheRoleAsync`,
`AnAnonymousSessionlessRequestIsGrantedTheAnonymousRoleOnlyAsync`,
`TheIdentityOfAValidateSessionLessRequestHandlerReceivesTheMappedRolesAsync`,
`AValidateSessionLessRequestHandlerThatSetsNoIdentityKeepsNoIdentityAsync`;
end to end
`ARoleMappedNodeIsReadWithTheRolesMappedFromTheAccessTokenAsync` (the
ServerDiagnosticsSummary, readable for SecurityAdmin only, is denied for
a token whose claim is not mapped, readable once the rule maps it,
denied for another token) and
`ACancelledAccessTokenValidationAnswersTheTimeoutAndReturnsItsPlaceAsync`.
The anonymous and no-identity tests guard unchanged behavior. With the
`SessionManager.cs` change removed the three claim-mapping tests, the
end-to-end role test (`Bad_UserAccessDenied`), the end-to-end
cancellation test (`Bad_UnexpectedError` instead of `Bad_Timeout`) and
`ACancelledValidationReturnsItsPlaceAsync` fail. Lines of this commit
were not re-measured for patch coverage; the figure below is for
`496f84fce`.
- **`ClientTest`**: the three assertions that accepted
`Bad_ServiceUnsupported` too are back to master (`Bad_SessionIdInvalid`
only), so the PR no longer touches that file. Since #4646 a closed
`Session` refuses a request without authenticationToken itself; the
server code is not reached. The three tests pass with the single code
(12 cases on net10.0).
- Results on macOS: `Opc.Ua.Server.Tests` full run on net10.0 8076
passed / 9 skipped / 1 failed
(`ACancelledValidationReturnsItsPlaceAsync`, which asserted the old
`Bad_UnexpectedError` mapping; updated to the propagated cancellation
and passing); the session-less, SessionManager and role filters 368
passed / 0 failed on net10.0 and on net8.0, and the session-less
invocation and budget tests 73 passed on net10.0 after the last test was
added. `Opc.Ua.Sessions.Tests` with the
HTTPS/WebApi/WSS/OpenApi/Sessionless/ClientTest filter: 400 passed / 241
skipped / 0 failed on net10.0 and on net8.0 (the earlier host-name
failures were an environment issue of the test machine and are gone).
- Builds: `Opc.Ua.Server`, `Opc.Ua.Server.Tests` and
`Opc.Ua.Sessions.Tests` for net48, net8.0, net9.0 and net10.0 and
`Opc.Ua.Server` in Release net10.0: 0 errors, no new warnings (the
CA2213 in `SamplingGroup.cs` and the CA1861/CA2025 in other test files
are on master too).
- `dotnet format` (whitespace, style, analyzers `--severity info`, one
`--include` per changed file): no finding on a changed line. What
remains is on master lines (`SessionManager.cs` 552, 1374, 2205 and
`DependencyInjectionStandardServer.cs` 228) and the line-ending finding
of a macOS checkout.
- Coverage (`.azurepipelines/check-coverage.ps1` against master, only
the session-less tests of `Opc.Ua.Server.Tests` on net10.0): patch
105/116 changed lines (90.5 %). The 11 uncovered lines are Session-bound
lines of `ValidateRequestAsync` and the lockout logger that moved with
the change; the rest of the suite covers them. The project floor is not
meaningful for this subset.
- Fork CI and CodeQL: previous head `0fba9f21d` (master incl. #4674
merged): [CI run
37307824769](https://github.com/biancode/UA-.NETStandard/actions/runs/37307824769)
green (one net48 timing test, `KeepAliveHandlerV2Async`, failed in the
first attempt and passed on the job rerun; the PR does not touch
subscriptions), [CodeQL run
37307829138](https://github.com/biancode/UA-.NETStandard/actions/runs/37307829138)
green. Current head `e9a35980b`: [CI run
37364178979](https://github.com/biancode/UA-.NETStandard/actions/runs/37364178979),
[CodeQL run
37364182697](https://github.com/biancode/UA-.NETStandard/actions/runs/37364182697)
(results pending).

## Design decisions

Marc's answers (2026-10-05) and what the PR does:

1. **Built in** when `AddSessionlessInvocation` was used. Done.
2. **Service-set limit for all paths**: it applies before a
`ValidateSessionLessRequest` handler too. Done, unchanged.
3. **`Bad_ServiceUnsupported`** for a request without a token on a
server without the feature. Done, unchanged. A token that names no
Session stays `Bad_SessionIdInvalid`.
4. **Identity model**: an Access Token in the RequestHeader, validated
as an issued JWT token through the identity registry, anonymous only by
opt-in. Unchanged. The REST authentication is #4674, now in master.
5. **Separate budget** for session-less requests: added, see "Budget for
session-less requests". Open: no limit per identity yet (identities are
claims-defined, so the keys would be unbounded); say if you want one.

## Related Issues

- Fixes #4658
- Relates to #4657 (the REST binding's session-less service set), #4646
(a closed `Session` no longer sends requests without token), #4541
(resource isolation) and #4674 (REST authentication)

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. (Affected
suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered
by the fork CI's Windows net48 legs.)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-06 07:07:52 +00:00
romanett 61edaa1ed0 Remove netstandard2.1 and net472 target frameworks (#4624)
# Description

Drops support for **netstandard2.1** and **.NET Framework 4.7.2**.
Libraries now target `net48`, `net8.0`, `net9.0` and `net10.0`.
`Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` still also target
`netstandard2.0`, but only because the Roslyn source generators load
them. There is no general netstandard2.0 support.

**Preprocessor directives** (112 files)
- Every `#if`/`#elif` mentioning `NETSTANDARD2_1*`, `NET472*`,
`NET_STANDARD_TESTS`, `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING` or
`NETSTANDARD*` was evaluated against the remaining TFM set (net48,
net8/9/10, plus netstandard2.0 inside `Opc.Ua.Types`/`tools`) and
simplified. Dead branches were removed.
- Example: `NETSTANDARD2_1 || NET8_0_OR_GREATER` → `NET8_0_OR_GREATER`.
- Example: `NET8_0_OR_GREATER || NET472_OR_GREATER` → removed (always
true).
- Netstandard-only code was removed: `UnixFileModeShim`, the CRDT
reflection path in `ReplicatedGossipOptions.CreateReaderOptions`, and
the netstandard2.1-only test variants.

**Custom test logic**
- `targets.props` drops the `net472`, `netstandard2.0` and
`netstandard2.1` `CustomTestTarget` profiles, `NetStandardTests` and
`SKIP_ECC_CERTIFICATE_REQUEST_SIGNING`. Supported values are now
`net48`, `net8.0`, `net9.0`, `net10.0`.
- The `NET_STANDARD_TESTS` define is removed from 29 test projects.
- The "build netstandard shells as net10.0" overrides are removed from
18 projects.
- `RestrictForLegacyTfm` now only applies to `net48`.
- `tests/customtest.bat` is updated to the new values.

**Build / packaging / CI**
- MSBuild TFM conditions are simplified: Kestrel package choice, MQTTnet
v4 (net48 only), BouncyCastle, Di/OpenUsd/Vision/Stress test excludes,
and `System.IO.FileSystem.AccessControl` (dropped from Core).
- The MigrationAnalyzer nuspec drops its net472/netstandard2.1 lib
groups. The Azure signing lists are updated.
- `get-ci-matrix.ps1` removes the net472/netstandard test profiles
(including the PR-gating `linux-netstandard2.1`) and the matching build
legs.

**Breaking API change (2.0 preview):** removed
`HttpsTransportListener.IsWssTransportSupported` and
`ReplicatedGossipOptions.IsGossipStateDecodingSupported`. They only
returned `false` on netstandard2.1 and are now always `true`. The test
guards that used them were removed as well.

**Docs**
- NuGet READMEs, `README.md`, `DeveloperGuide.md`, `PubSub.md`, the
certificate/ECC/identity docs and the 2.0 migration guide (plus its
`.agents` skill mirror) are updated.
- The migration guide notes that 4.7.2 consumers must move to 4.8 and
netstandard2.1 consumers to net8.0+.

### Reviewer notes
- The `System.IO.FileSystem.AccessControl` `PackageVersion` pin is left
in `Directory.Packages.props`. It has no direct reference any more, but
transitive pinning is on, so it was kept to avoid shifting resolved
versions.
- The `#if` rewrite was done mechanically by a truth-table evaluator.
The diff in the `.cs` files is limited to directive lines, removed dead
branches, and stale-comment updates.

### Verified locally
- `dotnet build UA.slnx -p:CustomTestTarget=net48`: 0 errors.
- `dotnet build UA.slnx -p:CustomTestTarget=net10.0`: 0 errors.
- Default multi-target builds (no `CustomTestTarget`) of Opc.Ua.Types,
Bindings.Https, PubSub.Mqtt and Redundancy.Server succeeded.
- net10.0 tests:
  - Opc.Ua.Redundancy.Server.Tests: 840/840 passed.
  - Opc.Ua.Types.Tests: 11363 passed, 1 skipped.
  - ModelDependencyScanner tests: 20/20 passed.
- The full test suite was **not** run locally.

## Related Issues

- None

## 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.
- [ ] 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.
- [ ] 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.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:09:49 +02:00
romanett 76e5efde99 Keep GDS roles on hosted logins, add GDS user token policies and startup tasks (#4590)
## Proposed changes

Fixes three problems with a GDS hosted through `AddGdsServer`.

### #4584: admin users lost their GDS roles
The hosted service registered the generic
`UserNamePasswordAuthenticator` and `X509Authenticator` after start.
They replaced the GDS's own authenticators, which grant the roles
`IUserDatabase` assigns, so `RegisterApplication` returned
`Bad_UserAccessDenied` once an `IUserManagement` was registered.

- The GDS builder's `AddDefaultIdentityAuthenticators` now keeps the
GDS's own authenticators for UserName and X.509. The same options
configure them: `EnableAnonymous`, `EnableUserNamePassword`,
`EnableX509`, `UserDatabase` and `UserCertificateTrustList`.
`EnableAnonymous = false` now actually rejects anonymous tokens on the
GDS.
- The generic default registration is left as it is, so a regular server
sharing the container still gets it.
- DI authenticators and augmenters are now staged before start and
registered in `OnServerStarted`, before the endpoints open. They come
after the built-ins, so `AddIdentityAuthenticator<T>()` still replaces a
built-in.
- `ConfigureRoles` now reaches the GDS: its `IRoleManager` is used
through `CreateRoleManager`.

### #4585: GDS offered only Anonymous logins
- Added `GdsServerOptions.UserTokenPolicies`, which is bindable from
`OpcUa:Gds:Server:UserTokenPolicies`.
- When the list is empty, the GDS advertises Anonymous plus UserName. It
leaves out any type the default authenticator options disable. Anonymous
covers the pull flow (§7.6); `RegisterApplication` needs DiscoveryAdmin
or ApplicationAdmin (§6.5.6).
- Corrected the `ConfigureBuilder` doc. The callback runs *before* the
security configuration and the GDS extension are added, not after.

### #4586: GDS never ran startup tasks
- The hosted GDS now runs `IServerPreStartupTask` and
`IServerStartupTask` with the same order, cancellation and failure
handling as `OpcUaServerHostedService`.
- New `IGdsServerBuilder` extensions: `AddStartupTask<T>()`,
`AddStartupTask(callback)` and `AddPreStartupTask<T>()`.
- Scope, as asked in the issue: tasks registered through the GDS builder
run on the GDS only. Plain `IServerStartupTask` and
`IServerPreStartupTask` registrations, and a shared `IRoleManager`,
apply to the GDS only when no regular server (`AddServer`) shares the
container. This keeps a task meant for the regular server from running
twice.
- `StandardServer.AddPreStartupTask` is now `protected internal`, so
server subclasses such as the GDS can stage pre-startup tasks.

## Related Issues
- Fixes #4584
- Fixes #4585
- Fixes #4586

## Types of changes
- [x] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)

## Checklist
- [x] I have read the CONTRIBUTING doc.
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added necessary documentation
(`docs/DependencyInjection.md`, GDS section).

## Further comments
New tests are in
`tests/Opc.Ua.Gds.Tests/Hosting/GdsServerHostedServiceIdentityTests.cs`:
- A UserName login keeps DiscoveryAdmin and CertificateAuthorityAdmin,
with and without `IUserManagement`, and neither generic authenticator
appears in the registry.
- A wrong password is rejected.
- A custom UserName authenticator still wins.
- Disabled Anonymous is rejected and not advertised.
- Default and configured token policies, plus config binding.
- `ConfigureRoles` reaches the server.
- Startup and pre-startup task order, scoping when a regular server is
registered, and a failing task stopping startup.

`GdsIdentityForwardingTests` now expects the extra GDS defaults marker
descriptor.

Local runs: `Opc.Ua.Gds.Tests` hosting fixtures on net10.0 give 74
passed / 0 failed. The GDS server and tests also build for net48, and
`dotnet format whitespace` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:33:24 +00:00
romanett a0d569d756 Fix Server and GDS audit findings, review findings and spec-conformance gaps (#4571)
## Summary

Fixes the findings of a bug audit of `Opc.Ua.Server`,
`Opc.Ua.Gds.Server` and `Opc.Ua.Gds.Client`. Rebased on master after
#4516.

What went in:
- **Audit:** 93 findings, each checked against the OPC UA specification.
- **Code review of those fixes:** 35 further findings.
- **Spec grounding of every fix commit:** 18 conformance gaps, now
closed.

Commits are one per finding, named `Fix <ID>`, `Fix review <ID>` or `Fix
spec gap <name>`. The IDs refer to the audit report (S = Server areas,
G1 = GDS server, G2 = GDS client).

Validation:
- Each finding was traced in code and checked against OPC 10000 (parts
3, 4, 5, 9, 11, 12, 13, 18, 20).
- Every fix commit was checked against the spec text again afterwards: 0
non-conformant, remaining gaps fixed.
- Regression tests were added for most fixes.
- Suites run on net10.0 before the rebase: Server, GDS, History,
Configuration, Features, Core.Security, DI, Redundancy, LDS,
InformationModel, PubSub.Server, Client, Sessions and the three
Subscriptions suites, all green. The same suites on the rebased branch
are still running; results will be posted as a comment.

## Behaviour and user-facing changes

### Secure defaults
- **`Historize()` / `HistorianBuilder`** grant HistoryRead only. History
writes need an explicit `historyAccessLevel: HistoryRead |
HistoryWrite`.
- **File-system mount:**
- Anonymous sessions can no longer create, delete, move or open files
for writing (`Bad_UserAccessDenied`).
  - UserWritable and UserExecutable now reflect the user.
- `AllowAnonymousWrite` restores the old behaviour; the ReferenceServer
sample turns it on.
- **Default PKI root** for the hosted server, hosted GDS, hosted LDS and
`OpcUaApplicationConfigurationProvider`:
- Moved from the shared temp directory to `{LocalApplicationData}/OPC
Foundation/{ApplicationName}/pki`, and startup fails if no per-user
directory exists.
- When certificate stores of an earlier version still exist under temp,
a warning names both paths. Existing deployments that relied on the
default get a new application certificate (and the hosted GDS a new CA)
unless `PkiRoot` is set to the old path.
- **GDS:**
  - `GdsServerOptions.AutoApprove` defaults to `false`.
- The default KeyCredential store grants no roles unless
`AuthorizeRoles` allows them.
- Certificate requests (StartSigningRequest, StartNewKeyPairRequest,
FinishRequest) need an encrypted channel.
- **Hosted server anonymous logins:**
- Anonymous is rejected (`Bad_IdentityTokenRejected`) when the default
authenticator options disable it.
- A custom `AddIdentityAuthenticator<T>()` on its own no longer disables
anonymous.
- With anonymous disabled and no configured token policies, the endpoint
advertises the authenticators' token types instead of Anonymous.
- **Runtime-added roles:** their properties and methods need
SecurityAdmin over an encrypted channel, like the well-known roles.
- **Disabled users:** users disabled through Part 18 UserManagement can
no longer log in through `RegisterDefaultAuthenticators(IUserDatabase)`.

### GDS server (Part 12)
- **Certificate requests:**
- FinishRequest / ReadRequest with another application's RequestId
returns `Bad_InvalidArgument`.
  - Every Start*Request returns a new RequestId.
- A CSR without the ApplicationUri is rejected
(`Bad_CertificateUriInvalid`).
- An abstract or unsupported certificate type is rejected at Start
(`Bad_InvalidArgument`).
- **Private-key password:** no longer persisted in the JSON applications
database. A password saved by an earlier version is read into memory
once and dropped on the next save.
- **CheckRevocationStatus:** validates offline against the GDS's own
trust lists and CRLs. It returns Good for good GDS-issued certificates
and `Bad_CertificateInvalid` for input that is not a certificate.
- **Certificate-group trust lists:**
- Writes need CertificateAuthorityAdmin; SecurityAdmin and application
admins are read-only.
  - Writes are validated with the Part 4 certificate validation.
- **AuthorizationService:**
- The legacy RequestAccessToken applies `AccessControl` and issues the
token for the session identity.
- StartRequestToken passes the session identity to every identity-aware
provider.
  - Unknown ResourceIds return `Bad_NotFound`.
- Expired token requests and refresh tokens are pruned, with at most 10
000 pending requests (`Bad_TooManyOperations`).
- **KeyCredential:** requests with PublicKey + SecurityPolicyUri are
rejected with `Bad_SecurityPolicyRejected` (secret encryption is not
implemented); a PublicKey without SecurityPolicyUri is rejected with
`Bad_InvalidArgument`.
- **Audit events:** access-token audit events no longer contain the
password or token data.
- **RegisterTickets / UnregisterTickets:** need the RegistrarAdmin role
when called from a session.
- **Managed application nodes:** moved from namespace 0 to their own
namespace (`ManagedApplicationsNamespaceUri`); the NodeId is the
ApplicationUri, and the mandatory child properties exist.
- **Hosted GDS:**
- New `GdsServerOptions.CertificateGroups`
(`GdsCertificateGroupOptions`), bindable from configuration.
  - The mandatory DefaultApplicationGroup is always present.
  - The default CA subject uses the resolved application name.
- **JSON applications database:**
- A corrupt file throws instead of being silently replaced by an empty
database.
- Saves are atomic and write in place on bind mounts; on net8+ builds
they also follow symlinks and keep the Unix file mode.
  - Issued certificates and trust-list links survive a reload.

### GDS client
- **Trust-list download:**
  - Reads until an empty chunk (Part 20 short reads).
- A size-limit rejection of the final end-of-file read, as sent by
released servers, counts as end of file.
- **Default certificate group:** a null group means
DefaultApplicationGroup, and vendor groups in other namespaces are no
longer mistaken for well-known groups.
- **Status codes and errors:**
  - Missing directory proxies report `Bad_ServerNotConnected`.
- Short or wrongly typed method outputs throw `BadUnexpectedError` /
`BadTypeMismatch`.
- **GdsAccessTokenProvider:** no longer caches a failed client task, and
one caller's cancellation no longer fails other callers.

### Trust lists, push and ConfigurationFile (Part 12 / Part 20)
- **AddCertificate:**
  - `IsTrustedCertificate=false` returns `Bad_CertificateInvalid`.
- Added certificates are validated (Part 4), and a certificate revoked
by a current CRL returns `Bad_CertificateRevoked`.
- **CloseAndUpdate:** validates the uploaded list against its own
certificates and CRLs, not the stores it replaces. Validation failures
are no longer recorded in the server's Rejected store.
- **RemoveCertificate:** removing a trusted or issuer CA that other
certificates still need returns `Bad_CertificateChainIncomplete`.
- **Handles:**
  - Several read handles may be open at once.
- Any open while a write handle exists returns `Bad_NotReadable` /
`Bad_NotWritable`.
  - A write open while any handle is open returns `Bad_NotWritable`.
  - Handles of closed sessions are released.
  - Idle handles close after ActivityTimeout (default 60 s).
- **Status codes:**
- Read clamps to the remaining bytes and no longer enforces the size
limit.
  - Oversized Write / AddCertificate returns `Bad_RequestTooLarge`.
  - Unsupported open modes return `Bad_NotSupported`.
- **ConfigurationFile.CloseAndUpdate:**
  - Empty Targets return `Bad_InvalidArgument`.
- The ConfigurationUpdatedAuditEvent is raised with the configuration
owner as source.
- With the new `IApplicationConfigurationFileTargetProvider`, providers
receive the Targets, a bad target returns Uncertain with per-target
results, and session-interrupting updates are applied after
RestartDelayTime.
- **Default namespace permissions:** namespaces registered after the
first permission lookup now get their NamespaceMetadata default
RolePermissions and AccessRestrictions (no stale negative cache).

### Sessions, identity and roles (Part 18)
- **Session timeouts:** a failure closing one timed-out session no
longer stops the session-timeout monitor.
- **ChangePassword:** an empty old password returns
`Bad_IdentityTokenInvalid`.
- **Removing or disabling a user:** closes only that user's
UserName-token sessions.
- **Role configuration changes:** roles granted by an `ImpersonateUser`
mapping are kept.
- **X509Subject rules:** a certificate subject value containing a quote
never matches an X509Subject rule.
- **`ImpersonateUser` callback:** no longer runs under the global
session lock.
- **Session diagnostics:** LocaleIds are kept as the client sent them,
while null LocaleIds are ignored for translation.

### Subscriptions and monitored items (Part 4)
- **Acknowledgements:** a failing acknowledgement returns a per-entry
Bad status instead of failing the whole Publish.
- **`DiscardedMessageCount`:** counts messages dropped from the
retransmission queue and unsent overflow messages.
- **Durable subscriptions:** transferring a restored anonymous
subscription works (no NullReferenceException).
- **Transferred subscriptions:** sampling and permission checks use the
item's current owner session.
- **Access denied:** a monitored item whose user may no longer read the
attribute gets a `Bad_UserAccessDenied` notification instead of going
silent. Non-Value attributes need Browse permission (RolePermissions
needs ReadRolePermissions), and User* attributes are evaluated per
subscriber.
- **ModifyMonitoredItems:** a SamplingInterval of -1 uses the
subscription's publishing interval.
- **AggregateFilter priming:** needs HistoryRead / ReadHistory
permission and is bounded (100 000 values, bounded page size).
- **Resource leak:** event monitored items no longer leak a consumer
task per create/delete cycle.

### Node management services (Part 3 / Part 4)
- **Reference types:** AddNodes, AddReferences, DeleteReferences and
Browse reject ids that are not ReferenceTypes with
`Bad_ReferenceTypeIdInvalid`. AddNodes, AddReferences and
DeleteReferences also reject abstract ReferenceTypes; Browse still
accepts them (e.g. `HierarchicalReferences` with includeSubtypes).
- **Concurrent AddNodes:** duplicate BrowseNames under one parent get
`Bad_BrowseNameDuplicated`.
- **TranslateBrowsePathsToNodeIds:**
- Requires Browse permission on intermediate nodes and final targets
(RolePermissions and AccessRestrictions).
  - A long RelativePath no longer rejects the whole request.
- **SemanticChangeEvent:**
- Raised once per actual change of a semantic property, whether or not
it is monitored.
  - Also raised for properties with the SemanticChange access-level bit.
  - Reports the owning node's TypeDefinition as AffectedType.
- **Localized diagnostics:** operation-level text is translated when
OperationLocalizedText is requested.

### History and aggregates (Part 11 / Part 13)
- **DeleteRawModified:** an unspecified or reversed range returns
`Bad_InvalidTimestampArgument`, and StartTime == EndTime deletes the
value at StartTime for every provider.
- **ReadAtTime fallback:** reads only the values around each requested
time. A time whose bound is not found within the search limit returns
`Bad_BoundNotSupported` instead of failing the request.
- **ReadProcessed:** a ProcessingInterval shorter than one tick returns
`Bad_AggregateInvalidInputs`.
- **Delta / DeltaBounds:** widen to the next larger signed type when the
result does not fit the source type (e.g. a decreasing unsigned counter,
or an Int16 overflow).
- **Range / Range2:** return the source data type.

### Alarms and fluent API (Part 9)
- **Suppression:** an alarm stays suppressed while any suppression group
is active, and re-evaluation without a state change does not move
TransitionTime.
- **`MonitorVariable(variable)`:** sets SourceNode, SourceName and
InputNode to the variable and makes it a ConditionSource (HasCondition
to the alarm, HasEventSource from the object).
- **Fluent input/output arguments:** InputArguments / OutputArguments
added through the fluent API to a registered method are readable.
- **Root notifiers:** a failed root-notifier registration no longer
leaves an orphaned event worker.
- **State-machine handlers:** exceptions are logged at Error.

### File system (Part 20)
- **Delete / MoveOrCopy:**
  - Only act on direct children of the directory.
- A NodeId whose kind does not match the entry returns `Bad_NotFound` (a
file NodeId can no longer delete a directory).
- Copying a directory into its own subtree returns
`Bad_InvalidArgument`.
- **Locking:**
- An open file, or a directory containing one, cannot be deleted or
moved (`Bad_InvalidState`).
  - The check is atomic with opens.
- While a Delete or Move runs, read opens get `Bad_NotReadable` and
write opens `Bad_InvalidState`.
- **Open:**
- Reserved mode bits or EraseExisting without Write return
`Bad_InvalidArgument`.
  - Read+Write returns `Bad_NotSupported`.
  - Read while a writer is open returns `Bad_NotReadable`.
- **Client `FileSystemErrors`:** maps `Bad_NotReadable` to
`IOException`.
- **RuntimeNodeSet:** well-formed NodeSets with a stray `Model` element,
or two models in one file where one requires the other, now load.

### Server core, hosting and lifecycle
- **`SessionEstablishmentQueueLimit` > 0:**
CreateSession/ActivateSession now queue for a permit. The permit is
acquired before the request is registered, and a waiting handshake
releases its worker through the request-parking policy.
- **Node manager lifecycle:**
  - Add, reload and remove no longer wait for parked Publish requests.
- A Migrate reload racing with CreateMonitoredItems no longer blocks
later lifecycle operations.
  - Late items on retired or removed managers get `Bad_NodeIdUnknown`.
- **HistoryUpdate:** a batch mixing data and event details is bounded by
both limits.
- **Server stop/start:** stopping the server disposes the configuration
watcher and certificate subscription; starting recreates them.
- **Monitored-item queue factory:** a factory supplied by the caller or
the DI container is no longer disposed by the server.
- **Audit events:** each certificate validation error raises its own
AuditCertificate event. A failed OpenSecureChannel without a service
result is audited as failed.

## New public API (additive)
- `IQueuedSessionEstablishmentLimiter`
- `StandardServer.OwnsMonitoredItemQueueFactory` (protected virtual)
- `DefaultPkiRoot.Get` (Opc.Ua.Configuration)
- `TrustList.SetCertificateValidation`
- `NamespaceMetadataRegistry.Invalidate`
- `IApplicationConfigurationFileTargetProvider` and
`ApplicationConfigurationUpdatePlan`
- `GdsServerOptions.CertificateGroups` / `GdsCertificateGroupOptions`
- `InMemoryKeyCredentialRequestStore.AuthorizeRoles`
-
`DefaultManagedApplicationsNodeManager.ManagedApplicationsNamespaceUri`
- `AuthorizationServiceManager.RequestAccessTokenAsync(...,
IUserIdentity?, ...)`
- `AllowAnonymousWrite` on the file-system node manager and its factory
- `IFileSystemHost.TryBeginMutation` / `EndMutation`, replacing
`HasOpenHandles`

## Not fixed / known limitations
- **KeyCredential:** secret encryption with the client's PublicKey
(requests needing it are rejected).
- **CSR SAN restriction:** restricting subject / SAN values copied from
a CSR (CA policy).
- **Ticket signatures:** ticket signature verification (needs a
trusted-signer configuration).
- **FileType Read+Write open:** needs a read/write stream contract in
`IFileSystemProvider`.
- **Custom `IUserDatabase` implementations:** the disabled-user check
still scans all users per login. The built-in stores do a single lookup.
- **64-bit Delta precision:** limited by the calculator's double
arithmetic.

## Tests with changed expectations
Existing tests were updated where they asserted the old behaviour:
- File-system status codes
- Trust-list handle eviction
- `DiscardedMessageCount`
- Localized diagnostics bits
- Delta on unsigned sources
- SourceNode of fluent alarms
- ReferenceServer transfer permission test (now expects
`Bad_UserAccessDenied` notifications)
- `UserDeactivationShutdownReviewTests` (the revoked session is a
UserName login)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:26:05 +02:00
Marc Schier c4e0721a29 Add a documentation learning path and fix issues found while verifying it (#4555)
## Summary

### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.

### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.

### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.

## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.

Review and validation artifacts are maintained outside the repository.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-28 19:19:39 +00:00
Marc Schier f9892020a3 Add configurable server resource isolation (#4541)
# Description

Add configurable server resource isolation on top of the merged #4522
foundation.

- Coordinate connection, handshake, retained-message,
session-establishment, queue, execution, request-cost and parked-request
limits through one server-scoped provider.
- Keep configured startup, continuity/reconnect, control and
trusted-owner reservations inside existing totals. Shared capacity
remains usable up to explicit owner ceilings.
- Use authoritative Session membership for reassembly continuity
capacity without granting unrelated resource or rate privileges.
Preserve explicit owner mappings and bounded tracking.
- Cache immutable classification evidence while retaining current
Session and classifier checks.
- Support weighted request ordering or FIFO independently of resource
accounting. Installed providers enforce decoded-request limits in both
modes.
- Preserve fail-fast reassembly, valid Abort handling, fixed deadlines,
active-work protection, reverse-connect handoff and bounded OpenAPI
processing.
- Support custom-handler parking through `IRequestParkingPolicy`,
`WithRequestParking(...)`, and `RequestLifetime.ParkSink`. Parking
releases execution capacity only, keeping retained-data and parked
charges until completion. Built-in Publish and the global opt-out remain
unchanged.
- Provide direct, fluent and DI configuration, public incoming-request
parking support, effective startup limit logs and user-facing
documentation.

Balanced remains the default. SharedOnly without an explicit provider
retains shared behavior for intentionally incompatible/unlimited
configurations. Reservations are finite and are not one full-message
allowance per Session.

## Related Issues

Builds on merged #4522. Fixes #4554.

## Review and CI corrections

All three previously missed findings in [review
5326734718](https://github.com/OPCFoundation/UA-.NETStandard/pull/4541#pullrequestreview-5326734718)
are addressed in `9e25e4ecc`:

1. A custom FIFO provider receives RequestQueue, RequestQueueBytes,
RequestExecution and ParkedRequest accounting. A blocked head cannot be
overtaken.
2. `IResourceIsolationReassemblyProvider` replaces the sessionless
threshold independently of `UseFairScheduling`. The aggregate byte
ceiling remains enforced.
3. Runtime `ReservedReassemblyBytes` includes all configured protected
stage floors, including ControlReserved.

The review's two already-resolved changes remain intact.

The CI collection race was fixed at its source in `e1a994a4b`:
`ArrayOf.ToArrayOf` treats a separately read collection count as an
initial capacity hint and grows/trims for the actual enumeration.
Deterministic tests cover growth, shrinkage and stale-zero counts.

The subsequent recovery-test race was fixed in `05753ce77`: fake time
advances only after a gated keepalive response has been processed and
reset its timer, not merely after request arrival. Recovery-order and
timeout assertions remain intact, with additional response-state
assertions. Instrumentation reproduced the original failure twice; both
fixed variants passed 400 executions per framework.

CodeQL alert 388 was verified and dismissed as a false positive. Startup
logs contain aggregate numeric capacities, not credentials or caller
identities.

## Validation

| Scope | Result |
| --- | --- |
| Latest automated checks at `05753ce7775a58801b4ba9d8f6d7781b709d0b89`
| 167 successful, one package-publication check skipped, no failures |
| Remaining external prerequisite | `license/cla` pending: Contributor
License Agreement is not signed yet |
| FIFO/weighted queues, custom parking and server-channel selection |
175 passed on each net10.0/net48 |
| Isolation, configuration and parking-hosting selection | 259 passed
net10.0; 257 passed net48 |
| ArrayOf conversion tests | 73 passed on each net10.0/net48 |
| Recovery-audit and keepalive fixtures | 40 passed on each
net10.0/net48 |
| NativeAOT smoke from preceding updates | Published and executed
successfully |

No tests, assertions, coverage thresholds, or required checks were
weakened. The remaining CLA prerequisite must be completed through [CLA
Assistant](https://cla-assistant.io/OPCFoundation/UA-.NETStandard?pullRequest=4541).

This is not CTT certification or an unconditional network-DDoS,
whole-process-memory or fleet-wide guarantee.

## 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.
- [ ] 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.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
2026-09-27 21:11:52 +02:00
Marc Schier 57c0a32ed4 Fix incomplete-message buffering and channel cleanup (#4522)
# Description

Fix incomplete-message buffering, channel cleanup, session resource
accounting, and enforcement of existing transport admission settings.

- Share a targeted `ChunkReassemblyBudget` across server listeners. The
reference default is 64 MiB, with a 32 MiB global occupancy threshold
for sessionless intermediate chunks. This is priority headroom for
activated sessions, not a guaranteed bootstrap partition. Single/final
chunks and unrelated response/client buffers remain outside this budget.
- Retain precise incoming/final payload quotas, valid Abort handling
without channel teardown, terminal late-final rejection, and
exactly-once buffer/reservation cleanup.
- Use one assembly timer per quota/clock scope, asynchronous cleanup, a
fixed deadline, a 30-second fallback for non-positive lifetimes, and
best-effort ERR `BadTimeout` before closure.
- Apply negotiated TCP/WebSocket/Pipe receive limits and size pipe
rentals to actual chunks.
- Add the optional `ISessionBindingProvider` and immutable
`SessionBindingContext` seam. Managed membership counts distinct
committed sessions, not responses, and handles reactivation, transfer,
closure, expiry, restoration and shutdown. Read-only snapshots require
token/channel/security validation and never replace service
authorization.
- Wire default and explicitly injected binding providers through raw
TCP, Kestrel and UACP WebSockets; preserve existing callback/manager
interfaces and direct construction.
- Enforce existing `ConnectionRateLimiter` and channel capacity settings
on Kestrel TCP and UACP WebSocket admission, with physical-transport
leases surviving reverse handoff and releasing on cancellation, close,
failed attachment and stop. HTTP/JSON requests are not treated as UASC
channels.
- Add regression coverage and documentation. Stabilize the CI
cancellation test by waiting for the resolver to enter before canceling.

This is the foundation only. New Balanced/FairShare/TrustedReservations
policies, tenant isolation and guaranteed bootstrap reserves belong in
the separate stacked follow-up, not this PR.

## Related Issues

Fixes #4521.

## Validation

| Scope | Result |
| --- | --- |
| Core transport, budget, binding-context and DI tests, net10.0 | 717
passed |
| Core transport, budget, binding-context and DI tests, net48 | 664
passed, 6 platform-specific skips |
| Session bindings, transfer/security/expiry/restore and hosted DI,
net10.0 | 69 passed |
| Same focused Server suite, net48 | 69 passed |
| ServerRedundancyHandlerTests, Release/net48 | 39 passed |
| ConsoleReferenceServer net10.0 | Build succeeded, zero warnings/errors
|
| Server and HTTPS binding netstandard2.1 | Both builds succeeded, zero
warnings/errors |

The new admission fixtures also passed five repeated runs after
correcting asynchronous connection-context disposal. Full-solution
validation, coverage comparison and CI for this update remain pending.

## 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.
- [ ] 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.
- [ ] I have addressed **all** PR feedback received.
2026-09-24 20:12:55 +02:00
Marc Schier 138086863b Fix audit findings and review regressions across the OPC UA stack (#4503)
## Scope

Audit fixes and review follow-ups across the OPC UA Core, Client,
Server, transport, and redundancy components. This includes implemented
Tier 1, Tier 2, and Tier 3 work; it does not claim that every item in
the 244-finding audit backlog is complete.

The review batch fixes mandatory full-key record protection and owned
plaintext buffers; transactional pending-certificate claims;
shared-channel nonce and certificate ownership; discovery and
token-policy validation; reconnect/callback ordering without changing
unbounded buffering defaults; server permissions, filters,
monitored-item lifetime, and alarm topology; historian paging,
retention, and annotations; atomic user/password metadata; and canonical
file identities with bounded alias and role refresh workers.

## Review-batch verification at `ab686ccae`

The following results come from fresh test builds using `dotnet test`,
`--no-restore`, `-m:1`, and an explicit `CustomTestTarget`. They replace
the earlier description's library-build/`--no-build` totals, which did
not establish the state of the merged test sources.

| Scope | Target | Passed | Skipped |
|---|---|---:|---:|
| Core review regressions | net10.0 | 827 | 0 |
| Client and lifecycle regressions | net10.0 | 630 | 0 |
| Server review regressions | net10.0 | 1970 | 0 |
| Redundancy Server / Client / PubSub | net10.0 | 276 / 10 / 11 | 0 |
| Sessions / file-and-alias / streaming / HTTPS-WSS integration |
net10.0 | 8 / 21 / 30 / 20 | 0 |
| Core review regressions | net48 | 786 | 39 |
| Client recovery and policy regressions | net48 | 54 | 0 |
| Server ownership, aliases, queues, and user-state regressions | net48
| 137 | 0 |

Every listed run exited successfully. The 39 net48 skips are existing
platform/capability cases. New key-agreement tests assert
unavailable-policy behavior on unsupported targets rather than adding
skips or pretending those targets performed an unsupported handshake.
Additional exact node/permission/filter/alarm checks passed 13 Core and
46 Server cases.

The PR-scoped formatter verification covers 26 projects and 389
non-generated C# files. A Roslyn syntax audit covers 53 changed
interfaces and 263 members, with no missing
parameter/type-parameter/return contracts and a 120-column limit for
their declarations and documentation. No full-solution,
all-target-framework, or NativeAOT matrix is claimed from these local
runs; the new CI run must establish its own result.

## CI follow-up at `e1bc3ac3b`

The follow-up fixes the failures reported by Azure build 18400. TLS
certificates retain configured DNS names, including mixed-case URI
schemes, without relaxing hostname validation. Reverse-WSS fixtures use
the DNS identity in the client certificate. Reconnect completion is
published only after cycle cleanup, so a subsequent reconnect cannot
join an old completed cycle and retain old certificates.

Certificate-alarm roots now support event subscriptions. The pump sample
has one continuous notifier path, with no duplicate shortcut. Historian
and aggregate fixtures share the provider's retention clock. Alarm
collection lets the server expire idle Publish requests rather than
canceling only the client wait. The corrected server, WoT, and
managed-session fixtures exercise valid requests and initialized
lifecycle contracts. Downlevel crypto tests preserve real credential and
key-ownership assertions without enabling unsupported raw-ECDH policies
or adding skips.

Fresh local evidence after integration includes:

| Scope | Target | Passed | Skipped |
|---|---|---:|---:|
| Client lifecycle / channel ownership / builder fixtures | net10.0 and
net48, each | 96 / 103 / 123 | 0 |
| Combined server lifecycle, events, aggregates, crypto, and contracts |
net48 | 358 | 0 |
| Full lifecycle plus certificate-alarm and aggregate fixtures | net10.0
| 116 | 0 |
| Final server-contract and history-routing delta | net10.0 and net48,
each | 84 | 0 |
| Subscription / XRegistry / WoT event regressions | net10.0 | 7 / 2 / 3
| 0 |
| Full Sessions project | net10.0 | 780 | 379 |
| Full ComplexTypes project / downlevel resolver fixtures | net10.0 /
net48 | 2854 / 15 | 0 |
| Full WoT project / downlevel structured-write fixtures | net10.0 /
net48 | 1496 / 27 | 0 |
| TLS domain-name regressions | net10.0 and net48, each | 21 | 0 |

All listed runs exited 0. The Sessions skips come from existing endpoint
and policy capability guards; they are not counted as passes. No skip
was added. Worker validation also covered full DI (426 passes), full
History (540 passes, 21 existing skips), 38 exact downlevel
event/history failures, and 60 repeated certificate-rotation race cases.
The final crypto fixtures passed 73 cases on each target with the
unchanged leak detector; a temporary premature-disposal mutation made
all three borrower tests fail and was then removed.

All 29 C# files in this follow-up pass included-file formatting at info
severity across 12 projects. The complete PR interface audit now covers
54 interfaces and 271 members with zero missing contracts or line-width
findings. No full-solution or NativeAOT matrix is claimed. New-head CI
has started and remains authoritative for the remote result.

## Test-result gate follow-up at `4b6e1203a`

Azure build 18401 reported the net48 Client job as successful despite a
test-host crash after only 264 passing tests. Its old evaluator counted
per-test failures but ignored the failed run verdict. This was a partial
run, not a benign exit after the suite had completed.

The shared result evaluator now rejects aborted, errored, incomplete,
malformed, or inconsistent TRX reports. Each file must describe a
completed run, so a passing file cannot mask another file's failure. It
retains support for completed runs followed by a benign nonzero host
exit, existing skipped-result conventions, and no-TRX MTP jobs.

All 78 regression cases pass after integration using Windows PowerShell
5.1. Real completed Sessions and Client reports also pass. The
process-test fixture follows the existing NET10_0-only tools-test
pattern; its net48 build is not counted as a test run. Formatting passes
for the added test file.

The Client host crash itself has not reproduced locally. The isolated
fixture passed 59 tests, and two fresh full fast-CI selections each
completed with 2,392 passes and three existing skips, including a run
with crash collection enabled. The CI merge tree is identical to the
tested source tree. No speculative runtime change was made, and the
original partial CI run is not claimed as a full-suite pass. Azure build
18402 has now completed the full net48 Client run: 2,392 passed, three
existing console skips, and a successful test-host exit. The corrected
evaluator independently validated outcome=Completed. Across the matrix,
119 test-project jobs succeeded; Sessions net10.0 was canceled when the
remote provider deprovisioned its runner, so the overall Azure pipeline
was canceled. That cancellation is not claimed as a code failure or a
successful run. CodeQL and three Docker image jobs were still running at
the latest check. The migration discovery failure remains separate.

## Documentation follow-up at `57ed02bea`

The environment blocker has been removed by the maintainer. The three
bundled migration references now exactly match the canonical documents
after deterministic link rewriting; all 15 bundled documents pass
`validate-migration-plugin.ps1`. The canonical migration prose was not
rewritten. `FileSystemClient.md` now documents that extended Windows
roots preserve literal trailing-dot names, completing the filesystem
review follow-up.

The filesystem thread has been replied to and resolved after
publication. The review pass now has 108 of 110 threads resolved. The
two explanation-only threads remain open as requested; no unfinished
change request is being represented as resolved.

## Cross-platform follow-up at `0d9d936a8`

With migration discovery restored, the Linux and macOS test matrix
exposed two stale durable-subscription expectations. The expected value
83,442 encoded the old 32-bit overflow when converting 3,600 hours to
milliseconds. The existing overflow-safe server correctly returns
14,400,000 lifetime intervals at a 900 ms publishing interval. Only
those two expected constants changed; production behavior,
negotiated-hour checks, and all other assertions remain unchanged.

The exact local lifetime selection reproduced both failures before the
correction. The full durable suite now passes 36 cases with 11 existing
skips on each of net10.0 and net48. All four lifetime cases executed and
passed on both targets, and included-file info-severity formatting
passes.

## Session shutdown follow-up at `8daf7f091`

A late net48 Sessions failure exposed a shutdown race. Canceling an
in-flight keepalive read can produce a transport
`ServiceResultException`. The retiring worker treated its own
cancellation as proof that the peer was unavailable, so `CloseAsync` or
`DisposeAsync` could skip the server-side `CloseSession` request. The
worker now ignores that service error only when its own token is
canceled; genuine peer errors still update liveness.

The full integration fixture reproduced the original error-code
mismatch. Four deterministic mock-transport cases then produced three
failures and one passing genuine-error control before the fix. They
verify successful close, returned server error, asynchronous disposal,
and unchanged behavior for a real keepalive failure. Fresh related runs
pass 75 client and 101 integration cases on each of net48 and net10.0,
with no skips. Ten repetitions of the formerly flaky full net48 fixture
pass all 1,010 cases. All three changed files pass info-severity
formatting, and temporary diagnostic instrumentation was removed.

## Atomic listener snapshot follow-up at `82dc22b5d`

Ubuntu Core CI exposed an unsafe formatter rewrite in TCP listener
disposal: `ConcurrentDictionary.ToArray()` had become a collection
spread. A concurrent channel removal between the spread's count and copy
operations could leave a default entry, causing a null dereference
during disposal. The listener now uses the dictionary's atomic `Values`
snapshot and disposes those channels outside the listener lock.

Deterministic regressions close zero, one, or all channels at the copy
boundary. Before the fix, the one/all-removal cases reproduced the exact
null dereference; the unchanged-count control passed. All 23
admission/listener cases now pass on net10.0 and net48, and the full
fast Core net10.0 suite passes 5,267 cases with 88 existing platform
skips. Both changed files pass info-severity formatting. A bounded audit
of the formatter's other snapshot rewrites found no further verified
unsafe concurrent-registry copies.

## Shutdown-test observation follow-up at `58403ee8b`

The net48 shutdown-order test raced its own observation: it read
`m_publishWorkerTask` after starting shutdown, which can legitimately
drain the worker and clear that field immediately. The test now captures
the worker before shutdown starts and confirms it is initially running.
It still requires the worker to finish while shutdown remains blocked by
the held manager semaphore. Production behavior is unchanged.

A temporary scheduling perturbation reproduced the exact null-field
failure. The corrected capture passed 100 perturbed iterations; both the
temporary repeat and yield were then removed. The final 126
subscription/lifecycle cases pass on each of net48 and net10.0, and
formatting passes. A complete local Sessions net10.0 run also passed 780
cases with 379 existing capability skips in 7m20s after the earlier
keepalive correction.

## Retirement and alias-startup follow-up at `1a048ff39`

The shadow-generation test now observes its documented asynchronous
cleanup through the existing bounded helper. Removing the replacement
does not join a background drain that already claimed the retired
generation. The test still checks live-registration removal immediately
and requires the old node to disappear; no timeout or production
lifecycle behavior changed. All 81 lifecycle cases pass on each of
net10.0 and net48.

Initial alias-node materialization now retries one timed-out, read-only
snapshot query before creating nodes for that root. The retry is logged
through source-generated logging. Each query retains the original shared
100 ms deadline; repeated timeouts, other errors, and cancellation still
abort startup. Client searches and live refresh do not gain this
immediate retry. Fault-injection tests cover success after one timeout,
a persistent timeout, a different store failure, and cancellation. All
187 related alias/hosting cases and the original certificate-rotation
integration case pass on each target; the positive test records exactly
one retry warning. The alias documentation now accurately describes the
shared request budget.

All changed C# files pass formatter checks, and the complete
54-interface/271-member documentation audit passes. Fresh CI is running
on `1a048ff39`; the previous completed matrix exposed only the two
failures corrected in this batch and their dependent summaries.

## Sample endpoint allocation follow-up at `d08a94925`

The strong-cluster test harness allocated OPC UA and Raft endpoints in
separate released-port batches. Uniqueness was enforced only inside each
batch, so the same OS-assigned port could appear in both protocol
groups. The harness now requests one distinct batch for both groups and
splits it before launching any replicas.

Deterministic one- and three-replica allocation cases reproduced
overlapping groups with the old plan and pass with the corrected plan.
The complete 17-case short-haul sample suite passes without skips; all
failover, historian-continuation, and service assertions remain
unchanged. Both changed files pass info-severity formatting. This
prevents intra-cluster self-collisions but does not claim to reserve
released ports against unrelated external processes.

## Code-review follow-up at `085fd9636`

A review of the complete branch diff found five defects this PR
introduced. All are now fixed, each in its own commit.

`RelatedTo`'s recursive-to-iterative rewrite defaulted the optional
`includeTypeDefinitionSubtypes` and `includeReferenceTypeSubtypes`
operands to `false`. Part 4 §7.7.4 specifies both default to `true`, so
a filter that omitted them silently stopped matching subtypes.
Dynamically created key-credential nodes derived their namespace from
the containing folder, which is the standard `i=18155` node in namespace
0 — the namespace reserved for the OPC UA standard address space; they
now resolve the server-owned namespace and fail with `BadInternalError`
rather than guessing index 1. The retained audience-less
`CreateTokenData` overload produced tokens that verification always
rejects, so it is now `[Obsolete]` and throws instead. A failed
reference to a remote parent left the new node registered with a
one-sided inverse reference, and is now rolled back. An existing `.pfx`
is restricted before it is rewritten, rather than after its new key
bytes are on disk.

One further reported finding was rejected after investigation.
`JwksIssuerKeyResolver` deliberately does not dispose retired key
snapshots: `RetiredKeysAreCollectibleWhileResolverRemainsAliveAsync`
proves the resolver retains no retired snapshot, and
`ReturnedKeysSurviveRepeatedRefreshAndResolverDisposalAsync` requires
already-returned keys to keep verifying across refreshes and resolver
disposal. Adding disposal would have broken that tested contract, so the
exploratory change was reverted.

Validated on net10.0 and net48: 256 Core and 633 Server cases pass, with
one pre-existing net48 skip. The build reports zero compiler and
analyzer diagnostics, all eight included-file info-format checks pass,
the contract audit covers 58 interfaces and 310 members with no
findings, and the 15 bundled migration documents validate. All 122
review threads are resolved; the key-credential namespace thread was
reopened to retract an earlier incorrect answer and then resolved
against the real fix.
## Review-body follow-up at `da0d59cfb`

The re-review at `8daf7f091 + 82dc22b5d` listed four items in its
summary that had no inline thread of their own, so they had not been
picked up. All four are now fixed.

Both event-dispatch permission gates had been tightened from not-Bad to
`IsGood`, which silently dropped events whose permission verdict is
Uncertain; the data-change path in the same file still treated Uncertain
as deliverable, so the two had diverged. `EventSourceRegistry` only
cleared `ConsecutiveFailures` when a source was deactivated, so a source
that had backed off to the 30-second cap kept that delay for the rest of
the process even after starting successfully.
`UserNameIdentityTokenHandler` built a temporary issuer chain whose
`Add` calls take independent certificate references, and never released
them.

The fourth was the receiver-side sequence rollover, which the reviewer
had correctly re-flagged as still open despite its thread being
resolved. `m_sequenceRollover` was set permanently, so the second legal
wrap on a long-lived channel was rejected and faulted the channel along
with the transport-reconnect OPN. OPC 10000-6 does not cap a
SecureChannel at one wrap. The flag is now cleared once an accepted
number has traversed the normal range, which keeps rejecting a replayed
low number that never left the rollover window.

Validated on net10.0 and net48: 636 / 593 Core cases and 135 / 134
Server cases pass, including the new
`UaSCBinaryChannelSequenceRolloverTests` and the Uncertain-permission
delivery cases. The build reports zero compiler and analyzer diagnostics
and the changed files pass included-file info-format checks on both
targets.

The previous head `085fd9636` was fully green: 279 checks passed and
Azure build 18414 succeeded with 137 of 137 jobs, after the platform
automatically retried a job whose NuGet tool download had hit a
transient TLS error.
## Reported issues #4508 and #4509 at `6ad1ecf16`

**#4508 (V2 subscription recreate deadlocks against a faulted transport)
was not yet fixed and is now closed by `bac260a06`.** Both drain sites
the report identifies were still unbounded. A publish worker that
started its request before the quiesce parks inside the service call on
the channel ready gate while holding an active publish request; the
recreate that would make the channel ready is the very operation
awaiting the drain, so the wait never ends.

The drain now cancels in-flight attempts through a per-attempt token
linked to the worker token. An aborted worker rolls its acknowledgements
back, releases the request and returns to the paused park instead of
terminating, so it resumes when the caller leaves the quiesce. Publish
ingress stays quiesced for the caller's operation, so
`DropPendingForSubscription` keeps the guarantee the report asks to
preserve and no blind timeout is added. The new regression parks a
worker on a cancellable gate that never completes on its own: it
reproduces the hang for the full 30-second budget before the change and
passes after it.

**#4509 (identity provider fails the initial connect) was already fixed
for its primary symptom.** `HandleConnectAsync` materializes the
provider's identity through `CreateIdentitySelectionContext` before the
first `CreateAsync`, so Anonymous is no longer substituted against a
UserName-only endpoint. `6ad1ecf16` adds the two remaining details from
the report: the enabled-policy set now includes the negotiated channel
policy, so a client cannot reject the policy it already accepted as
`NotEnabledByClient`; and when discovery has not populated the
endpoint's token policies the connect keeps the configured identity so
the attempt fails at the transport layer for the reconnect policy to
retry, instead of failing at the identity layer.

Validated on net10.0 and net48: 477 / 473 client session and
subscription cases pass, the build is free of compiler and analyzer
diagnostics, and the changed files pass included-file info-format checks
on both targets. The full `Opc.Ua.Sessions.Tests` suite also passes
locally on net10.0 with 782 passed and 379 pre-existing capability
skips.

GitHub checks are green on this head: Build and Test .NET 10.0, Docker
Sample Images CI, and NuGet Publish all succeeded. Azure build 18417
completed 134 of its 135 jobs successfully; its net10.0 Sessions job was
cancelled by the hosted-agent provider ("Received request to
deprovision"), the same infrastructure flake that hit build 18409
earlier on a head predating these changes. That job is not claimed as
passing, and the local full-suite run above covers the same project. The
previous head `da0d59cfb` completed Azure build 18415 with every job
succeeded.
## Review follow-up at `a08f0c12b`

Eleven new review threads arrived, nearly all documentation. Before
acting on each "this is new in 2.0" remark I checked the API against
`master378` rather than assuming, because a genuine 1.5 API still needs
its migration entry.

`ManagedSession`'s server-redundancy refresh had two hard-coded
two-second bounds. The new `ServerRedundancyOptions` exposes
`RefreshTimeout` for the metadata read and `PeerDiscoveryTimeout` for
each peer lookup, defaults both to the previous two seconds, validates
that a bound is positive or `Timeout.InfiniteTimeSpan`, and flows
through `ManagedSessionOptions`, `ManagedSession.CreateAsync`, the
fluent builder, and `DefaultServerRedundancyHandler`.

`AsyncCustomNodeManager` and `KeyCredentialBridgeAuthenticator` are both
absent from `master378`, so their obsolete members had no compatibility
constraint and are removed rather than carried:
`AsyncCustomNodeManager.ReadInitialValue`, the audience-less
`CreateTokenData` that only threw, and the audience-less `CreateProof`
that produced a proof no server accepts.
`CustomNodeManager2.ReadInitialValue` and the `MonitoredNode2` migration
section are genuine 1.5 APIs and are untouched.

The documentation now states master's behaviour instead of the delta
from 1.5.378, with each moved item already covered under
`docs/migrate/2.0.x/`. `IHistorianAnnotationProvider` is described by
its annotation-time key rather than as "legacy" — it is new in 2.0, so
the label implied a compatibility constraint that does not exist. The
KeyCredential bridge payload's `"version"` field is explained as a
vendor-extension format version with no OPC UA counterpart, and its
version-1 upgrade narrative is gone. `CertificateManager.md`'s migration
walkthrough is replaced by the resolver API it targets.
`NodeManagers.md`, `Sessions.md`, `AsyncServerSupport.md`,
`Certificates.md`, and `Profiles.md` no longer announce features
relative to 1.5. `Benchmarks.md`, `WhatsNewIn2.0.md`,
`DeveloperGuide.md`, and `docs/migrate/` keep their 1.5.378 references,
which are their subject.

CI then exposed a defect this batch introduced: five fixtures resolve
`ManagedSession`'s private constructor by its exact positional parameter
types, so the added parameter compiled cleanly but made `GetConstructor`
return `null` at run time. `7df1d5c40` updates every site;
`Redundancy.Client` passes 126 of 126 and the affected client fixtures
112 of 112.

The BouncyCastle question is answered and tracked in #4510:
`Opc.Ua.Core` already project-references `Opc.Ua.Security.Certificates`,
which already carries the conditional BouncyCastle reference, so the
raw-ECDH polyfill belongs there rather than as a new Core dependency.
The `IUserDatabase` metadata question is addressed to another maintainer
and is deliberately left open.

## Reported issue #4509 follow-up at `5d4d13d63`

The reporter verified `6ad1ecf16` and found one case still failing,
correctly. The empty-token-policies branch kept the configured identity
so a server that is down surfaces a transport failure rather than an
identity error. A provider-only configuration has no configured
identity, so it kept `null`: `OpenCoreAsync` turned that into Anonymous
and `OpenValidateIdentity` rejected it with `BadIdentityTokenInvalid`,
silently ignoring the configured provider.

The branch now requires a configured identity to defer to. Without one
the provider is consulted, so its own `BadIdentityTokenRejected` names
the offered and rejected policy set instead. Two regression tests cover
both configurations; the first fails against the previous behaviour by
reaching the session factory with a `null` identity, which is the exact
step that produced the reported error. That branch had shipped without a
test, which is why the case was missed.

## Review and CI status at `5d4d13d63`

**All actionable review threads are resolved: 132 of 133.** The single
open thread is a question the reviewer addressed to another maintainer
about moving user metadata into `IUserDatabase`; it is left open for
them rather than resolved silently. Every resolved thread carries a
reply naming its fix commit.

**Latest-head CI is green on `5d4d13d63`: 279 checks passed, zero
failed, zero pending**, with one conditional signed-package publishing
job skipped. Local validation for this batch: the full
`Opc.Ua.Client.Tests` suite passes 2,430 of 2,430 on net10.0,
`Opc.Ua.Redundancy.Client.Tests` 126 of 126, and `Opc.Ua.Server.Tests`
590 of 590 for the affected selections. Both net10.0 and net48 build
clean for `Opc.Ua.Client`, `Opc.Ua.Server`, and the changed test
projects, and all changed files pass included-file info-severity
formatting on both targets.

The PR remains open; no merge was performed. As stated at the top, this
does not claim that every item in the 244-finding audit backlog is
complete.
## Latest CI follow-up at `b6f2dfa43`

Azure build 18442 exposed a net48 compile regression in the Kafka
adapter: `RuntimeHelpers.GetUninitializedObject` is unavailable on .NET
Framework. Commit `2cf1e0eed` now uses normal `ProducerMessage`
construction on `NETFRAMEWORK` while retaining reflection-based
construction for modern targets that can load a different Dekaf runtime
asset. The exact net48 CI selection passes 92 of 92 tests, and the
netstandard2.1 adapter guard selection passes 11 of 11.

After merging the latest `master`, GitHub Actions run 35643592139 is
green. The initial Ubuntu Core.Schema job built successfully and passed
160 of 160 tests, then hit an external 403 while finalizing its
test-result artifact; its failed-job rerun completed successfully. Azure
build 18451 was canceled when its remote provider deprovisioned the
net10.0 Sessions runner (`Received request to deprovision: The request
was cancelled by the remote provider`). Azure attached no test task log
or source error, and the canceled parent left that child check stale as
in-progress. No source workaround, test skip, or check relaxation was
made for either infrastructure failure.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 68f26810-22b8-476f-abcd-0a8c82d6e208
2026-09-22 09:10:58 +02:00
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
romanett 747f635518 Fix the Opc.Ua.Core defects from the server/core audit (#4475)
# Description

Fixes every `src/Opc.Ua.Core` item found by a twelve-reviewer audit of
`src/Opc.Ua.Server` + `src/Opc.Ua.Core` taken at 0e600c0f0, across all
three tiers of that report (security/data-loss/crash, confirmed
functional, and reviewer-traced). **The `src/Opc.Ua.Server` half of the
audit is deliberately out of scope here** and remains open.

## Security and resource safety

- **Explicitly configured trusted certificates were ignored.**
`CertificateTrustList.TrustedCertificates` (the `<TrustedCertificates>`
config element, `SecurityConfiguration.AddTrustedPeer`) never reached
the validator, so a peer trusted only that way was rejected with
`BadCertificateUntrusted`. The list now travels from configuration
through `TrustListEntry` into `TrustListState`, and is consulted both
when deciding trust and when completing a chain — OPC 10000-4 6.1.3:
"The list of trusted Certificates may contain a Certificate issued to
another Application or it may be a Certificate belonging to a CA". The
store is still consulted first, so a CA configured both ways keeps its
CRL check. Cached validation cores are evicted when a trust list is
re-registered, so a reconfiguration actually takes effect.
- **Unauthenticated receive-buffer leak.** `SaveIntermediateChunk` now
always takes ownership of the chunk: it either stores it or returns it
to the `BufferManager`. A server without a `None` endpoint gives every
TCP client a discovery-only channel, and each non-discovery request
leaked one `ReceiveBufferSize` buffer. Added `TakeSavedChunks` for the
"body already saved" case; passing the chunk again would have queued the
same buffer twice. A channel torn down while a multi-chunk message is
still arriving now releases what it had buffered, which nothing did
before.
- **Decrypted OpenSecureChannel buffers** are returned when verification
fails, including the sibling paths where the sequence-number or
certificate check rejects the message *after* the body was handed back.
- **RSA Diffie-Hellman peer values** are validated against `2 <= y <=
p-2` (RFC 7919 5.1), so the shared secret can no longer be forced to a
known constant by sending 0, 1 or p-1.
- **`RemovePadding` verified no padding at all** for channel messages:
the loop started at `data.Offset` and stopped at `paddingSize`, so on
any message with a header it ran zero times. The filler loop is
corrected — OPC 10000-6 6.7.2.5.1: "The value of each byte of the
padding is equal to PaddingSize" — and the signature is now checked
*before* the padding is inspected so the two failures cannot be told
apart.
- **Key material zeroing** on the ECC secret path: the plain text, the
derived key and IV, and the ECDH shared secret, on both sides. The
decrypt wipe covers the failure path too, which is the one that matters
— the body is decrypted in place before the signature is verified.
- **HTTPS responses** are read with `ResponseHeadersRead` and bounded by
`MaxMessageSize`, instead of the whole body being allocated before the
limit was applied.
- **`X509CertificateStore.IsRevokedAsync`** reports an unanswerable
revocation check as `BadCertificateRevocationUnknown` instead of
throwing. The throw surfaced as an unsuppressible
`BadCertificateInvalid` and failed every CA-issued certificate on Linux
and macOS. See reviewer note 2.
- **Certificate handle ownership**: `ClientChannelManager` AddRefs what
it hands to a transport that disposes it (and releases it again when the
channel never opens), `CertificateTrustList` releases the handle `Add`
took its own reference to, and `X509CertificateStore` disposes the
platform handles nothing else owns.

## Transport

- The receive loop is now **one atomically swapped epoch of (transport,
token)**, so a reconnect that binds a new socket to an existing channel
supersedes the old loop rather than racing it. Previously the supersede
path was unguarded and could start two readers on one socket, dispose an
in-use `CancellationTokenSource`, and leak one per reconnect.
- A **reconnect hand-over detaches the transport before closing
anything**, so the socket the adopted channel now answers on is not
closed underneath it. Previously the queued `OpenSecureChannelResponse`
failed with `BadConnectionClosed` and reconnect to a dropped channel
could never succeed.
- A blocked or unservable **accepted socket is closed** rather than left
open until process exit.
- An **invalid response sequence number reconnects the channel** instead
of throwing into a handler that did nothing, which left the pending
request hanging until timeout. OPC 10000-6 6.7.7 requires the receiver
to close the channel, and the same section requires the error to be
logged — which `HandleMessageProcessingError` now does instead of
discarding it silently.
- The **abort chunk** is encoded against the room the chunk has rather
than what it currently holds, so a short offending chunk can still carry
the error body (OPC 10000-6 6.7.3).

## Discovery and endpoints

- `GetEndpoints` no longer drops the HTTPS and WSS JSON/OpenAPI
endpoints, and no longer collapses them onto the binary one (the dedup
ignored `TransportProfileUri`). `profileUris` filtering matches them too
— OPC 10000-4 5.5.4.2 defines it as the "List of Transport Profile that
the returned Endpoints shall support", and a client asking for the JSON
profile previously got an empty list back. Results are ordered
deterministically with **binary first**, so a client that takes the
first endpoint that fits keeps getting the one it got before these twins
were published.

## Smaller confirmed defects

`FilterEvaluator.InList` returned after the first string operand;
`SelectedUserTokenPolicy` setter always ended at `-1`;
`Utils.UpdateExtension` could not delete; a lease holder never stepped
down while the shared store was unreachable *or hanging*;
`IsUriHttpRelatedScheme` never matched `http`;
`SessionLessServiceMessage` dropped a single `LocaleId`;
`GetRandomDouble` used `ToSingle` and typed variant arrays ignored the
requested `builtInType`; source-generated log calls never reached the
trace file.

## Related Issues

- No tracking issue was opened for this work; it derives from an audit
report rather than a filed issue. Happy to open one and link it if
maintainers prefer.

## Notes for reviewers

1. **`MaxRejectedCertificates` semantics.**
`ICertificateStore.AddRejectedAsync` documented "0 is unlimited", but
`DirectoryCertificateStore` wrote every certificate and then deleted all
of them at 0 (net effect: store nothing), and `CertificateManager`
clamped a negative value onto 0 and *depended* on that to implement
"clear the store". `SharedKeyValueCertificateStore` meanwhile treated 0
as genuinely unlimited, so the two store types did opposite things at
the same setting. This PR keeps the existing observable behaviour (0 or
less keeps no history and clears what is stored), removes the
write-then-delete-all churn, aligns the KV store, and corrects the
interface doc and `docs/DependencyInjection.md`. **The KV store is the
one place whose behaviour changes** relative to master.

2. **Revocation status where X509Store has no CRLs.** `IsRevokedAsync`
used to throw there, which surfaced as an unsuppressible
`BadCertificateInvalid` and failed every CA-issued certificate on Linux
and macOS - the outage the audit flagged. It now returns
`BadCertificateRevocationUnknown`, which is what the Windows branch of
the same method and a directory store without a CRL already return, and
the status `RejectUnknownRevocationStatus` decides on (OPC 10000-4
6.1.3, Find Revocation List). With the default policy the certificate is
accepted; an operator who opted into strict mode gets a rejection, as
before on master. An earlier revision returned `BadNotSupported`, which
the validator discards outright and which made revocation fail open even
in strict mode; review caught it.
3. **`ManagedChannelKey` gained a `TransportProfileUri`** property and
an optional trailing constructor parameter. Source-compatible, but it is
public API.

## 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.
- [ ] 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.
- [ ] I have addressed **all** PR feedback received.

### Regression test coverage

Each of these fails against `master`'s source and passes with the fix.

| Fix | Test |
|---|---|
| Padding filler bytes were never verified |
`CryptoUtilsSymmetricPaddingTests` — forged filler rejected, plus a
round trip over every padding length |
| Padding removed before the signature was checked |
`CryptoUtilsSymmetricPaddingTests.SymmetricDecryptReportsSignatureFailureAheadOfPadding`
|
| RSA-DH peer value not range checked | `RsaDiffieHellmanPeerValueTests`
— 0, 1, p-1, p, above p, both range boundaries, group mismatch |
| A degenerate DH nonce threw instead of being rejected |
`UaSCBinaryChannelNonceTests` |
| `<TrustedCertificates>` never reached the validator |
`CertificateValidationCoreTests` (peer, issuer, and the negative
control),
`CertificateManagerTests.ExplicitlyTrustedPeerFromConfigurationIsAcceptedAsync`
|
| Cached validation cores survived a trust-list change |
`CertificateManagerTests` — registration after a failed validation, and
`UpdateAsync` onto a different store |
| `MaxRejectedCertificates` normalisation |
`CertificateManagerTests.MaxRejectedCertificatesNormalisesNegativeValues`
|
| `AddRejectedAsync` retention contract |
`RejectedCertificateRetentionTests` — both store types, at zero,
negative and a positive cap |
| PEM rewrite left the old tail behind |
`DirectoryCertificateStoreTests.DeleteFromAMultiCertificatePemFileTruncatesTheFileAsync`
|
| `IsRevokedAsync` threw where CRLs are unsupported |
`CertificateStoreTest.IsRevokedAsyncReportsNotSupportedWithoutCrlSupportAsync`
(runs on Linux/macOS, ignored on Windows) |
| ECC secret left in the caller's buffer |
`EncryptedSecretTests.EccDecryptWipesThePlainTextFromTheCallersBufferOnFailure`
|
| Trust-list read leaked a handle per entry |
`CertificateTrustListHandleTests` |
| A channel that never opened leaked its certificate |
`ClientChannelManagerHandleTests` |
| Chunk ownership, CLO double free, chunk-limit teardown |
`TcpServerChannelBufferTests` (4 new) |
| Receive loop did not follow a replaced transport |
`TcpServerChannelDeterministicTests` (3 new) |
| HTTPS body unbounded, failures unmapped |
`HttpsTransportChannelResponseTests` (5) |
| JSON/OpenAPI endpoints dropped, collapsed, or unordered |
`ServerBaseTransportProfileTests` (12) |
| Channel key ignored the transport profile |
`ClientChannelManagerManagedTests` (2 new) |
| `SelectedUserTokenPolicy` setter always ended at -1 |
`ConfiguredEndpointTests` (4 new) |
| `UpdateExtension` could not delete | `UtilsAdditionalTests` (3 new) |
| A single `LocaleId` was dropped | `SessionLessServiceMessageTests` (2
new) |
| `GetRandomDouble` / typed variant arrays | `DataGeneratorTests` (4
new) |
| `TraceLoggerProvider.IsEnabled` short-circuited every log call |
`TraceLoggerProviderTests` (3 new) |
| A leader never stepped down on an expired lease |
`SharedStoreLeaseElectionTests` (3 new) |
| `FilterEvaluator.InList`, `IsUriHttpRelatedScheme` | existing tests
corrected in the first commit |

**Not covered.** The reconnect hand-over ordering, the OpenSecureChannel
decrypted-buffer returns, the client-side bad-sequence reconnect, the
abort-chunk sizing and the listener closing a blocked socket all need a
two-socket reconnect or a full asymmetric OpenSecureChannel exchange,
which the deterministic channel harness does not model. They are
exercised by the integration suites but not asserted on directly.
## Spec conformance

Where a fix has a normative basis I checked it against the published
specification rather than only against the audit report:

| Change | Reference |
|---|---|
| Every padding filler byte is verified | OPC 10000-6 6.7.2.5.1 — "The
value of each byte of the padding is equal to PaddingSize". The loop
started at `data.Offset` and stopped at `paddingSize`, so on any message
with a header it ran zero times and no filler byte was ever checked. |
| Signature verified before the padding is inspected | OPC 10000-6
6.7.2.5.1 — the signature "includes the headers, all Message data, the
PaddingSize and the Padding"; 6.7.7 — the chunk "shall not be
interpreted until the Message is decrypted and the signature and
sequence number verified". |
| Message-processing errors are logged | OPC 10000-6 6.7.7 — "the
receiver shall log a Bad_SequenceNumberInvalid error", and stacks "shall
have a mechanism to log errors when invalid Messages are discarded". The
handler was an empty method. |
| A bad response sequence number reconnects the channel | OPC 10000-6
6.7.7 — "the receiver shall close the communication channel". It
previously threw into that empty handler and the pending request hung
until timeout. |
| The trust list's own certificates are honoured as peers **and** as
issuers | OPC 10000-4 6.1.3 — "The list of trusted Certificates may
contain a Certificate issued to another Application or it may be a
Certificate belonging to a CA". |
| The abort chunk is sized by the chunk's capacity | OPC 10000-6 6.7.3 —
the abort body is `Error` plus a `Reason` string, so a chunk shorter
than the error body could not carry it. |
| JSON/OpenAPI endpoints are published and match `profileUris` | OPC
10000-4 5.5.4.2 — `profileUris` is the "List of Transport Profile that
the returned Endpoints shall support"; a client asking for the JSON
profile previously got an empty list. |
| RSA-DH peer value restricted to `2 <= y <= p-2` | RFC 7919 5.1, which
the finite-field policies are built on. I found no OPC-specific wording
for this. |

One point where the spec argues against the current shape is called out
in reviewer note 2 below.

### Test runs

On **net10.0**, with `Opc.Ua.Core.Tests` and
`Opc.Ua.Security.Certificates.Tests` also run on **net48**. The
remaining suites were not run on the .NET Framework targets locally, so
that checklist box stays unchecked.

| Suite | Result |
|---|---|
| Opc.Ua.Core.Tests (net10.0) | 4630 passed, 0 failed, 88 skipped |
| Opc.Ua.Core.Tests (net48) | 4545 passed, 0 failed, 104 skipped |
| Opc.Ua.Core.Security.Tests | 556 passed, 0 failed |
| Opc.Ua.Security.Certificates.Tests (net10.0) | 355 passed, 0 failed |
| Opc.Ua.Security.Certificates.Tests (net48) | 353 passed, 0 failed |
| Opc.Ua.Configuration.Tests | 227 passed, 0 failed |
| Opc.Ua.Core.Encoders.Tests | 2929 passed, 0 failed |
| Opc.Ua.Bindings.Https.WebApi.Tests | 346 passed, 0 failed |
| Opc.Ua.Redundancy.Server.Tests | 783 passed, 0 failed |
| Opc.Ua.Client.Tests | 2217 passed, 0 failed |
| Opc.Ua.Server.Tests | 5552 passed, 0 failed |
| Opc.Ua.Sessions.Tests | 778 passed, 0 failed, 379 skipped |
| Opc.Ua.Gds.Tests | 1117 passed, 0 failed |

`Opc.Ua.Sessions.Tests` reports 379 skipped; that is identical to a
`master` baseline run in a separate worktree (security policies the
platform does not advertise), not a regression from this change.

`dotnet format whitespace --verify-no-changes` reports no violations in
any file this PR touches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 08:22:41 +02:00
Marc Schier 2d9088119b Fix alias startup ordering and retained translation fallbacks (#4469)
# Description

Complete the two P2 follow-up fixes identified during review of #4456.

- Copy alias stores after application `IServerPreStartupTask`
registrations finish, but before address-space creation. This preserves
stores initialized asynchronously during startup, for both the default
DI server and compatible custom servers. Task ordering, once-only
execution, failure/cancellation behavior, and opt-in snapshot
materialization remain intact.
- Preserve the original `TranslationInfo` fallback locale, template,
key, and arguments while rendering the selected translation. Retained
values and service results can be translated for another session without
losing their original fallback.
- Keep selected templates unformatted until rendering, use the selected
locale for formatting, retain metadata through locale filtering, and
avoid double-formatting escaped braces during single-language
conversion.
- Add deterministic alias initialization and translation regressions,
extend native hosting coverage, and update the feature documentation.

Public signatures and `LocalizedText` struct layout are unchanged. No
dependencies, migration-guide changes, CI settings, or
coverage-threshold changes are introduced.

## Related Issues

- Related to #4410.
- Follow-up to the two P2 review findings in #4456.

## Validation

The new regressions failed before the relevant fixes and pass afterward.

| Scope | net10.0 | net48 |
| --- | --- | --- |
| Complete Types test project | 10,265 passed; 1 existing skip | 10,255
passed; 1 existing skip |
| Affected hosting, aliases, resources, and configuration tests | 561
passed | 561 passed |
| Existing real-session alias integration tests | 11 passed | 11 passed
|

- Built the affected libraries across all configured target frameworks
with **0 warnings and 0 errors**.
- Published and ran the actual Windows NativeAOT test executable: **7
HostingAotTests passed**, including initialized alias registries and
English-to-German-to-English fallback preservation. Local compilation
used `IlcSingleThreaded=true` after a parallel compiler attempt
exhausted host memory; repository settings are unchanged.
- Focused coverage: **28/29 changed executable production lines
(96.55%)**.
- Changed-file formatting and diff checks pass.

The results above are the stated local project/filtered runs, not a
complete local `UA.slnx` test run. Remote CI and CodeQL results are
pending.

## 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.
- [ ] I have addressed **all** PR feedback received.
2026-09-11 14:28:46 +02:00
Marc Schier 0e600c0f0b Fix hosted-server DI composition gaps (#4456)
# Description

Complete the remaining hosted-server DI hooks so applications can
compose the regular server without subclassing `StandardServer` solely
for these extension points.

- Add `ConfigureServerProperties` and ordered, per-server
`ConfigureResources` callbacks. BuildInfo defaults come from the
effective application configuration; explicit property registrations
take precedence without mutating shared options.
- Add authenticator instance/validator-aware factory overloads and
generic/async-delegate startup tasks. Startup tasks are awaited in
registration order, and failures or cancellation clean up the server.
Host shutdown waits for execution cleanup before disposing injected
dependencies, including when its shutdown deadline expires.
- Add asynchronous and legacy node-manager factory instances plus
deferred, configuration-aware `ArrayOf<IAsyncNodeManagerFactory>`
registrations. Resolve factories after configuration and certificate
setup while preserving ownership and feature isolation.
- Register alias stores before address-space creation and add opt-in
standard `TagVariables`/`Topics` materialization. Existing
custom-category behavior is retained; browse nodes are startup snapshots
while queries and supported LastChange values remain live.
- Derive the default DI server from `ReverseConnectServer`, retaining DI
roles/session hooks and authoritative XML/stream configuration. Ignore
queued callbacks from stopped or superseded timers to prevent
reconnection or rearming after shutdown.
- Fix resource translation lookup/formatting exposed by the new
callbacks, retain existing constructor signatures and
direct/custom-server paths, and document the optional hosting APIs.

Adopting these hooks is optional; existing construction and
subclass-based customization remain supported. The migration guide is
unchanged.

No changes to UA-.NETStandard-Samples and no new dependencies.

## Related Issues

- Fixes #4410

## Validation

- Focused server hosting, aliases, reverse-connect, configuration, and
resource suites: **571 passed on net10.0 and 571 passed on net48**.
- Existing affected real-session alias integration fixtures: **11 passed
on each framework**.
- Real reverse-connect discovery, session creation, and BuildInfo reads
verified for options, XML-file, and stream configuration.
- Published and executed the Windows NativeAOT test binary: **7
HostingAotTests passed**.
- Final server-library build passed across all configured target
frameworks with **0 warnings and 0 errors**; no new diagnostics in
changed test files.
- Focused changed-line coverage: **336/338 executable production lines
(99.41%)**.
- Deterministic timer regressions failed before the shutdown fix and
pass afterward.
- CI shutdown follow-up: the unchanged
`UnavailableUpstreamEndpointFailsMappedReadAsync` scenario passes
locally, and **245 hosting tests pass on each of net10.0 and net48**,
including regressions for an expired host shutdown deadline and
already-canceled startup. The local net48 run uses a short NUnit work
directory to avoid Windows path-length limits.

## Azure build 18136 follow-up

- Await WoT browse reconciliation before upload, default-version, and
enable/disable handlers return. Three barrier-controlled regressions
reproduce the exact missing version metadata before the fix.
- Make the reconciliation test observe the actual corrective state frame
rather than require a new acknowledgement after a no-op reseed. The
corrected tests also verify value, status, timestamp, and quiescence;
production gossip behavior is unchanged.
- Drain redirected stdout/stderr when sample processes exit, replacing a
fixed grace delay that could lose the final success line. A controlled
real-process regression fails before the fix.
- Fix CA1508, CA1823, CA1850, CA2012, and CA1861 at their sources, and
correct unsupported legacy AOT declarations and the existing
stress-placeholder target. Modern test targets and AOT analysis remain
enabled.

Follow-up validation:

- Full **Release `UA.slnx` builds pass on net10.0 and net48 with 0
warnings and 0 errors**.
- Complete previously failing WoT net48 job: **1496 passed**. The
identical binary was run from a short local assembly path to avoid the
nested repair worktree exceeding Windows path-length limits.
- Complete previously failing Redundancy.Server net10.0 job: **783
passed**.
- All short-haul HA/process cases: **12 passed**, including both
original identity-failover topologies.
- Affected WoT lifecycle selection: **113 passed on Release net10.0**;
controlled replication selection: **15 passed on each framework**.
- No test assertions, timeouts, coverage thresholds, collector settings,
or CI failure gates were weakened. Existing legacy empty-project result
notices and auto-property coverage exclusions remain unchanged.

These are full solution builds plus the reported test scopes, not a full
local `UA.slnx` test run.

Remote validation at `77c1055f1adb1557c5bf99b70e76f4a0cd95ee94`: **all
265 PR checks passed**, including Azure build **18172**, GitHub Build
and Test, CodeQL, and Docker image checks.

## 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.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-10 05:56:52 +02:00
romanett 1af191ec26 Drop the .Common suffix from the three GDS projects (#4438)
# Description

Every other companion specification in the stack ships as `<Family>`,
`<Family>.Client` and `<Family>.Server` — `Opc.Ua.Di`, `Opc.Ua.ISA95`,
`Opc.Ua.Positioning`, `Opc.Ua.Robotics`, `Opc.Ua.Vision`,
`Opc.Ua.WotCon`, `Opc.Ua.XRegistry`, and `Opc.Ua.Lds.Server` without a
family package. GDS was the one exception, carrying a `.Common` suffix
that named nothing.

| Before | After |
| --- | --- |
| `src/Opc.Ua.Gds.Common` | `src/Opc.Ua.Gds` |
| `src/Opc.Ua.Gds.Client.Common` | `src/Opc.Ua.Gds.Client` |
| `src/Opc.Ua.Gds.Server.Common` | `src/Opc.Ua.Gds.Server` |

## No source changes were needed

`RootNamespace` was already the final name in all three projects — the
namespaces have always been `Opc.Ua.Gds`, `Opc.Ua.Gds.Client` and
`Opc.Ua.Gds.Server`, and only `AssemblyName` and `PackageId` disagreed.
Nothing that compiles against these assemblies changes a `using`
directive; the diff outside the moves is entirely ids, paths and prose.

Directories were moved with `git mv`, and every one of the 73 file moves
is detected as a rename at 100% similarity (bar the handful that also
had a name in their text), so `git log --follow` and blame survive.

## Consumer impact

The package ids change, which is visible to anyone consuming the NuGet
packages:

| 1.5.378 package | 2.0 package |
| --- | --- |
| `OPCFoundation.NetStandard.Opc.Ua.Gds.Common` |
`OPCFoundation.NetStandard.Opc.Ua.Gds` |
| `OPCFoundation.NetStandard.Opc.Ua.Gds.Client.Common` |
`OPCFoundation.NetStandard.Opc.Ua.Gds.Client` |
| `OPCFoundation.NetStandard.Opc.Ua.Gds.Server.Common` |
`OPCFoundation.NetStandard.Opc.Ua.Gds.Server` |

`docs/migrate/2.0.x/packages.md` gains a renamed-packages section — it
is the document that undertakes to cover NuGet renames, and had no GDS
entry — and the migration skill's package table gains the two rows a
1.5.378 consumer needs. That is the only hand-written content in this
PR; everything else is mechanical.

Assembly names follow the package ids, so the section also notes that
binding redirects, trimming descriptors and signing manifests naming the
files directly have to be updated.

## Build infrastructure

Everything naming the projects or their output moves with them:

- `UA.slnx` and `tools/MigrationAnalyzer.slnx` (both re-sorted — the
project lists are alphabetical, and `Gds` now sorts before
`Gds.Client`).
- `.azurepipelines/expected-packages.txt` — 3 ids, also re-sorted for
the same reason.
- `.azurepipelines/signlistDebug.txt` and `signlistRelease.txt` — 18
paths each. Their internal ordering is left alone: that group was
already unsorted, and reordering a signing manifest is diff noise.
- `samples/Reference/ConsoleReferenceServer/Dockerfile`, the two
metapackage nuspecs, the analyzer nuspec's package dependencies, and 8
`.csproj` references across tests, samples and tools.

`preview-pack.slnx` is generated from `UA.slnx` by `generate-slnx.ps1`,
so there is nothing static to edit. Nothing under `.github/workflows`,
and no `.ps1`/`.yml` in `.azurepipelines`, names these projects.

The migration analyzer's shim tree mirrors the source project layout by
its own documented convention, so `Gds.Client.Common/` becomes
`Gds.Client/` and its readme table follows.

## Deliberately not changed

Prose naming past work: `plans/23` still records that its deferral
happened during the "GDS Client.Common modernization", which is the name
that effort had. File *paths* in the same document were updated.

## Validation

`net10.0`:

- `UA.slnx`: 0 errors. 14 warnings, all pre-existing
CA1861/CA1850/CA1307 in `XRegistry`, `Types.Tests` and `WotCon.Tests` —
none in anything this PR touches.
- Assemblies build as `Opc.Ua.Gds.dll`, `Opc.Ua.Gds.Client.dll`,
`Opc.Ua.Gds.Server.dll`, with no `*.Common.dll` produced anywhere.
- `Opc.Ua.Gds.Tests`: 1,116 passed, 0 failed, 54 skipped.
- `Opc.Ua.MigrationAnalyzer.Core.Tests`: 10 passed, 3 skipped.
- A repo-wide grep for the old names returns only the intentional old →
new mappings in the migration docs.

## Checklist

- [x] Existing tests pass
- [x] No new tests — this is a rename with no behaviour change
- [x] Consumer-visible package rename documented in the migration guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:08:54 +02:00
Marc Schier 4c973dc2e0 Remove the implicit fluent ReferenceServer root (#4394)
## Summary

- stop `AddNodeManager(namespaceUri, build)` from creating a hidden
`ReferenceServer` folder
- keep the named root as an internal opt-in used only by
`AddReferenceServer`
- centralize fluent node-manager factory registration so the interface
implementation and extension path cannot diverge
- add hosting coverage for the no-root default, explicit
callback-created roots, and the reference-server preset
- update hosting API documentation and examples to show explicit root
creation and placement

## Relationship to #4389

This is an independent hosting bug in `FluentNodeManagerFactory`; #4389
does not change that factory. Keeping it separate avoids expanding the
already large architectural PR. The node-source branch has been rebased
over this fix locally without reintroducing the implicit root.

## Validation

| Scope | Result |
| --- | --- |
| Focused hosting tests `net10.0` | 17 passed |
| Focused hosting tests `net48` | 17 passed |
| Full `Opc.Ua.Server.Tests` `net10.0` | 4,861 passed, 5 skipped |
| Full solution build `net10.0` | Succeeded |
| Full `Opc.Ua.Server.Tests` `net48` | 4,855 passed, 6 skipped before
the assembly-wide teardown reported four outstanding certificate
wrappers; the same teardown result reproduced on the independent #4376
branch |

## Related Issues

Fixes #4388

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-03 10:39:00 +02:00
Marc Schier 4eccafbe2f Remove AAS projects and samples (#4359)
# Description

Remove the Asset Administration Shell projects and samples from the
mainline
repository while preserving the complete implementation and history on
the
`aas` branch.

This change:

- removes the `Opc.Ua.Aas`, `Opc.Ua.Aas.Client`, `Opc.Ua.Aas.Server`,
and
  `Opc.Ua.Aas.WoT` projects;
- removes the AAS test project, AOT coverage, sample, and documentation;
- removes AAS entries from `UA.slnx`, the expected package list, and
related
  build configuration;
- removes the now-unused `System.IO.Packaging` package version; and
- updates remaining documentation and comments so they no longer
advertise
  removed AAS functionality.

The preservation branch `aas` points to the original `master` commit
before
this removal.

## Related Issues

- None.

## Validation

- `dotnet restore UA.slnx`
- `dotnet build UA.slnx -c Release --no-restore`
- Repository-wide sweep for dangling AAS project, package, file, and
  documentation references

The full Release build completed with zero errors. It reported 31
existing
analyzer warnings in files unrelated to this change.

## 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.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. (Not applicable: this removes
a feature and its tests.)
- [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.
- [ ] I have addressed **all** PR feedback received.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-31 17:08:04 +00:00
romanett 52df519e50 Expose the client configuration provider and add an opt-in eager load on host start (#4343)
# Description

`AddClient(configurationFile)` (#4328) loads the supplied configuration
XML lazily on the first DI session connect, and the
`ClientSuppliedConfigurationProvider` behind it was reachable only
through internals. Hosted clients whose user interface needs the
`ApplicationConfiguration` **before any session exists** therefore had
no public way to trigger the load or to read the configuration back —
the concrete blocker for moving the 14 WinForms sample clients and the
GDS client off the classic eager `ApplicationInstance` path
(OPCFoundation/UA-.NETStandard-Samples#794 /
OPCFoundation/UA-.NETStandard-Samples#808).

Two small additions, exactly as designed in #4340:

**1. `OpcUaClientOptions.ConfigurationProvider` is now public** (`get`;
the setter stays `internal`).

It is already typed as the public
`IOpcUaApplicationConfigurationProvider`, so consumers get everything
they need with **no new types**: `GetAsync(ct)` as an explicit load
trigger that does not connect a session, `Configuration` for the loaded
document, and `Application` (`IApplicationInstance`) for the
applications that manage their own instance certificate. Registering the
supplied provider as `IOpcUaApplicationConfigurationProvider` in DI was
deliberately *not* done — `OpcUaServerHostedService` binds to the last
registered interface when it has no document of its own, so in a
combined client+server host a client-only document would silently
capture the server.

**2. `OpcUaClientOptions.LoadConfigurationOnStart` — an opt-in eager
load for hosted applications.**

It registers an internal `ClientConfigurationLoaderHostedService` that
awaits `GetAsync` during host start, the client-side twin of
`ReverseConnectManagerHostedService`. When `host.StartAsync()` returns,
the document is loaded and validated, the application-instance
certificate is ensured, `Configuration` is published onto the resolved
options (exactly as the connect path does), and a load failure fails the
host start instead of surfacing on the first connect. It is registered
ahead of the reverse-connect hosted service, so the configuration is
complete before anything consumes it.

The flag applies to every provider source — a document supplied via
`ConfigurationFile` / `ConfigurationStream` and a shared
`ConfigureApplication(...)` provider alike — and is a no-op for an
explicit `Configuration`, which is already complete. It is bindable from
`OpcUa:Client:LoadConfigurationOnStart`. Applications without a Generic
Host call `GetAsync` on the provider directly instead; both routes share
the provider's single-flight load, so the configuration is never loaded
twice.

A WinForms client then becomes:

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

await host.StartAsync();

ApplicationConfiguration configuration = host.Services
    .GetRequiredService<OpcUaClientOptions>()
    .ConfigurationProvider!.Configuration;
```

**Reviewer note:** because the flag decides a *service registration*, it
must be set inside the `AddClient(...)` callback or bound from the
configuration section — setting it on the resolved options afterwards
has no effect. This is called out in the XML docs and in
`DependencyInjection.md`.

**Coverage:** three tests added to `ClientConfigurationFileTests` for
the supplied-document path (eager load on start including the
certificate bootstrap and `Application` surface, load failure surfacing
on start, no hosted service registered without the flag) and a new
`ClientEagerConfigurationLoadTests` fixture for the provider-agnostic
behavior (shared `ConfigureApplication(...)` provider awaited exactly
once, explicit-`Configuration` no-op, host-startup-token cancellation
aborting the load, configuration-section binding, and an end-to-end
`host.StartAsync()` test). All 331 `ClientBuilder` tests pass (8 new),
as do the 185 server hosting tests including the combined client+server
fixtures. `Opc.Ua.Client` builds warning-free on all target frameworks;
the test project builds clean on net10.0 and net472.

**Docs:** new "Client: loading the configuration eagerly" section in
`docs/DependencyInjection.md` and a pointer in the `Opc.Ua.Client` NuGet
README.

With these in place the sample clients can switch in a follow-up to
OPCFoundation/UA-.NETStandard-Samples#808.

## Related Issues

- Fixes #4340
- Builds on #4328

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-08-30 20:50:46 +02:00
romanett 70c8ce4649 Allow passing an existing config XML to the client DI AddClient (#4328)
# Description

> **Stacked PR**: based on `romanett/server-di-config-xml-e30c4f`
(#4325) so the diff shows only the client changes. Once #4325 merges
this will be rebased onto `master`.

Gives the **client** DI surface the same migration path for existing
configuration XML files that #4325 gave the server: applications that
already own a classic `MyClient.Config.xml` can pass it to `AddClient`
and keep **all** of their settings.

New API surface:

- `AddClient(string configurationFile, Action<OpcUaClientOptions>?
configure = null)` loads the `ApplicationConfiguration` from the file;
the optional callback configures the remaining client options (session
endpoint, identity, reverse connect, ...).
- `AddClient(Stream configurationStream, ...)` for documents that are
not files on disk, e.g. embedded resources. The stream is read once and
disposed after loading.
- `OpcUaClientOptions.ConfigurationFile` / `ConfigurationStream` back
the overloads, so the options-callback route works too and the file path
can be bound from configuration (`OpcUa:Client:ConfigurationFile`).
- `OpcUaClientOptions.ConfigureLoadedConfiguration` runs after the
document is loaded and validated but before the application-instance
certificate check, for code-side overrides of individual file settings.

Mechanism and behavior:

- A new internal `ClientSuppliedConfigurationProvider` implements
`IOpcUaApplicationConfigurationProvider` and plugs into the client's
existing configuration-provider seam. The document loads **lazily on
first use** — the first session connect, reverse-connect startup, or an
explicit `GetAsync` — through
`ApplicationInstance.LoadApplicationConfigurationAsync`, so every
setting (security configuration, certificate stores, transport quotas,
client configuration) applies exactly as on the classic path, including
localhost fix-up.
- The load **ensures the application-instance certificate** (mirroring
the shared `ConfigureApplication(...)` provider), which the pre-existing
manual workaround — pre-loading and assigning
`OpcUaClientOptions.Configuration` — never received. DI-registered
`ICertificateManager` / `ICertificatePasswordProvider` are honored.
Single-flight loading; `IAsyncDisposable` like the shared provider.
- The supplied document is authoritative and takes precedence over a
shared application registered with `ConfigureApplication(...)`.
Ambiguous combinations are rejected eagerly at `AddClient` with clear
`InvalidOperationException`s: file + stream, document + explicit
`Configuration`, document + application identity properties, and a
white-space path arriving through configuration binding. The start-time
`IValidateOptions` validator accepts a supplied document as a
configuration source.
- `ConnectManagedSessionAsync` publishes the lazily loaded configuration
onto the resolved options after `GetAsync`, so the channel manager and
session builder see it exactly as on the other configuration paths;
reverse connect consumes the provider through its existing async-startup
path unchanged.

Coverage: new `ClientConfigurationFileTests` fixture (8 tests) — loading
from file and stream with assertions on the document settings, the
override callback, the certificate bootstrap, provider caching and lazy
semantics, precedence over `ConfigureApplication`, load failure for a
missing file, argument validation, ambiguous-combination rejection,
configuration-section binding, and validator acceptance. All 323
existing ClientBuilder tests and all 180 server hosting tests (including
the combined client+server fixtures) pass; `Opc.Ua.Client` builds
warning-free on net472, net8.0, and net10.0 (AOT analyzers).

Docs: new "Client: using an existing configuration XML file" section in
`docs/DependencyInjection.md` with cross-links from the server section,
an updated pointer in the 2.0 migration guide, and a NuGet README
mention.

## Related Issues

- Fixes #4327
- Stacked on #4325 (server-side counterpart)

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
2026-08-29 13:33:40 +00:00
romanett b9657e5f0d Allow passing an existing config XML to the hosted server DI AddServer (#4325)
# Description

Extends the hosted-server dependency-injection surface so applications
that already own a classic `ApplicationConfiguration` XML file
(`*.Config.xml`) have a direct migration path to DI with **all** of
their existing settings applied.

New API surface:

- `AddServer(string configurationFile, Action<ApplicationConfiguration>?
configure = null)` and the `AddServer<TServer>` variant load the
configuration from the file instead of building it from
`OpcUaServerOptions`.
- `AddServer(Stream configurationStream, ...)` (plus generic variant)
for documents that are not files on disk, e.g. embedded resources. The
stream is read once and disposed by the hosted service during startup.
- `OpcUaServerOptions.ConfigurationFile` / `ConfigurationStream` back
the overloads, so the options-callback route works too and the file path
can be bound from configuration (`OpcUa:Server:ConfigurationFile`).
- `OpcUaServerOptions.ConfigureLoadedConfiguration` runs after the
document is loaded and validated but before certificates are checked, so
individual settings can be overridden from code without editing the
file.

Behavior:

- The document is loaded through
`ApplicationInstance.LoadApplicationConfigurationAsync`, so every
setting (base addresses, security policies, certificate stores,
transport quotas, operation limits, user token policies) applies exactly
as on the classic path, including localhost fix-up.
- The supplied document is authoritative: the builder-feeding options
(`ApplicationName`, `EndpointUrls`, `PkiRoot`, policy toggles,
`ConfigureBuilder`, ...) are not applied on this path, and it takes
precedence over a shared application registered with
`ConfigureApplication(...)`. Options acting on the hosted server itself
(`Identity`, `ConfigureRateLimits`) and all fluent registrations
(`AddNodeManager`, `ConfigureRoles`, identity authenticators, ...)
compose unchanged.
- DI-registered `ICertificateManager` / `ICertificatePasswordProvider`
are honored, the "listening at" log reports the loaded base addresses,
and the unmatched user-token-policy startup warning is derived from the
policies the document advertises.
- Setting both `ConfigurationFile` and `ConfigurationStream` fails
startup with a clear `InvalidOperationException`; a missing file fails
startup with the original `ServiceResultException`.

Coverage: new `ConfigurationFileHostingTests` fixture (12 tests) with
end-to-end hosted starts from file and stream asserting the loaded
settings, custom `TServer` types, the override callback, precedence over
`ConfigureApplication`, warning derivation from the document,
missing-file and file+stream startup failures, argument validation, the
duplicate-registration guard, and configuration-section binding.

Docs: new "Migrating with an existing configuration XML file" section in
`docs/DependencyInjection.md` (plus first-class options table rows), a
pointer in the 2.0 migration guide, and a NuGet README mention.

## Related Issues

None — additive convenience overloads on the existing hosted-server
dependency-injection surface; no behavior change for existing
registrations.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 10:48:24 +02:00
Copilot de3476d332 Remove HTTPS binding dependency from server packages (#4297)
# Description

`Opc.Ua.Server` and `Opc.Ua.Gds.Server.Common` depended directly on
`Opc.Ua.Bindings.Https`, which pulled ASP.NET Core into server
applications
even when optional HTTPS-based transports were not used.

- Remove the HTTPS binding project references from Server and GDS
entirely.
- Add a Core-owned `IOpcUaTransportBuilder` contract for builders that
support
  transport registration.
- Move the type-preserving HTTPS, WSS, Kestrel OPC TCP, and Web API
fluent
  extensions into `Opc.Ua.Bindings.Https`.
- Remove the repository-wide `NoHttps` / `NOHTTPS` build switch.
- Keep ConsoleReferenceServer HTTPS/WSS support through an explicit
binding
  reference and the ASP.NET Core container runtime.
- Document that applications must reference the optional binding package
  directly.

## Validation

- Built `Opc.Ua.Bindings.Https` in Release for all target frameworks.
- Built Server and GDS for .NET 10 and .NET Framework 4.8.
- Built ConsoleReferenceServer and PumpDeviceIntegrationServer for .NET
10.
- Passed 65 HTTPS binding dependency-injection tests.
- Passed focused Server and GDS transport-builder tests.
- Verified refreshed Server/GDS restore assets and output manifests
contain no
  `Opc.Ua.Bindings.Https` dependency.

## Related Issues

Fixes #4296

## 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.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce build or analyzer
warnings.
- [ ] I ran all tests locally using the `UA.slnx` solution against .NET
Framework and .NET 10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Copilot-Session: 41b98d43-8bb8-4587-8f3b-9532544dd582
2026-08-21 07:29:18 -07:00
Marc Schier 397db603db Add OPC UA for Asset Administration Shell: the published OPC 30270 and v3 draft (#4244)
# Description

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

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

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

## What is in here

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

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

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

## Defects found and fixed along the way

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

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

## Specification findings

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

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

## Known limitation

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

## Related Issues

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

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Verification

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

### Review pass

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

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

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

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Copilot-Session: c3d88950-4a65-4ff6-87d2-1f545a875c9b
Copilot-Session: b8ab3763-b9bd-46ae-b0ef-045db26e5782
2026-08-18 08:11:47 -07:00
Marc Schier 0612a43fe4 Pluggable cryptography and hardware-held private keys (#4192)
Makes the stack's cryptographic operations replaceable - by another library, an offboard service, or hardware (TPM 2.0 / HSM / PKCS#11 token / cloud KMS) - with no performance cost in the default configuration, and extends this to certificates so a private key need never exist in process memory.

Also fixes three pre-existing defects found while validating: STACKGEN001 erroring for every compilation the stack generator was merely loaded into, certificates failing to load from paths past MAX_PATH on Windows, and the MonitoredNode2 tests starving the thread pool.

Fixes #4190
2026-08-09 12:40:26 +02:00
Marc Schier 168b3de353 Replace the RequestManager AsyncLocal guard with an explicit operation context (#4163)
# Description

`RequestManager` tracked whether the calling flow was serving a Client
request in a `private readonly AsyncLocal<bool> m_inServiceDispatch`. It
was set by a `StandardServer.ProcessRequestAsync` override and read by
`NodeManagerLifecycle`, so that a lifecycle operation (`AddAsync` /
`ReloadAsync` / `RemoveAsync`) started from inside a request fails fast
with `InvalidOperationException` instead of draining — and therefore
waiting for — its own request.

Two problems with that mechanism, raised in review of #4093:

1. An `AsyncLocal<T>` written inside an `async` method never flows back
to the caller that awaited it, so every scope had to chain to the
previous value by hand and restore it on dispose. Subtle, and easy to
get wrong when a new entry point is added.
2. Ambient state is invisible at the call site, so the lifetime of the
mark could not be reasoned about from the code that depended on it —
especially once work is handed to a background task.

`RequestManager` also had no business carrying ambient state on behalf
of a different subsystem; its job is tracking requests for the drain.

## What changed

The guard is now an **exact identity lookup** against the request
registry `RequestManager` already maintains, and the operation is
threaded **explicitly**:

* **`RequestManager`** — `internal bool
IsExecutingRequest(IOperationContext?)` performs a reference-identity
lookup against `m_requests` under the existing `m_requestsLock`.
`m_inServiceDispatch`, `EnterServiceDispatchScope()`, the nested
`ServiceDispatchScope` class and the flag-based `IsExecutingRequest`
property are gone.
* **`StandardServer`** — the `ProcessRequestAsync` override is removed;
it existed only to open the dispatch scope.
* **`INodeManagerLifecycle`** — every member (`AddAsync` ×2 factory
kinds, `ReloadAsync` ×2, `RemoveAsync`) now takes the caller's
`IOperationContext?` directly, ahead of `CancellationToken ct =
default`. No overloads and no parallel signature are kept: the interface
is new in 2.0 and outside the 1.5.378 compatibility rule, so call sites
state their context explicitly or pass `null`.
* **`HostedNodeManagerLifecycle`** and
**`RuntimeNodeSetLifecycleExtensions`** thread the argument through;
`AddRuntimeNodeSetAsync` / `ReloadRuntimeNodeSetAsync` remain single
methods.
* **`SystemContextOperationExtensions.GetOperationContext(this
ISystemContext)`** (new, `Opc.Ua.Core`) lets a NodeManager or Method
callback hand over the operation it already received without
downcasting. It switches on `SessionSystemContext` and `SystemContext`,
the two independent roots every context derives from. No new interface
is introduced: casting the context to `IOperationContext` would not do,
because a context implements that by delegating to the operation it was
created for, so the cast yields the context rather than the operation.
* **`NodeManagerLifecycle.EnsureNotRequestCallback`** no longer consults
`ServerState`. The previous `CurrentState == Running` predicate could
let a genuinely re-entrant call slip past while the server was shutting
down; the request registry answers exactly in every state.

A NodeManager or Method callback now reads:

```csharp
await m_lifecycle.ReloadAsync(m_registration, replacement, context.GetOperationContext(), ct);
```

and a control-plane caller (hosted service, DI consumer) passes `null`.

## Behaviour

Unchanged for the case the guard exists to catch: a lifecycle call made
on behalf of an executing request is still rejected up front with the
same `InvalidOperationException` and the same message.

Two deliberate differences:

* **Improvement** — an internal `OperationContext` that was never
enrolled as a Client request is now correctly allowed through. The
boolean flag could not distinguish it from a real request.
* **Accepted trade-off** — a caller inside a request that passes no
operation is no longer detected at entry and falls back to the
pre-existing bounded `RequestManager.RequestDrainTimeout`, which already
covered any request that bypassed the service pipeline. Automatic
detection for an uncooperative caller inherently requires ambient state,
which is exactly what this change removes. Documented in
`docs/NodeManagers.md`.

## Alternatives considered and rejected

| Alternative | Why rejected |
| --- | --- |
| Delete the guard, rely only on `RequestDrainTimeout` | Turns a clear
immediate error into a multi-minute hang. |
| Infer the caller from the `CancellationToken` it passes | Works only
for the exact request token, breaks on linked tokens; ambient magic of a
different kind. |
| In-flight callback counter on `MasterNodeManager` | False positives:
rejects legitimate control-plane calls whenever any request is
concurrently dispatching. |
| Defer the drain to a background task so re-entrancy cannot deadlock |
The drains are woven into transactional commit / rollback and binding
reconciliation; deferring them breaks the transactional guarantees. |
| `Activity.Current` / `ThreadLocal` | The same ambient magic the issue
asks to remove. |

## Testing

* `RequestManagerTests` — the three `EnterServiceDispatchScope` tests
are replaced by coverage for `IsExecutingRequest(context)`: null,
never-registered, only-while-scope-open, visible across an `await` and a
background `Task.Run`, and two concurrently executing requests not
confusing each other.
* `NodeManagerLifecycleTests` — rejection from an executing request for
Add / Reload / Remove without invoking the factory, acceptance for a
context that is not an executing request, acceptance once the request
has completed, and the full callback shape
(`ServerSystemContext.Copy(operation)` → `GetOperationContext()` →
rejected).
* `HostedNodeManagerLifecycleTests` — forwarding tests for the
caller-context argument on all five members.
*
`tests/Opc.Ua.Core.Tests/Stack/State/SystemContextOperationExtensionsTests.cs`
(new) — `GetOperationContext` across both context roots and the
null-argument guard.

## Related Issues

- Fixes #4149

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 14402d11-e540-4faa-a5cd-94881bd7ce85
2026-08-03 20:31:51 +00:00
Marc Schier f0ab7878ea Core stack: reusable file directory binder, geolocation seam and state machines (#4123)
**Stack 1/5** — base `master`. Part of the #4096 split; see #4096 for
the overall picture.

## What this is

The core stack changes the Robotics, OpenUSD and Positioning work
depends on. No new projects; every change is to an existing one, so this
is reviewable without any of the companion specs.

## Contents

**`Opc.Ua.Server`**
- `FileSystem` gains a reusable `IFileDirectoryBinder` and
`IFileSystemHost`, so any node manager can serve a Part 5
`FileDirectoryType` over `IFileSystemProvider` instead of that binding
living inside one companion model.
- Entry names are validated before they reach the provider. The contract
promises forward-slash provider-relative paths but never enforced it, so
a backslash payload such as `..\..\..\Windows\System32\evil.txt`
bypassed the obvious provider-side check on Windows. Defence in depth —
the shipped `PhysicalFileSystemProvider` was already safe.
- `Location` adds a single `IGeoLocationProvider` seam so one provider
implementation serves both OPC 10000-211 GPOS and OPC 10030 ISA-95. The
two specs disagree on representation, not on content, so the difference
is a projection rather than a reason to write the provider twice. Ships
a WKT text formatter and an in-memory provider.
- `StateMachines` adds `FiniteStateMachineDispatcher`, the Part 16
transition guard used by DI software update and later by Robotics task
control.

**`Opc.Ua.Types`**
- Node copies no longer consume NodeIds from the `NodeIdFactory`.
`Initialize` materialises children through `CreateChild`, which assigns
a per-instance NodeId, but the copy then overwrites every child NodeId
from its source, so the assignment is discarded. A counting factory
silently burned identifiers; a tracking one leaked them.
- `JsonDecoder` resolves a relative encodeable type id against the
namespace table before the factory lookup.

**`Opc.Ua.Client`** — a file system path segment written without a
namespace prefix is qualified with the namespace of the parent it is
resolved against, matching the provider-owned BrowseNames.

**`Directory.Build.targets`** — the `RestrictForLegacyTfm` no-op now
also applies when a project pins a single `TargetFramework` and
`CustomTestTarget` names a different one. That is the same situation it
already handled for the legacy frameworks, and it is what keeps the
per-TFM CI matrix green once net10.0-only projects appear later in the
stack.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.Types.Tests | 8088 / 0 |
| Opc.Ua.Server.Tests | 3873 / 1 |
| Opc.Ua.Client.Tests | 2105 / 0 |
| Opc.Ua.Core.Tests | 4145 / 0 |
| Opc.Ua.Features.Tests | 134 / 0 |

The single `Opc.Ua.Server.Tests` failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`,
which is pre-existing and environmental: the PKI path in my working
directory exceeds MAX_PATH. It reproduces on clean `master` and passes
from a shorter directory.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
2026-07-31 13:13:47 +02:00
Marc Schier 8a10139b8c Fix OPC UA address-space compliance in the device integration stack (#4117)
# Description

A live OPC UA compliance audit of `samples/PumpDeviceIntegrationServer`
against the **OPC 40223 Pumps** and **OPC 10000-100 (DI)** companion
specifications found **13 address-space violations** plus a
serialization bug in the MCP tool. Every fix lands at its **root cause
in the shared stack** rather than as a workaround in the sample, so all
servers benefit.

The sample now exposes **N fully simulated pumps** (`--pumps N`) and
advertises the DI conformance facets it actually satisfies.

> This branch has been merged with `master` including **#4116** ("Add
simulation to PumpDeviceIntegrationServer and Event modelling"), which
independently reworked the same pump simulation and the same fluent
alarm builders. See [Relationship to #4116](#relationship-to-4116) below
for how the two were reconciled.

## Findings and fixes

| # | Finding | Root cause | Fix |
|---|---------|-----------|-----|
| 1 | `OverTempAlarm` created by the fluent API did not exist in the
address space | `AttachAlarm` attached the alarm with `AddChild` but
never indexed it into the node manager | Register the node; the same gap
existed in `AddObject`, `CreateInstance` and the state-machine creators
|
| 2 | Alarms were unsubscribable — `EventNotifier = 0` everywhere, no
`HasNotifier` from `Server` | No event-source wiring at all | Set
`HasCondition`, initialise
`SourceNode`/`SourceName`/`ConditionName`/`InputNode`, promote
`EventNotifier` up the ancestor chain, publish `HasNotifier` from the
`Server` object |
| 3 | Every instance NodeId was minted in the **DI standard namespace**
(`ns=4;s=5001_Pump #1`) | `DiNodeManager.New` inherited the parent's
namespace index | New `DiNodeManager.InstanceNamespaceIndex`; instances
move to the server's application namespace |
| 4 | `Server/Namespaces` described only 2 of 6 namespaces | The
NodeSet2 importer drops nodes parented to namespace 0, so companion-spec
metadata objects never reached the address space — and no route covered
the server's own namespace | New `NamespaceMetadataPublisher` walks the
`NamespaceArray` and fills version/publication date from
`ModelDependencyAttribute` |
| 5 | Vendor BrowseNames (`Diagnostics`, `LastError`, …) sat in the DI
namespace | Builders defaulted to the parent's namespace | Default to
the server namespace; **spec-defined BrowseNames are unchanged** |
| 6 | `AccessLevel="5"` from the Pumps NodeSet was reported as `1` | The
ModelDesign `AccessLevel` enum is not `[Flags]` and cannot represent
`CurrentRead \| HistoryRead` | Carry the verbatim NodeSet2 bitmask and
emit named `AccessLevels` constants |
| 7 | `Historizing = true` while `HistoryRead` returned
`BadHistoryOperationUnsupported` | Nothing reconciled the declared
history surface with the wired historians | Startup clears the
advertisement (including masking the attribute read callbacks) when no
provider resolves; the sample wires a real historian |
| 8 | Mandatory `ProductInstanceUri` unset | Sample omission | Populated
on every pump |
| 9 | Mandatory `TrueState`/`FalseState` empty on every supervision
boolean | Absent in the official NodeSet; a server must still populate
them | Populated on every pump |
| 10 | Machinery `Machines` folder empty (OPC 40001-1 §9.2 says
**shall**) | `TryAddToMachinesFolder` used `AddChild`, which
*re-parents* the device away from `DeviceSet` | Add the `Organizes`
reference instead |
| 11 | BrowseName contained a space and `#` | Sample naming | `Pump_1`
with the readable label in `DisplayName` |
| 12 | `Maintenance` group materialised but empty | Sample omission |
Populated |
| 13 | Uninitialised values reported `Good` | No initial-value status |
`BadWaitingForInitialData` until the first simulation tick |
| — | `ServerProfileArray` advertised only `StandardUA2017` |
`DiNodeManager` did not implement the existing `IConformanceContributor`
| Contribute DI 1.05 server facets computed at runtime from what is
actually wired, merged with `StandardUA2017` |
| — | MCP tool serialised boolean `false` and numeric `0` as JSON `null`
| `Variant.Null` is `default`, so `Variant.Equals` adopts the non-null
type and compares against the default value | Dispatch on `BuiltInType`
and use typed `TryGetValue` accessors instead of the prohibited
`AsBoxedObject` |

## Relationship to #4116

#4116 landed on `master` while this branch was in flight and targeted
overlapping ground. The merge is a **union of both intents**, not a
fast-forward:

- **`AlarmBuilderExtensions`** — kept both sides. #4116 contributes the
parent-must-be-an-Object guard, `SetEnableState`, and the
`HasEventSource` references; this branch contributes the `HasCondition`
reference, condition source initialisation, **registration of the alarm
with the node manager so it is browsable**, notifier promotion up the
whole ancestor chain, and root-notifier registration.
- **`SupervisionBuilderExtensions`** — took #4116's `SetAlarmActive`. It
is more spec-correct than this branch's version: it honours
`EnabledState`, resets Acked/Confirmed on activation, and computes
`Retain` per OPC 10000-9 so an unacknowledged Condition stays retained
after going inactive. This branch's null guard was kept so a malformed
alarm cannot dereference a missing `EnabledState`.
- **`PumpNodeManager.Configure.cs`** — took #4116's **push-based**
simulation (`RegisterPumpSimulation`, `CreatePumpSimulation`,
`IValueUpdater<T>`, `Initialize`/`Advance`/`Publish`, deterministic
per-pump phase offsets) as the structure, then folded in this branch's
compliance work: configurable pump count, Maintenance group,
`TrueState`/`FalseState`, historian wiring, per-pump mandatory
Identification values, and `BadWaitingForInitialData` until the first
tick.
- **README** — union, keeping #4116's address-space validation workflow
section.

Two test adjustments were needed, both because the merge legitimately
changed behaviour the tests pinned:

- `PumpHostedReferenceTests` now resolves pump NodeIds from the browse
result instead of hard-coding the DI namespace, and waits for the first
published value. `PumpCreatedAfterStartupJoinsTheLiveSimulationAsync`
was **added** to restore #4116's coverage that a pump created after
startup through `ConfigureDevicesFor` joins the live simulation.
- `FluentAlarmRegistrationIntegrationTests` no longer asserts that
`Retain` clears when an alarm goes inactive — that pinned this branch's
weaker semantics, and #4116's Part 9 behaviour is correct.

## Breaking change

Runtime-created DI device instance NodeIds move from the companion-spec
namespace to the server's application namespace, e.g.
`ns=<DI>;s=5001_Pump #1` becomes `ns=<application>;s=5001_Pump #1`. This
invalidates NodeIds cached by clients from earlier 2.0 previews. It is
**not** a model change — standard DI/Machinery/Pumps type NodeIds and
spec-defined BrowseNames are untouched.

Device integration hosting is new in 2.0, so there is nothing to migrate
from 1.5.378 and this is deliberately *not* in `docs/MigrationGuide.md`.
Anyone tracking 2.0 previews should rediscover devices by browsing
`DeviceSet` rather than persisting NodeIds, and resolve namespace
indexes from `NamespaceArray` per connection.

## Verification

Compliance is pinned by regression tests that assert **through the
server's service surface** (`BrowseAsync` / `ReadAsync` /
`HistoryReadAsync`), not by inspecting `NodeState` objects in memory.
That distinction mattered: three defects survived in-memory assertions
and were only caught through the read path — most subtly, `Historizing`
was cleared while `AccessLevel` still advertised `HistoryRead`, because
attribute read callbacks re-applied the NodeSet bits.

Post-merge on net10.0:

| Suite | Result |
|---|---|
| `Opc.Ua.Di.Tests` | 326 passed |
| `Opc.Ua.Server.Tests` | 3846 passed, 5 skipped |
| `Opc.Ua.Tools.Tests` | 288 passed |
| `Opc.Ua.History.Tests` | 506 passed, 27 skipped |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |

The full solution was also built and tested on **net48** before the
merge.

Review feedback from `copilot-pull-request-reviewer` has been addressed
in commit `badad31dc`: the shared `ServerObjectState` lock was removed
(the forward `HasNotifier` edge is published through the owning node
manager instead), MCP array serialization now preserves element types,
and the pump option guards report the specific rejected option. The only
failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
pre-existing certificate-store issue that **passes when run in
isolation** and is unrelated to this change.

## Documentation

Updated `docs/DeviceIntegration.md`, `docs/MigrationGuide.md`,
`docs/HistoricalAccess.md`, `docs/DeveloperGuide.md`,
`docs/McpServer.md`, `docs/README.md`,
`samples/PumpDeviceIntegrationServer/README.md` and
`tools/Opc.Ua.Mcp/README.md`, and consolidated the four node-manager
documents into a single [`docs/NodeManagers.md`](docs/NodeManagers.md) —
overview, built-in managers (including `MasterNodeManager`), core vs
custom, registration, server address-space metadata, and source
generation — with a TOC and all inbound links repointed.

## Related Issues

No tracking issue exists for this work yet — it originated from an
ad-hoc compliance audit of the pump sample. Given the size of the change
and the runtime-breaking NodeId move, please open (or link) a tracking
issue so the design is recorded as an ADR before merging.

- Fixes #

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d50c3fc-58b1-4c99-85a4-1e32c88666d9
2026-07-30 16:04:18 +02:00
Marc Schier 275c3800b5 [Server] Add live NodeManager lifecycle support (#4094)
# Description

Adds supported live add, reload, and remove lifecycle operations for
NodeManager instances on a running server.

The change introduces a DI-backed `INodeManagerLifecycle` service with
direct `StandardServer` access, generation-aware registration handles,
copy-on-write routing, transactional prepare/commit/rollback, runtime
NodeSet convenience APIs, append-only namespace updates, model and
semantic change notifications, additive-compatible complex type priming,
and complete teardown behavior.

Lifecycle publication is coordinated with requests, sessions,
subscriptions, monitored items, continuation points, and shutdown so
retired managers cannot remain reachable or be disposed while still in
use. Compatible monitored items transfer to replacement generations
without a transient bad status. Removed or incompatible Nodes publish
`BadNodeIdUnknown` as required by OPC UA Part 4 section 5.8.4.1, retain
their MonitoredItem identity, and automatically recover when a
compatible Node with the same NodeId returns.

Registration is documented end to end in the new
[docs/NodeManagerRegistration.md](docs/NodeManagerRegistration.md),
which covers compile-time, startup, and runtime registration together
with the guarantees for MonitoredItems, Browse continuation points,
namespace indexes, DataTypes, and change notifications.
## Review feedback addressed

Interface surface reduced, as requested:

- `INodeManagerMonitoredItemTracker` folded into
`ISubscriptionMonitoredItemLifecycle`.
- `INodeManagerMutationCoordinator` and the asynchronous MonitoredItem
recovery entry point folded into `IDynamicNodeManagerHost`.
- `ISessionClosingRegistry` folded into `IServerInternal`, then replaced
by `ISession.IsClosing`.
- `ISubscriptionDeletionRegistry` folded into `ISubscriptionManager`,
then removed entirely.
- `INodeManagerContinuationPointTracker` folded into `ISession`.
- The synchronous recovery entry point kept on its own
`ISyncNodeManagerMonitoredItemRecovery`, because `AddPredefinedNode`
cannot await; it still routes through the asynchronous
`INodeManagerMonitoredItemLifecycle` that `AsyncNodeManagerAdapter`
exposes.
- `IMonitoredItemLifecycle` renamed to `IDetachableMonitoredItem`.

Reach into the Subscription area reduced to the minimum the feature
needs:

- The lifecycle feature itself leaves `SubscriptionManager` almost
untouched: two guards that reject a request from a Session that is
closing, on `CreateSubscription` and on `TransferSubscriptions`, because
it would create state that is about to be torn down. The deletion claim
registry, the session closing gates, the closed-session bookkeeping and
the deletion restore paths introduced by earlier revisions are gone.
Transferring a subscription away from a session that is closing stays
allowed, since that is the recovery path the service exists for; the
race that exposed is fixed by serialising source-queue closure with the
transfer rather than by rejecting it.
- Queueing is disabled again for a MonitoredItem with `QueueSize <= 1`,
as before this PR. Without a queue the Client is served the last sampled
value, so the missing-Node notification simply becomes that value;
losing values is expected for a MonitoredItem without queuing.
- What remains of the notification is one `DataValue` plus one bool in
`DataChangeQueueHandler`, mirroring the existing overflow pair, which
stops it being the value discarded when a configured queue is full.
- `MonitoredItem` keeps its original publish path, its original queue
creation and restore paths, its original triggering behaviour, and no
per-epoch bookkeeping.
- `IStoredMonitoredItem` carries `IsDeleted` and `IsDetached`, so a
restored item reports the state it was stored with rather than one
inferred from the last value's StatusCode.

Other feedback:

- Documented `HostedNodeManagerLifecycle`, `IDynamicNodeManagerHost`,
`NodeManagerRoutingTable`, and the `RequestManager` validation and
request scopes.
- Renamed `ValidateMonitoredItemAsync` to `CanAttachMonitoredItemAsync`
and `RestoreMonitoredItemAsync` to `RecoverMonitoredItemAsync`, so the
lifecycle API no longer collides with the durable Subscription
`RestoreMonitoredItem` path used at server startup.
- Removed the `CustomNodeManager2` dependency from the synchronous
MonitoredItem recovery path.
- `DataChangeQueueHandler` is back to the two constructors 1.5.378
shipped, each taking an optional `TimeProvider`.
- Session closing is a one-way latch, and the Session is removed even
when teardown throws.
- Documented what a custom NodeManager has to implement to take part in
live lifecycle operations, and which NodeManager implements
`INodeManagerReloadParticipant` today.

## Fixes found during review

- The guard that stops a NodeManager lifecycle operation being started
from inside a service handler never fired. It was backed by an
`AsyncLocal` written inside `async ValidateRequestAsync`, which is
invisible to the caller that awaited it, so a lifecycle call from a
NodeManager callback waited for its own request and held the lifecycle
semaphore forever. The mark is now applied by the service dispatcher,
and the drain is bounded so no path can wedge the lifecycle permanently.
- `ValidateRequestAsync` is no longer virtual. An override that rejected
a request after calling `base` left it registered forever; subclasses
now use `OnRequestValidatedAsync`, whose failures complete the request.
- A MonitoredItem deleted and disposed while a reload was handing it to
the replacement NodeManager could be attached anyway. The item now
reserves itself for the hand-over and the transition takes it back if it
was disposed meanwhile.
- The Overflow bit is recorded and applied when the value is published,
as the surrounding design does, rather than written into the value at
enqueue time.
- MonitoredItem cleanup for a Subscription being deleted no longer
passes null entries to `DeleteMonitoredItemsAsync`.
- Browse continuation points of a retired NodeManager are persisted and
disposed outside the Session lock.

## Open review discussions

Three threads are open, replied to and awaiting reviewer decisions:
separating service-call dispatch from NodeManager handling in
`MasterNodeManager` (tracked as #4098), the request-scoping shape in
`StandardServer`, and how much of the async MonitoredItem service
implementation should stay in `Subscription`.

Issue #4102 records a specification ambiguity found while implementing
the missing-Node notification: Part 4 §5.8.4.1 makes it mandatory, but
the protected over-capacity queue entry §5.13.1.5 defines applies to
`EventQueueOverflowEventType` only, so the specification does not say
how it survives a full queue.

## Validation

- Full `Opc.Ua.Server.Tests` on net10.0: 3,768 passed, 5 skipped.
- Lifecycle, MonitoredItem, Subscription and continuation point suites:
185 passed on net10.0 and 75 on net48.
- Server builds complete with zero warnings or errors.
- Two failures are pre-existing and unrelated:
`DurableDataValueQueueVerifyReferenceBatchingAsync` (flaky
`BatchPersistor` file contention, passes in isolation on net10.0 and
net48) and
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`
(fails on a clean tree with these changes stashed).

## Related Issues

- Fixes #3993
- Follow-up tracked in #4098
2026-07-28 16:06:28 +02:00
Marc Schier e5cb64cb62 Make fluent sample security settings explicit (#4087)
# Description

Makes security-critical configuration explicit in the modern fluent
client and server samples instead of relying on hidden defaults.

- Adds an application-wide `ConfigureSecurity` DI callback for advanced
certificate validation settings.
- Makes PKI roots and certificate validation settings explicit in the
Pump, Boiler, Calculator, and Minimal Client samples.
- Makes the Calculator sample's endpoint policies and anonymous
authentication choice explicit.
- Adds consolidated developer guidance for application certificates,
endpoint security, identity, resource controls, and every advanced
security-builder setting.
- Preserves the samples' existing automatic-trust behavior while clearly
marking it as sample-only.

## Related Issues

- Fixes #4085

## 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.
- [ ] I have addressed **all** PR feedback received.

Copilot-Session: 833a8855-af3c-4a85-887b-32bf38d92cfa
2026-07-24 12:56:51 +02:00
Copilot b76022d13d Add MinimalClient - lightweight OPC UA console client with fluent API (#4006)
# Description

Adds `MinimalClient`, a compact console OPC UA application demonstrating
the unified dependency-injection surface, secure endpoint discovery,
managed sessions, fluent V2 subscriptions, and Alarms & Conditions
support.

## Changes

- Adds `ConfigureApplication(options => ...)` to build and validate one
shared `ApplicationConfiguration` for combined client/server hosts.
- Extends `OpcUaClientOptions` so the existing `AddClient(options =>
...)` callback accepts application identity, PKI, and
certificate-validation settings like `AddServer(...)`.
- Composes root `ConfigureApplication(...)` values with client defaults
in either registration order; shared values take precedence and client
values fill gaps.
- Keeps server samples on their established `AddServer(...)`
configuration pattern.
- Adds `MinimalClient` as a .NET 10 Native AOT console application using
top-level statements and `HostApplicationBuilder`.
- Discovers a `SignAndEncrypt` / `Basic256Sha256` endpoint by default;
`--insecure` and `--auto-accept` are explicit opt-ins with warnings.
- Demonstrates fluent `AddSubscription(...)`,
`TryAddMonitoredItem(...)`, and `AlarmClientFactory` usage.
- Adds targeted configuration, client, and combined client/server tests.

## Example

```csharp
builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "MinimalClient";
        options.ApplicationUri = "urn:localhost:OPCFoundation:MinimalClient";
        options.ProductUri = "uri:opcfoundation.org:MinimalClient";
        options.Session = new ManagedSessionOptions
        {
            SessionName = "MinimalClient",
            SessionTimeout = TimeSpan.FromSeconds(60)
        };
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = discoveryUrl;
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    })
    .AddSubscriptions()
    .AddAlarms();
```

## Related Issues

_If there is no issue yet, open one and link it here._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce new build or
analyzer warnings.
- [ ] I ran all tests locally using the `UA.slnx` solution against at
least .NET Framework and .NET 10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
2026-07-17 19:36:53 +02:00
Marc Schier 8538f531e7 Update paths for the consolidated repository layout 2026-07-16 06:26:05 +02:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00