18 Commits

Author SHA1 Message Date
romanett 8c52cba0b2 CTT reference server: faster alarm cycle in --ctt mode, MinSubscriptionLifetime 1000 ms (#4766)
Shortens CTT runs against the reference server without changing what the
CTT verifies.

## Changes
- **Alarm simulation step in `--ctt` mode: 1000 → 500 ms.**
`AlarmNodeManagerFactory.SimulationInterval` (default stays 1000 ms) is
passed to `AlarmNodeManager`; `ConsoleReferenceServer --ctt` sets it via
`Quickstarts.Servers.Utils.ConfigureCttModeNodeManagers()` to
`Utils.CttAlarmSimulationInterval` (500 ms) before the node managers are
created. A full alarm cycle (40 steps through all limits) takes 20 s
instead of 40 s. Every alarm still passes through all states and limits;
the alarm NodeIds carry the interval
(`…ExclusiveLevelAlarmType.500.Alarm` in CTT mode).
- **`Ctt.ReferenceServer.Config.xml`: `MinSubscriptionLifetime` 10000 →
1000 ms.** CTT scripts request short subscription lifetimes on purpose
and then wait for them to expire (Subscription Basic 063/016, Durable
012, Minimum 02 004, Publish Min 05 Err-001, Diagnostics 024); the 10 s
floor inflated those waits. Part 4 sets no minimum; the server still
enforces lifetime ≥ 3 × keep-alive.

## Measured (patched CTT 1.05.513 scripts)
| Part | Before | After |
| --- | ---: | ---: |
| A&C (p18), alarm step only + CTT Alarm Cycle Time 20 | 932 s | 576 s |
| Subscriptions (p16), MinSubscriptionLifetime only | 679 s | 579 s |

Combined full run with the matching CTT-side changes (ReadyToRun server
build, publish interval 150 ms, SecureChannel min lifetime 1000 ms, A&C
Refresh script waits): **72 → 54 min**, 0 regressions against the CTT
baseline (the only error is the known CTT-binary defect GDS 019).

The CTT CI repo derives its `Alarm Cycle Time` from
`Utils.CttAlarmSimulationInterval`, so runs against branches without
this change keep the old 30 s setting.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:49:18 +02:00
romanett 28f1401324 Server: cut request-path lock contention and allocation; add load harness (#4707)
Profiling the reference server under load showed most request-path CPU
spent spinning on contended locks, plus several avoidable allocations
per request or per node. This PR removes them, switches the reference
server to Server GC, and adds the load harness used to measure it.

## Results

Paired A/B against `master` (3 alternating rounds, medians, 20 sessions,
workstation GC on both sides, 24-core machine):

| Scenario | Throughput | p50 | p90 | Server CPU / op | Server alloc /
op | Lock contention |
|---|---|---|---|---|---|---|
| Read, 1 value | **+45%** (94k → 137k req/s) | −20% | −46% | −37% |
−39% | −94% |
| Read, 100 values | **+40%** | −26% | −17% | −24% | −25% | +130% ¹ |
| Read, 100 values, SignAndEncrypt | **+48%** | −36% | −20% | −30% |
−26% | +106% ¹ |
| Browse (Server object) | **+33%** | −31% | −12% | −13% | −14% | −40% |
| Write, 100 values | +13% | −12% | −4% | −10% | −19% | −23% |
| Data-change subscriptions | ±0 ² | ≈ | ≈ | −14% | −17% | −29% |

¹ The faster server spends more time in the remaining per-node locks.
² Throughput is bounded by the writer rate in this scenario.

Separately, Server GC instead of workstation GC doubles Read-100
throughput (21k → 44k req/s) and cuts p90 from 2.1 ms to 0.6 ms, because
workstation GC paused the server for about half the wall time under
load. The working set grows from ~210 MB to ~430 MB.

## Changes

- **`ObjectPool<T>`**: lock-free slot array. `ConcurrentBag.Count` took
every per-thread lock on each `Return` of the pooled response value-task
source, once per request.
- **`FairRequestQueue`**:
- drops redundant gate entries: the fail-fast pre-checks and the stopped
check read volatile fields and are re-checked under the gate; the owner
count is folded into selection;
- coalesces wake pulses with a pending flag, so the bounded signal
channel's lock is only taken when no token is outstanding. Wake-count
semantics in the tests are unchanged.
- **`DefaultServerResourceIsolationProvider`**:
  - compares peer addresses without `GetAddressBytes()` allocations;
- builds the unsecured verification context only when a classifier is
configured.
- **TCP listener / channel**: caches the certificate DER copies per
channel, keyed on the certificate instance. `X509Certificate2.RawData`
returns a fresh copy on every access, and the listener read it for every
request.
- **`EndpointBase`**: resolves the request `ActivitySource` once instead
of `Assembly.GetCallingAssembly()` per request.
- **Node managers**:
- one `ServerSystemContext` copy per request (cached on the
`OperationContext`) for per-node callbacks: permission metadata, node
metadata, view membership, method lookup;
  - removes a discarded copy in `GetReferenceDescriptionAsync`;
- per-request operation caches are plain `Dictionary` instead of a
`ConcurrentDictionary` with 24 lock objects;
  - the validation cache is built in one pass;
  - fixes zero-capacity error lists.
- **`ResourceManager`**: translation lookups read an immutable snapshot
that is rebuilt after writes. Browse serialised on the global lock for
every display name.
- **`MonitoredItem`**: shares the default `DataChangeFilter` instead of
allocating one per sampled value.
- **Reference server**: `ServerGarbageCollection=true`; new "Garbage
collector mode" section in `docs/ServerScalability.md`.
- **`perf/`**: `ServerLoadHarness` (out-of-process server plus load
driver, server-side CPU/GC/alloc/contention deltas, optional
`dotnet-trace` capture), `TraceAnalyzer` (TraceEvent-based
CPU/allocation/contention aggregation), and paired A/B scripts. Not part
of `UA.slnx`.

### Behaviour notes for custom node managers
- The `IDictionary<NodeId, NodeState>` operation cache handed to
overrides is now a `Dictionary`. It was a `ConcurrentDictionary`, but
only one request flow uses it.
- Within one request, per-node callbacks of a node manager share one
`ServerSystemContext` copy.

### Later commits on this PR
- **Permission checks without `NodeMetadata`** (`9068684c3`): the
role-permission and access-restriction validators run on an internal
`PermissionMetadata` struct; `AsyncCustomNodeManager` fills it on the
stack from the same attributes and namespace defaults. Browse's
reference filter reads 3 attributes per target instead of 13. Read 100
values: 120.6 → 95.7 KB/request, ~21k → 26k req/s; Browse 67.6 → 51.8
KB, 26.7k → 29.8k req/s.
- **Read-only channel evidence** (`936b28d76`, `dc638ab9f`):
`SecureChannelContext.ClientChannelCertificate`,
`ServerChannelCertificate` and `ChannelThumbprint` are `ByteString`, so
the cached per-channel certificate buffer is shared without a copy and
cannot be altered by handlers or classifiers. **Public API change**, see
the new section in `docs/MigrationGuide.md`. The isolation provider's
revalidation snapshot still copies.
- **Session lookup** (`936b28d76`):
`SessionManager.TryGetSessionContext` takes the candidate snapshot
without the bindings lock; the validating check under the lock is
unchanged (2 instead of 4 global lock acquisitions per request).
- Unit tests for `ObjectPool`, the per-request system context cache, the
translation snapshot, `SecureChannelContext` null semantics and a
stopped `FairRequestQueue`; `perf/` follows the repository conventions.

## Testing
- `Opc.Ua.Server.Tests` (net10.0): 8386 passed before the `ByteString`
change. After it, 13 secure-session and isolation tests failed; both
causes are fixed in `dc638ab9f` and those tests (25 with their variants)
pass. A full rerun of the Server, Sessions, Core, Redundancy, LDS and
GDS suites on the final commit is in progress.
- Full solution builds for net10.0 and net48; `dotnet format whitespace`
clean.

### o6-automation opcua-benchmarks (sampling, deep pipelines)
Replicated both tests of the [o6-automation
benchmark](https://github.com/o6-automation/opcua-benchmarks) in the
harness (`--scenario counters`, and `read` with 10 clients × N
outstanding), paired against `origin/master`:

| Test | master | this PR |
|---|---|---|
| Subscription sampling, 100 Hz, every item ≥ 99 % of samples, 1 item |
FAIL (75–94 %) | PASS (100 %) |
| same, 1024 items | FAIL (64–73 %) | PASS (99.9 %) |
| same, capacity | — | between 4096 and 6144 items (~410–520k samples/s)
|
| Read, 10 clients × 100 outstanding, None | stalls (0–1.7k req/s) |
121–131k req/s |
| Read, 10 clients × 100 outstanding, SignAndEncrypt | stalls (0 req/s)
| 89–91k req/s |
| Read, 10 clients × 1000 outstanding | channels faulted
(`BadTcpNotEnoughResources`) | 0 errors, ~45–54k req/s |

- **Fixed-rate sampling** (`30b769d13`): the sampling loop waited one
interval after each sample, adding timer overshoot and sampling time to
every cycle; samples now have absolute deadlines.
- **Async response send** (`cdac5c5ca`): `SendResponse` blocked
thread-pool workers on the channel gate; with many requests in flight
the receive loop that was handed the gate waited for a pool thread and
the channel stalled. The TCP, Kestrel TCP and WebSocket listeners now
use `SendResponseAsync`.
- **Backpressure instead of channel fault** (`191cd2ba7`): server
channels stop reading while more than 100 responses are queued and
resume at 50, instead of faulting the channel. Client channels keep
reading. At 10,000 outstanding requests throughput is ~50k req/s with or
without the limit engaged, so the remaining gap to the 100-per-client
case is not the backpressure.

## Not changed
- `SentMessageQueue` deep-clones every `NotificationMessage`. Retained
messages return their pooled payloads on acknowledgement while the
response copy may still be queued for send, and a client can acknowledge
a sequence number before receiving it; the clone keeps a recycled
payload from reaching another session's response. Removing it needs a
send-completion hook from the transport.
- Generated responses allocate a default `ResponseHeader` that is
replaced (~2-3 % of per-request allocation). Fixing it means making
structure fields of every generated type lazily initialized, a
generator-wide change to default values, equality and encoding.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:00:49 +02:00
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

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

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00
romanett 5572107ab8 Push Model compliance: UpdateCertificate issuer check, TrustList audit events, NoConfiguration state, DefaultHttpsGroup (#4684)
# Description

Fixes found by a manual run of the OPC 10000-12 *Push Model for Global
Certificate and TrustList Management* test plan (183 test cases,
TC0–TC182) against the Reference Server. After these changes no test
case fails; the remaining non-pass verdicts are test-plan expectations
that contradict Part 12/Part 4/Part 20 (documented in the run report).

### Server behaviour changes

- **UpdateCertificate requires the issuer in the TrustList**
(`ConfigurationNodeManager.PushValidation.cs`). The validator classifies
an incomplete chain as suppressible; OPC 10000-4 Table 100 does not ("An
error during the chain creation may not be suppressed") and §7.10.5
requires the issuers to already be in the group's TrustList. A
certificate of an unknown CA is now rejected with
`Bad_CertificateChainIncomplete`. Expiry / host-name errors stay ignored
as §7.10.5 requires.
- **TrustList audit events reach the Server Object.**
`TrustListUpdateRequested`/`TrustListUpdated` were reported on the
TrustList node (`EventNotifier=0`, not linked to the Server), so no
client could receive them. `TrustList.SetAuditEventServer` routes them
through `IAuditEventServer`; wired for the Push TrustLists and the GDS
CertificateGroup TrustLists. New `AuditEvents` overloads keep the
node-based ones.
- **Duplicate AddCertificate** no longer makes the following
ApplyChanges fail with a bare `Bad` (store refused the second copy); the
commit is idempotent and its rollback keeps the pre-existing
certificate.
- **CloseAndUpdate validates CRLs** (incl. CRL-only updates): every CRL
must be signed by a CA of the new TrustList (§7.8.2.5). Previously an
unknown/forged CRL passed CloseAndUpdate and only failed ApplyChanges.
- **X.509 v1 certificates are rejected** by
CloseAndUpdate/AddCertificate/UpdateCertificate (OPC 10000-6 §6.2.2; new
`X509Utils.IsX509Version3`).
- **Application setup state (Annex G.2):** new
`StandardServer.StartupServerState` (virtual); the DI host returns
`NoConfiguration` when `ServerConfigurationOptions.InApplicationSetup ==
true`. `IsRunning` and `NodeManagerLifecycle` treat `NoConfiguration` as
operational. The Reference Server `--provision` mode starts in
`NoConfiguration` and exposes `InApplicationSetup=TRUE`.
- **DefaultHttpsGroup:** the group is only created when Https stores are
configured — the Reference Server config had none, so its
`opc.https`/`opc.wss` endpoints used the RSA application certificate
with an empty TLS trust list and the Https certificate could not be
pushed. The sample config now declares
`HttpsIssuerCertificates`/`TrustedHttpsCertificates` and an
`HttpsCertificateType` certificate (new `CertificateTypeString` value
`Https`). An Https certificate is assigned to DefaultHttpsGroup only (no
longer also listed in DefaultApplicationGroup). Fixed
`AddSecurityConfigurationHttpsStore`, which built the trusted Https
store path from the store *type*.
- Minor: AddCertificate logs a warning when it adds a CA without a CRL
while `RejectUnknownRevocationStatus` is enabled; UpdateCertificate
refused for an insufficient SecureChannel raises
`CertificateUpdateRequestedAuditEvent` with Status=false.

### Kept on purpose
- PEM certificates are accepted (converted to DER).
- `Bad_NotWritable` when opening an already open TrustList (OPC 10000-20
§4.2.2) and for AddCertificate while a read handle is open (§7.8.2.6).
- No validation rules for DefaultUserTokenGroup / DefaultHttpsGroup
TrustLists (Part 12 defines them only for ApplicationCertificateType
groups).

### Notes for reviewers
- Existing tests in `ConfigurationNodeManagerPushTests` and
`PushCertificateRequestRegressionTests` signed pushed certificates with
CAs that were not in the TrustList; they now trust their CA first
(behaviour change above).
- Adding the Https certificate to
`Quickstarts.ReferenceServer.Config.xml` gives the Reference Server's
opc.https/opc.wss endpoints their own TLS certificate.

### Tests
-
`tests/Opc.Ua.Server.Tests/Configuration/PushComplianceRegressionTests.cs`
— duplicate add/rollback, PEM, v1/v3, CRL issuer/forged/CRL-only, audit
routing.
- `tests/Opc.Ua.Server.Tests/ApplicationSetupServerStateTests.cs` —
NoConfiguration startup state.
- `tests/Opc.Ua.Gds.Tests/PushComplianceTests.cs` — end-to-end over a
secure channel: TrustList transaction lifecycle, open-handle rules,
masked reads, duplicate/PEM add, unknown CRL issuer, unknown certificate
issuer, cross-Session CSR key, nonce rules, RemoveCertificate chain/CRL
handling. Mutation-checked: reverting the issuer, duplicate-add and CRL
fixes makes the corresponding tests fail.
- Additional cases in `PushCertificateRequestRegressionTests`,
`CertificateIdentifierTests`, `ApplicationConfigurationBuilderTests`.

Local runs (net10.0): Opc.Ua.Gds.Tests 1257 passed / 0 failed / 56
skipped; Opc.Ua.Server.Tests push/TrustList/audit/lifecycle subset 1209
passed; Core and Configuration test classes passed. net48 build of the
affected test projects succeeds; `dotnet format whitespace` clean.

## Related Issues

- None (findings from a manual Push Model compliance run).

## 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.
- [ ] 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.5 <noreply@anthropic.com>
2026-10-06 18:48:02 +00:00
Copilot e78c648295 Images CI: publish samples once natively and stop compiling under QEMU (#4679)
# Description

Images CI took 45–100 min per image. The arm64 half of every
`linux/amd64,linux/arm64` build ran `dotnet restore` and `dotnet
publish` in an arm64 SDK container under QEMU. For `refserver`, publish
took 8 min natively for amd64 and 92 min emulated for arm64. On top of
that, every one of the 10 matrix legs recompiled the whole stack.

The output is framework-dependent portable IL, so the compile is now
done once, natively. The image legs only copy files onto the
per-platform runtime base image.

### Dockerfiles (all 10)
- **SDK stage runs on the build platform.** The `build` stage is pinned
to `--platform=$BUILDPLATFORM`. `-p:UseAppHost=false` keeps an x64 app
host out of the arm64 image.
- **New `prebuilt` stage.** It is chosen with a global `ARG
PUBLISH_SOURCE`, which defaults to `build`. Local `docker build` and the
`docker-compose` files are unchanged.
  ```dockerfile
  FROM scratch AS prebuilt
  ARG PUBLISH_DIR=artifacts/docker/publish
  COPY ${PUBLISH_DIR}/ /app/publish/

  FROM ${PUBLISH_SOURCE} AS publish
  ```
- **Pump: final-stage `RUN`s moved to a native stage.** The `sed` on
`openssl.cnf` and the `mkdir`/`chown`/`chmod` steps now run in a
`$BUILDPLATFORM` `layout` stage. The final stage copies `/layout/` to
`/`, so `/app` and `/diag` keep the `$APP_UID:0` owner and `g=u` mode
they had before. `openssl.cnf` therefore comes from the build-platform
base image. It is a plain config file, so it is expected to match
between architectures.

### `docker-image.yml`
- **Single image list.** The image list lives once, in `env.IMAGES`
(image, project, Dockerfile). `discover` emits it as the matrix.
- **New `publish-samples` job.** It runs `dotnet publish` for each
sample sequentially, so shared libraries compile once. It uses the same
options and NBGV version properties as the Dockerfiles and uploads the
output as the `docker-publish` artifact.
- **`build-and-push-image` legs.** Each leg:
  - downloads the artifact;
- builds with `PUBLISH_SOURCE=prebuilt` and
`PUBLISH_DIR=artifacts/docker/publish/<image>`;
  - no longer uses `setup-qemu-action`;
  - is named `build-and-push-image (<image>)`.
- **`images summary`.** It now also fails when `publish-samples` fails.

### Docs
- `ContainerReferenceServer.md` describes the publish-once / copy-only
flow and the build arguments.
- `DeveloperGuide.md` mentions the `publish-samples` job in the Images
CI section.

Expected: one native publish of about 7 min, then 10 parallel
copy-and-push legs of a few minutes each. Not yet confirmed on CI: the
sandbox could not pull MCR base images, so the prebuilt multi-platform
builds were checked against a Debian-based stand-in base, and the
build-from-source path was not built.

## Related Issues

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-10-06 14:21:44 +02:00
romanett 2871355d18 GDS: implement the AliasName Server facet (OPC 10000-17 Annex C) (#4668)
## Summary

Implements the **GDS AliasName Server facet** (OPC 10000-17 Annex C) in
`Opc.Ua.Gds.Server`, so that the CTT conformance unit *GDS AliasName
Discovery* passes apart from two CTT script defects (documented, Mantis
drafts in `plans/ctt-issues.md`).

Spec requirements:
- **C.1:** aggregate the AliasNames of registered Servers that expose
the `ALIAS` capability (OPC 10000-12 Annex D). Pull all
AliasNameCategory instances and merge those whose BrowseNames have the
same Name. Provide the ExpandedNodeId of every referenced node and the
ServerUri of the Server that contains it.
- **C.2:** on registration, merge the Server's AliasNames into a single
tree for TagVariables/Topics, combine aliases that share a name, and add
the ServerUri to `ServerArray`.
- **C.3:** on unregistration, remove that Server's aliases and
categories (but keep those other Servers still provide), remove its
AliasFor references, and remove its ServerUri from `ServerArray`.

### Design
- **`AliasNameAggregator`** (`Opc.Ua.Gds.Server.AliasNames`) holds one
snapshot per registered application and builds the merged view:
- Categories are keyed by their BrowseName path below `Aliases`. Aliases
are merged by name within a category; their targets are the union over
all sources.
- Targets are stored as namespace-URI ExpandedNodeIds. The ServerIndex
is resolved against the server's `ServerArray` each time the view is
built, because removing a ServerUri renumbers the entries after it.
  - Only ServerUris the aggregator added itself are removed again.
- Also implements `FindAlias`/`FindAliasVerbose`, recursive over
sub-categories.
- **`SessionAliasNameSourceReader`** reads a source over a client
Session by browsing the `Aliases` (i=23470) hierarchy and the AliasFor
references.
- It browses instead of calling `FindAlias` because the category
structure has to be aggregated too, and a source whose aliases come from
a NodeSet (like the CTT's embedded server) does not implement
`FindAlias`.
- It connects anonymously, to the most secure endpoint first, falling
back to SecurityMode None. It runs GetEndpoints once per DiscoveryUrl.
- **`ApplicationsNodeManager`** (new partial
`ApplicationsNodeManager.AliasNames.cs`):
- Hooks RegisterApplication (now `OnCallAsync`), UpdateApplication
(background re-read) and UnregisterApplication.
- Materializes the merged view as `AliasNameType` and
`AliasNameCategoryType` nodes in the GDS application-record namespace,
organized by the well-known TagVariables/Topics/Aliases objects. Each
category gets a `FindAlias` answered from the merged list.
- Changes and node updates are serialized, so a read that finishes after
a concurrent unregister cannot re-add the source.
- RegisterApplication waits at most `AliasNameRegistrationWait` (5 s)
for the read; a slower or unreachable source is finished in the
background and never blocks clients past their call timeout.
- Optional periodic re-read via `AliasNameRefreshInterval`; it also
retries sources that could not be read.
-
**`IAliasNameStoreRegistry.RegisterContributor`/`UnregisterContributor`**
(Opc.Ua.Server): a read-only contributor's aliases are added to the
`FindAlias`/`FindAliasVerbose` results of the categories it reports,
next to the owning store's. This lets the aggregated aliases share the
well-known categories with the server's own aliases, which matters for
the reference server, an AliasName Server and a GDS at once.
Contributors never receive Add/Delete calls.
- **Opt-in:**
`GlobalDiscoveryServerConfiguration.EnableAliasNameAggregation` is off
by default, because the GDS then opens client connections to the
DiscoveryUrls of registered applications.
`Ctt.ReferenceServer.Config.xml` enables it.
- `Opc.Ua.Gds.Server` now references `Opc.Ua.Client`; the `Opc.Ua`
meta-package already depends on both.

### CTT (1.05.513 with the patched scripts), p14-gds, reference server
`--ctt`, embedded CTT source + 2 extra AliasName sources

| GDS AliasName Discovery | before | after | after, with the C54/C55
script fixes |
| --- | --- | --- | --- |
| initialize | Skipped (no sources) | Pass | Pass |
| 001 | Error | Error (C55) | Pass |
| 002 | Error | Error (C54) | Warning (RegisterApplication > 100 ms) |
| 003 | Pass | Pass | Pass |
| 004 | Error | Error (C54) | Pass |
| 005 | Skipped | Error (C55) | Pass |
| 006, 007, 009, 011, 012, 014, 015 | Skipped | Error (C54) | Pass |
| 008 | Skipped | Pass | Pass |
| 010, 013 | Skipped | Error (C55) | Pass |

- **C54:** the script compares the GDS's AliasFor target (ServerIndex >
0, namespace URI, as Annex C.1 and Part 4 §7.16 require) with the
source's local NodeId using `equals()`, which can never match.
- **C55:** "no remote aliases" is checked as "no aliases", so it fails
on the reference server's own aliases.

Both are documented with draft Mantis texts (not filed). The remaining
script checks pass in 15/15 test cases.

No regressions:
- Application Directory and Query Applications are unchanged against the
baseline. The only difference is one ActivateSession timing warning in
one run.
- p09 (AliasName CUs) passes 32/32, identical to the baseline.

## Tests
- `AliasNameAggregatorTests` (unit) covers:
  - merging across Servers;
- categories kept while still referenced and removed after the last
source;
  - ServerArray add, remove and renumbering;
  - pre-existing ServerUris kept;
  - local targets;
  - FindAlias/FindAliasVerbose scope, pattern and reference-type filter.
- `GdsAliasNameAggregationTests` (end-to-end) registers a second
in-process reference server with `ALIAS` and reads it with the real
session reader. It checks:
  - aggregated nodes and remote AliasFor targets;
  - the ServerArray entry;
  - FindAlias on TagVariables and on the aggregated category;
  - that unregistering removes everything;
  - that an unreachable source still registers;
  - that a Server without `ALIAS` is ignored.
- Registry contributor tests in `AliasNameStoreRegistryTests`.
- Opc.Ua.Gds.Tests: 1246 passed, 0 failed, 56 skipped.
Opc.Ua.Server.Tests AliasNames: 198/198. InformationModel and Features
alias tests pass. net48 compiles.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:10:02 +00:00
romanett 87c35b84b6 Fix Opc.Ua.Client audit findings (security fallback, reconnect, subscriptions, redundancy, browse, files, WebApi) (#4572)
## Summary

Fixes the findings of a bug audit of `Opc.Ua.Client` and
`Opc.Ua.Client.ComplexTypes`, done on master plus #4516 (now merged).

- **Audit:** 82 findings across 12 areas, each checked against the OPC
UA specification. Final severities: 2 High, 37 Medium, 43 Low.
- **Fixes:** 76 fixed, 4 partial, 2 skipped (see [Not
fixed](#not-fixed--partial)).
- **Review:** a follow-up review of the fixes found 15 more defects in
them, all fixed here.
- **Tests:** every fix has a regression test, except a few races that
cannot be reproduced deterministically; the commit messages say which.

The finding IDs (`L<area>-<n>` for the audit, `G<n>` for the review)
appear in the commit titles.

## Behaviour and user-facing changes

### Security and endpoint selection
- **Reconnect no longer drops to weaker security (L3-1, High).** When
the server rejects the configured security policy or mode during
reconnect, `SessionReconnectHandler` no longer switches to whatever
endpoint the server advertises, None included. It only picks an endpoint
at least as strong as the configured one, judged by a locally computed
security level. The server-advertised `SecurityLevel` is not trusted. If
nothing qualifies, reconnect fails with the original error (new log
event `SessionReconnectHandler + 13`) and keeps retrying with the
original settings.
- **`CoreClientUtils.SelectEndpoint(useSecurity: true)` never returns a
SecurityMode None endpoint over opc.tcp (L9-9, High).** It now returns
`null` when no secure endpoint matches.
- When the discovery URL is `https`/`opc.https`, an HTTPS endpoint with
SecurityMode None is still accepted as a last resort, because TLS
protects it (Part 6 §7.4.1) (G17).
  - Endpoints with SecurityMode `Invalid` no longer count as secure.
- `GlobalDiscoveryServerClient` and `ServerPushConfigurationClient` now
throw `BadSecurityPolicyRejected` when no secure endpoint matches,
instead of passing `null` on. The ConsoleReferenceClient sample reports
this with a clear error.
- **No Bearer/Basic credentials over `http://` (L12-2).**
`WebApiClient.Create` and WebApi channel open/reconnect reject them with
`BadSecurityChecksFailed`. The public `WebApiClient(HttpClient, …)`
constructor also rejects them when the client's `BaseAddress` is not
https, and every send checks the address it actually goes to (covers a
`BaseAddress` set later).

### Session, keep-alive and identity
- **Faster detection of a silent server (L1-1).** The keep-alive Read is
now bounded by its `TimeoutHint` (2 × `KeepAliveInterval`), which is
what Part 4 §7.32 says the client should do. It then fails with
`BadNoCommunication`, so reconnect starts after about 2 ×
`KeepAliveInterval` instead of `OperationTimeout` (120 s by default).
- **Operations fail fast during a channel-manager recovery (L1-2, G9).**
`UpdateIdentityAsync`, `TransferSubscriptionsAsync` and
`ReactivateSubscriptionsAsync` fail with `BadSecureChannelClosed`
(retryable) instead of deadlocking. This covers the operation holding
the reconnect lock and any queued behind it.
- **Closing or disposing from a handler no longer waits on itself (L1-3,
G5, G6).**
- `Session` and `ManagedSession` Close/Dispose from a Notification
handler no longer wait 30 s for their own background drain.
- Disposing a `ManagedSession` from a keep-alive `PublishStatusChanged`
handler no longer deadlocks.
- Tasks spawned by a handler no longer count as "the handler" (the owner
marker is scoped to the work item).
- **Switching to an ECC user-token policy takes two ActivateSession
calls (L1-4, G4).** This follows Part 6 §6.8.2 (fetch the ephemeral key,
then activate).
- If the server returns no key, the switch fails with
`BadSecurityPolicyRejected`.
- If the second activation is rejected, the old identity stays on the
override policy with the key just fetched. Restoring the old key was
tried: against the reference server the next reconnect then failed to
decrypt the identity token.
- **`ManagedSession.ConfiguredEndpoint` follows failover (L2-6).** It
now reports the inner session's endpoint, so it changes after a failover
or network-path rotation.
- **An explicit identity update replaces the identity provider (L2-7).**
`ManagedSession.UpdateSessionAsync(identity)` stops the provider's
refresh loop and drops the provider for good. If the update fails, the
loop is restarted.
- **Reconnect waits for in-flight calls on a healthy channel (L2-3,
G8a).** Reconnect/failover on a healthy channel (certificate or
trust-list change, manual reconnect) waits for in-flight service calls.
The wait is bounded (max(`KeepAliveInterval`, 1 s)) only once the
channel is unhealthy (keep-alive stopped, channel not Ready), so a dead
channel no longer holds recovery for `OperationTimeout`.
- **Smaller fixes:**
- Restore reads the server certificate as a chain; this failed on macOS
(L1-5).
  - Session recovery can no longer run twice at once (L1-6).
  - Identity expiries more than 49.7 days out no longer throw (L2-2).
- Keep-alive reconnect suppression is recalculated on a lease swap
(L2-4).

### Reconnect state machine and reverse connect
- **⚠ `MaxTotalReconnectTime = 0` now means "no time limit" (L3-7).
Reviewer decision needed.** Before, 0 disabled reconnect entirely.
Negative values other than `Timeout.InfiniteTimeSpan` now throw
`ArgumentOutOfRangeException` when the state machine is constructed;
before, they crashed the worker. The XML docs of
`ReconnectPolicy`/`ReconnectPolicyOptions` are updated. If 0 should keep
meaning "no reconnect", this is a one-line revert.
- **A close racing the end of the reconnect budget is no longer lost
(L3-4).** It goes through the normal close instead of switching to
failover.
- **The "reconnect worker" marker covers one step only (L2-1).** It
lasts for a single connect/reconnect/failover/close step. Keep-alive,
publish and identity loops started in a step no longer count as the
worker afterwards.
- **Reverse-connect strategies `Undefined` and `Any` alone are removed
after the first callback (L8-1).** This matches the documentation;
before, they behaved like `Always`.
- **`AddEndpoint` for a URL that is already added disposes the replaced
listener (L8-2).**

### Service calls
- **Batched calls clean up after a failed later batch (L3-2).** When a
later batch of
`Browse`/`BrowseNext`/`HistoryRead`/`CreateMonitoredItems` fails, the
continuation points and monitored items created by earlier batches are
released or deleted before the exception is rethrown. This adds one
request. `AddNodes`/`RegisterNodes` are not undone.
- **Batched diagnostic infos point at the right strings (L3-3).** The
string-table indexes are rebased when diagnostics are padded.
- **`ReadBytesAsync` rejects oversized responses (L3-5).** It throws
`BadUnknownResponse` when the server returns more bytes than requested,
and `BadEncodingLimitsExceeded` beyond `int` range.
- **`ReadByteStringInChunksAsync` handles an unknown server limit
(L3-6).** When the server's `MaxByteStringLength` is 0 (absent) or
larger than `int`, it falls back to the client's
`MessageContext.MaxByteStringLength`. It throws only when both are
unknown.

### Browse and NodeCache
- **Empty browse pages with a continuation point are followed (L9-1,
L9-4, L9-5, G16).** Part 4 §5.9.2.1 allows a server to return an empty
page with a continuation point, so browsing keeps following it. After 10
consecutive empty rounds it gives up with `BadNoData` and releases the
continuation point.
- `BrowseAsync(NodeId)` now throws instead of silently returning a
truncated list.
  - `BrowseStreamAsync` yields a `BadNoData` result for that node.
- The same cap, through one shared helper, now also protects
`RoleManagementClient.ListRolesAsync` and the FileSystem
enumerate/empty-check loops.
- **Continuation points are released on failure (L9-3).**
`BrowseAsync(NodeId)` releases them on any exception, and
`BrowseStreamAsync` releases all points of the current page when the
consumer stops early.
- **NodeCache lookups propagate cancellation (L9-6, L9-8).**
`FindAsync`, `IsKnownAsync`, `FetchNodeAsync`,
`FindReferenceTypeNameAsync`, the reference-table fill and the
type-hierarchy search now let `OperationCanceledException` through.
Before, a cancelled lookup looked like "node does not exist".
- **A NodeClass mismatch fails only that node (L9-7).** It returns
`BadNodeClassInvalid` for that node instead of throwing
`KeyNotFoundException` for the whole batch.
- **Smaller fixes:**
- `MaxNodesPerBrowse` ≥ 2^31 no longer breaks the managed browse (L9-2).
  - Null discovery URLs are skipped (L9-10).
  - `ModelChangeTracker` no longer cancels a joined start (L9-11).
- **Log changes:** `BrowserContinuationPointExistsButBrowse` (Browser +
0) was removed, and `BrowserServerReturnedEmptyReferencesBut` now
includes the round count.

### Subscriptions (default engine)
- **Recovery after a transfer delivers each message exactly once (L5-1,
G22).** Messages already dispatched are acknowledged instead of
republished and delivered again. Messages whose earlier republish failed
are now republished and delivered, with the Republish flag and after
newer messages, instead of being acknowledged and lost.
- **A keep-alive that proves a gap triggers republish at once (L5-5,
G23).** The dedup mark moves only past messages that were actually
recovered, so a failed republish is retried by the next data message.
- **Item changes that fail are retried with backoff (L5-3).** They are
retried on the next apply pass after the state manager's 250 ms–5 s
backoff, instead of six times back to back. The six attempts now span
about 13 s.
- **Extreme revised intervals no longer throw (L5-4, L6-1).** Very large
or NaN intervals and counts revised by the server are capped (NaN falls
back to the requested value). Before, the overflow could stop every
publish worker.
- **`SetTriggeringAsync` from a callback no longer hangs (L5-7, G21).**
While the subscription is deleted, recreated or disposed, it returns
`BadOperationAbandoned`. A cancelled reset no longer makes later calls
fail.
- **Publish responses for unknown subscription ids (L6-3, L6-4, G20).**
The client no longer re-sends acknowledgements the server already
processed. An unknown id seen while a creation is pending is left alone
instead of being deleted.
- **Smaller fixes:**
- A null and an empty `IndexRange` are treated as equal, and restored
snapshots use `null` (L5-6).
  - A failed partition attach during restore is rolled back (L6-2).
- `StreamingSubscription` throws `ObjectDisposedException` after dispose
(L6-6).

### Subscriptions (classic `Subscription`)
- **Republish no longer abandons other missing messages (L7-1).** A
Republish response no longer overwrites `AvailableSequenceNumbers`, so
other missing messages are not given up while the server still holds
them.
- **Late copies are no longer delivered twice (L7-2).** Copies of
already-processed sequence numbers are ignored, and creating a new
server subscription resets the sequence state.
- **Disposing from a keep-alive handler no longer deadlocks (L7-3).**
`Dispose()` from a keep-alive `PublishStatusChanged` handler can return
while that callback is still finishing.
- **Transfer of a restored subscription (L7-4, G27).**
  - It succeeds even when some saved items were never created.
- The server subscription is deleted only when this session's own
TransferSubscriptions just moved it and adoption failed definitively
(item count mismatch or a non-transient status). Transient errors and
`ReactivateSubscriptionsAsync` never delete it.
  - The public `Subscription.TransferAsync` never deletes.
- **Smaller fixes:**
  - Cloned items keep their triggering links (L7-5).
  - `CreateAsync` clears stale server ids (L7-6).
- A `Good_SubscriptionTransferred` for the client's own transfer is
ignored (L7-7).
- An item removed while its create is in flight is deleted afterwards
(L7-8).
- The publish timeout for very long keep-alives no longer overflows
(L7-9).
  - A negative `MaxMessageCount` is handled (L7-10).
  - `LastMessage` tolerates a wrong notification type (L7-11).
  - Pending acks exactly 2^31 away no longer throw (L4-1).
- **Log changes:** new log events `Subscription + 68..70`.

### Redundancy
- **Warm/Hot demotion (L4-2, G1, G3).**
- Without token reuse, the demoted leader closes its session and
reconnects as a fresh standby.
- With token reuse, it abandons the shared server session locally,
without CloseSession, so the new leader can reactivate it and keep its
subscriptions. This uses the new internal methods
`ManagedSession.AbandonServerSessionAsync` and
`Session.AbandonServerSession`.
- `RoleChanged(false)` is now raised before the standby connect and no
longer depends on it, so `CurrentSession` can briefly be `null`.
- Exceptions from `RoleChanged` handlers are logged and do not stop the
teardown.
- **Promotion (L4-3, G2).** A promoted standby that adopts the leader's
session first closes its own server session. A retried promotion keeps
the session it already adopted.
- **Client failover takeover (L4-4, partial).** Subscriptions the backup
prepared in advance (not yet created, `TransferId` set to the active
client's id) are moved through the session's own transfer and keep
publishing. `docs/HighAvailability.md` is updated.
- **`ShouldFailover` through the interface (L4-6).** Calling it through
`IServerRedundancyHandler` no longer fails over away from a server that
is still Healthy.

### Fluent / dependency injection
- **Shared DI session (L10-5).** The shared session from `AddClient` is
reconnected after a caller disposes it, and is disposed with the
container. After that the connect delegate throws
`ObjectDisposedException`.
- **New public API:
`ManagedSessionBuilder.WithServerRedundancyOptions(ServerRedundancyOptions)`
(L10-6).** This is additive. The `Session.ServerRedundancy` options are
now actually applied.
- **`AddDiscoveryAndConnect` rewrites the advertised address (L10-7).**
It replaces the host and port the server advertises with those of the
discovery URL (Part 4 §5.6.2.1, NAT/containers), and prefers endpoints
with the same scheme.

### State machines and role management
- **`GetSubStateMachineAsync` returns the instance's own sub-state
machine (L10-1).** Before, it returned the type's placeholder.
- **`WaitForStateAsync` reports failures with OPC UA status codes
(L10-2).** It throws `BadNotFound` when `CurrentState/Id` cannot be
resolved, and `BadInvalidState` when observation ends without reaching
the state. Before, both surfaced as `OperationCanceledException`.
- **`RoleManagementClient.ListRolesAsync` pages correctly (L10-3).** It
follows continuation points, releases them on any failure, and throws on
a Bad browse status instead of returning an empty list.
- **Role properties are read into the right fields (L10-4).** Properties
whose namespace cannot be resolved are skipped instead of shifting the
other values.

### File system, temporary files and alias names
- **Path escaping (L11-8, G12).**
- Path segments escape `:` and `/` with `&`, and `&` itself as `&&`
before those characters or at the end of a name.
- `Format` and `Parse` round-trip all names. Typed input ending in `&/`
now reads the slash as part of the name.
- A segment without a namespace prefix also matches a namespace-0 child
when nothing matches in the parent's namespace.
- **Stale path cache (L11-1).** When a cached lookup fails,
`GetInfoAsync` retries once from the server. `ExistsAsync` and
`GetFileAsync` report "not found" instead of throwing.
- **`GenerateFileForReadAsync` waits for the file (L11-2).** It waits
until the file-generation state machine reaches ReadTransfer (`Idle`
counts as not started yet), and throws `IOException` if it reports
Error. The wait is bounded only by the caller's token.
- **Non-recursive delete refuses hidden children (L11-3).** It refuses
when the directory holds children that the subtype options hide from
enumeration. Before, the server deleted them all.
- **Alias names (L11-5, L11-6, L11-7).**
- `BadMethodInvalid` maps to `NotSupportedException` for all alias
calls, so `UseVerbose` falls back to `FindAlias`.
  - A duplicate alias name resolves to all its targets.
- If the refresh strategy fails to start, the resolver logs a warning
and keeps working without automatic invalidation. New log event block
`AliasNameResolver = 560`.
- **Null profile entries (L11-4).** Null `ServerProfileArray` entries no
longer crash `GetConformanceInfoAsync`.

### WebApi transport
- **Failures come back as `ServiceResultException` (L12-1).**
  - A timeout becomes `BadRequestTimeout`.
- HTTP 429/503 becomes `BadServerTooBusy` with the Retry-After hint, on
every target framework. `WebApiClient` checks the status itself (reusing
`HttpsTransportChannel.CreateServerTooBusyException`), so callers of
`WebApiClient` also get a `ServiceResultException` instead of an
`HttpRequestException` for these two statuses (G15).
  - An IO error becomes `BadNotConnected`.
- **Timeouts (L12-1, G14).**
- `OperationTimeout` applies to each request. When `RequestTimeout` is
unset, HttpClient's 100 s default no longer cuts off long Publish
requests.
- The GetEndpoints call at open is bounded by `OperationTimeout`, and
cancelling the open now throws.
- **Core change:** `HttpsTransportChannel.MapRequestFailure` went from
private to internal so the WebApi channel can reuse it.

### ComplexTypes
- **Out-of-range union selector (L12-3).** A union whose SwitchField is
larger than its field count now fails with `BadDecodingError` when
decoded and `BadEncodingError` when encoded (Part 6 §5.2.8).
- **Verbose JSON unions (L12-4).** They now decode their selected field
instead of `null`.

## Not fixed / partial
- **L2-5 (skipped):** an identity provider combined with a
caller-supplied reverse connection still can't connect to an
undiscovered endpoint. Fixing it means choosing the identity after
CreateSession, a design change.
- **L4-5 (skipped):** the one-time KeyCredential token needs a fresh
token on each reactivation. Today reactivation fails and the client
falls back to a new session.
- **L4-4 (partial):** only subscriptions the backup prepared in advance
survive a takeover.
- **L3-2 (partial):** `AddNodes`/`RegisterNodes` batches are not rolled
back.
- **L2-3 (partial):** service calls still hold the session lock for
their whole round trip.
- **Known from the review, not fixed here:**
  - G7: the batch cleanup request can't be cancelled.
- G11: the DI session's close at host shutdown never reaches the server,
and a closed session keeps being handed out.
- G13: a file renamed by another client is still found under its old
path.
  - G25: a clone-based transfer doesn't mark the original subscriptions.
  - G27b: the transfer item check matches by count, not by ID.
- **Outside this PR:** the reference server ignores `ECDHPolicyUri` on
ActivateSession (`SessionSecurityPolicyHelper`), which breaks Part 6
§6.8.2. As a result, the L1-4 two-step ECC switch ends in
`BadSecurityPolicyRejected` against our own server.

## Testing
All suites on net10.0, on the branch before the master merge. Every
suite had 0 failures:

| Suite | Passed | Skipped |
|---|---|---|
| Opc.Ua.Client.Tests | 2698 | 0 |
| Opc.Ua.Redundancy.Client.Tests | 134 | 0 |
| Opc.Ua.Subscriptions.Classic.Tests | 57 | 0 |
| Opc.Ua.Bindings.Https.WebApi.Tests | 385 | 0 |
| Opc.Ua.Sessions.Tests | 782 | 379 |
| Opc.Ua.Subscriptions.Tests | 640 | 6 |
| Opc.Ua.Client.ComplexTypes.Tests | 2858 | 0 |

After merging current master (net10.0), every suite had 0 failures
except one, a flaky new test fixed in 9f52952b2:

| Suite | Passed | Skipped | Failed |
|---|---|---|---|
| Opc.Ua.Client.Tests | 2719 | 0 | 1 (the flaky test) |
| Opc.Ua.Redundancy.Client.Tests | 134 | 0 | 0 |
| Opc.Ua.Client.ComplexTypes.Tests | 2858 | 0 | 0 |
| Opc.Ua.Subscriptions.Classic.Tests | 57 | 0 | 0 |
| Opc.Ua.Bindings.Https.WebApi.Tests | 397 | 0 | 0 |
| Opc.Ua.Sessions.Tests | 790 | 379 | 0 |
| Opc.Ua.Subscriptions.Tests | 640 | 6 | 0 |

The touched test projects also build on net48 with 0 warnings.

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

---------

Co-authored-by: Claude Opus 5.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>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-09-29 09:34:03 +00:00
romanett d6356766c5 Resolve the open CTT findings: aggregate fixes, de-DE ApplicationName and CTT configuration (#4545)
# Description

Resolves the open items of `plans/ctt-issues.md` (*Needs clarification*,
*Open server findings*, *CTT project configuration notes*). Each item
was checked against the OPC 10000 spec text. A server change was made
only where the server was wrong, and every server change has a
regression test. CTT script and oracle defects are filed in Mantis
instead.

### Server changes (`Opc.Ua.Server`)

- **NumberOfTransitions honours TreatUncertainAsBad.**
- Part 13 §5.4.3.24 does not count Bad values. §4.2.1.2 makes Uncertain
values "equivalent to Bad" when TreatUncertainAsBad is True, and the
aggregate definition doesn't override that.
- Uncertain values are therefore no longer counted, and no longer used
as the previous value, when the setting is True. The now unused
`TimeSlice.NonBadEarlyBound` is removed.
- **DurationInStateZero/NonZero status follows the variable's
interpolation.**
- §5.4.3.2.2: "A region is Uncertain if a region ends in a Bad or
Uncertain value and SlopedInterpolation is used."
- The calculator always built stepped status regions. So on a sloped
variable, the last region stayed Good while TimeAverage2 and Total2 over
the same interval were already Uncertain.
  - The duration value doesn't change.
- **AnnotationCount returns `Bad_NoData` and sets Partial.**
- Table 68 requires `Bad_NoData` for intervals before the start or after
the end of data. §5.3.3.2 requires the Partial bit for incomplete
intervals.
- `HistorianDispatcher` now derives the start and end of data from the
raw history (read with bounds) plus the annotation timestamps. It passes
them to a new `CalculateAnnotationCounts` overload; the old overload is
unchanged.
- Three existing dispatcher tests change their expectations. For
example, a node without any data now returns `Bad_NoData` instead of
Good zeros.

### Reference server sample and CTT configuration

- **`de-DE` ApplicationName.** The server advertises `de-DE` in
LocaleIdArray, but FindServers and GetEndpoints always returned the
en-US name. The sample now keys the ApplicationName and registers en-US
and de-DE translations (Part 4 §5.5.2.2 / §5.5.4.2 localeIds).
- **Subscription limit.** `Max Supported Subscriptions` in
`samples/UAReferenceServer.ctt.xml` and `MaxSubscriptionCount` in
`Ctt.ReferenceServer.Config.xml` go from 100 to 200. Subscription
Publish Min 05 `003.js` needs 190 subscriptions.
- **Alarm Cycle Time.** The CTT template's Alarm Cycle Time is 30 s. At
60 s it equalled `RequestedSessionTimeout`, so a Limit/Level CU that
starts a run finds its session expired (C40).

### Tests only

- **AddNodes duplicate BrowseName test:**
`AddNodeAsync_DuplicateBrowseNameUnderParent_ReturnsBadBrowseNameDuplicatedAsync`
never ran. Its items had no TypeDefinition, so its `Assume` always made
it inconclusive. The finding it belongs to (AddNodes after a rename
returning `BadNodeIdExists`) was already fixed by #4477.
- **RequestHandle echo:** a new test covers the ServiceFault for an
oversized argument in a chunked request (already fixed by #4488).
- **Value-based status:** a new test covers the aggregate status with
TreatUncertainAsBad = false (already fixed by #4503).

### Not changed

- **GDS QueryApplications/QueryServers still return records with
ServerCapability `NA`.** OPC 10000-12 §6.5.10/§6.5.11 says they "shall
not", but the CTT GDS scripts expect those records. Implementing the
exclusion turned 40 GDS test cases from pass to error, so it waits for
the script fix ([Mantis
11458](https://mantis.opcfoundation.org/view.php?id=11458)).
- **Idle SecureChannels are still closed after `ChannelLifetime`.**
Keeping them open would be security relevant. The CTT configuration's
`ChannelLifetime` of 120000 (#4502) covers the CTT case.

### Mantis issues filed

| Id | Issue |
| --- | --- |
| C51 | [11458](https://mantis.opcfoundation.org/view.php?id=11458) GDS
scripts expect NA records in query results |
| C52 | [11459](https://mantis.opcfoundation.org/view.php?id=11459) A &
C Refresh scripts leak subscriptions on the shared alarm session |
| C53 | [11460](https://mantis.opcfoundation.org/view.php?id=11460)
`StartThreadPublish.js` defaults misspelled properties |
| U2 | [11461](https://mantis.opcfoundation.org/view.php?id=11461)
Minimum2 oracle on reverse reads |
| U4 | [11462](https://mantis.opcfoundation.org/view.php?id=11462) Part
13 clarification: TreatUncertainAsBad and simple bounds (DeltaBounds) |

The A & C `BadSubscriptionIdInvalid` failures (Refresh, Refresh2,
Shelving) come from CPU load: with other processes at 100 % CPU, all
subscriptions of the CTT alarm session expire within one second. This
happens with and without this PR (3 of 3 loaded runs, 0 of 2 idle runs).

### CTT verification

CTT 1.05.06, scripts 1.05.513, compared at test-case level against a
build of the starting commit:

| Group | Before | After |
| --- | --- | --- |
| Aggregates | 4,562 error messages | 4,186. NumberOfTransitions: 12
test cases go from error to pass. AnnotationCount
`005-01.js`/`005-02.js` pass. NumberOfTransitions
`003-02.js`/`004-02.js` now fail because of C1 ([Mantis
11420](https://mantis.opcfoundation.org/view.php?id=11420)): they
request TreatUncertainAsBad = false, but the harness makes the server
use its default of true. |
| Discovery | 10 warnings | 0. Find Servers Filter `003.js`/`006.js` and
Get Endpoints `002.js` pass. |
| Subscription | – | Publish Min 05 `003.js` goes from warning to pass.
|
| Historical Access, GDS, Node Management | – | no new failures |

`plans/ctt-issues.md` is updated: resolved items leave the open
sections, and the new Mantis issues are recorded.

## Related Issues

- Related to #4479 (CTT configuration notes)
- Follows #4502

## 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.
- [ ] 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 run locally on net10.0 after merging master:

| Project | Filter | Passed |
| --- | --- | --- |
| `Opc.Ua.Server.Tests` | Aggregate, Historian, NodeManagement, Publish,
Subscription | 1,419 |
| `Opc.Ua.Core.Tests` | TcpServerChannelBuffer | 65 |
| `Opc.Ua.History.Tests` | Aggregate, Annotation | 67 |
| `Opc.Ua.Sessions.Tests` | discovery locale tests | 8 |

Before the merge, the whole `Opc.Ua.Sessions.Tests` ClientTest fixture
passed (372 passed, 0 failed).

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-25 12:30:25 +02:00
romanett 13f96fd4ba Raise ChannelLifetime for the CTT reference server and record the full CTT run of the merged CTT PRs (#4502)
# Description

Findings of a full CTT run (23 parts, 3,752 test cases, CTT 1.05.06 /
scripts 1.05.513) against the merge of #4477, #4482, #4485 and #4486,
which are all on master now, plus the CTT defects filed in Mantis.

No server code changes: the PR changes only the CTT configuration of the
reference server sample and the `plans/` documentation.

## CTT configuration: raise `ChannelLifetime` to 120 s

CTT Security None `007.js` and Security Basic256Sha256 `005.js` failed
(also on master): they keep SecureChannels without a Session idle for
about 51 s (60 s token, stretched by the CTT's CloseSession delay, C50)
and expect the newest one to close with Good.
`TcpTransportListener.DetectInactiveChannels` had already closed it
after `ChannelLifetime` (30 s) without traffic
(`CloseSecureChannel().Result received BadInvalidState`).


`samples/Reference/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml`
now sets `ChannelLifetime` to 120000. The default of 30000 and the
server's inactivity cleanup are unchanged.

An earlier revision of this PR changed the inactivity cleanup instead,
keeping idle Session-less channels open until their token expired (Part
4 §5.6.2.1). That change was withdrawn because it lets an
unauthenticated client hold a channel for up to the
`SecurityTokenLifetime` (one hour by default) instead of 30 s. The
difference to Part 4 §5.6.2.1 is recorded as an open server finding in
`plans/ctt-issues.md`.

Validated on 2026-09-24 with the reverted code and the new configuration
(full run, compared with the run of 2026-09-22 at test-case level):

- Security None `007.js` passes and Security Basic256Sha256 `005.js` has
only the C50 CloseSession delay warnings (both failed with 30 s).
- All other parts have the same error signatures as before; the
remaining differences are *"Response Timestamp shows a delay"* warnings
from a loaded machine (a back-to-back Session Services run with 30 s and
120 s on an idle machine is identical).
- A & C Refresh2 `Err_003.js`/`Err_004.js` and Shelving `initialize.js`
report `BadSubscriptionIdInvalid` in today's runs with both 30 s and 120
s on the same build, so they are unrelated to the configuration (both
passed again in a run on the idle machine with 120 s).

## Mantis

32 CTT defects filed on 2026-09-24 (11420–11454, script issues in *CTT
UA Scripts*, native aggregate oracle issues in *CTT UA Binary*); 12
drafts were already covered by existing reports (including GDS issues
11406–11413 by node-opcua and 11102 for the `opc.https`/`opc.wss`
endpoint selection); 5 are held back for further investigation.
`plans/ctt-issues.md` links every entry.

## CTT documentation (`plans/ctt-issues.md`, `plans/ctt-testing.md`)

- `ctt-issues.md` lists every failing CTT test case with a short
abstract and its Mantis issue; the details live in Mantis. It keeps the
defects not filed yet, the open questions (U1–U4), the open server
findings and the CTT project configuration notes, and no longer lists
findings the server has fixed.
- Notable CTT findings of this run: the aggregate oracle lacks the Part
13 Uncertain status and Calculated bit rules that #4477 implements
(C48/C49, 24 test cases); the CloseSession "delay in excess of 600ms"
warnings are a CTT client artifact (C50: the server answers in 2–5 ms,
the CTT stops the Session's SessionThread between stamping and sending);
the CTT's `FindTokenType` rewrites its cached endpoints, so Security
User Name Password 2 `015.js` reports duplicate PolicyIds (C36); A & C
Comment passes or skips depending on the date (C39).
- Open server finding: AddNodes after a child rename returns
`BadNodeIdExists` (#4477 × #4486 interaction, not covered by the CTT).
- `ctt-testing.md` is a runbook for agents: setup, headless runs,
selection generation, reading and comparing results, triage, recipes per
conformance group (A & C, Session/Subscription, Monitored Items,
security groups with an isolated PKI) and a full partitioned run of
every group (about 80 minutes). It points to `ctt-issues.md` for the
failures to expect.

## Related Issues

- Follow-up to #4477, #4482, #4485, #4486

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

Validated locally with a full partitioned CTT run of the Release net10.0
reference server (reverted code, `ChannelLifetime` 120000).

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 21:52:44 +02:00
Copilot d7dd1b0369 Fix telemetry component assembly identification (#4490)
# Description

Telemetry extensions resolved meters and activity sources as
`Opc.Ua.Types`, with results dependent on helper inlining. This
prevented reliable attribution to the emitting component.

- **Existing telemetry contract**: Add `CreateMeter(Assembly)` and
`GetActivitySource(Assembly)` directly to `ITelemetryContext`. The
separate `IAssemblyTelemetryContext` interface has been removed.
- **Reliable component attribution**: Capture the caller assembly in
non-inlined `CreateMeter`, `GetActivitySource`, and `StartActivity`
extensions, then forward it to the existing telemetry context.
- **Call-site routing and fallback**: Keep non-null meter calls on
`telemetry.CreateMeter()`. Use explicit
`TelemetryExtensions.CreateMeter(telemetry)` for nullable-context
fallback. Preserve default meter/activity-source fallback when a context
returns null.
- **Coverage and documentation**: Cover caller-assembly naming,
direct-context and extension routing, activity creation, and null-result
fallback. Update diagnostics examples, OpenTelemetry filters, and
migration guidance.

**Compatibility:** Direct implementations of `ITelemetryContext` must
implement the two new assembly-aware members. `TelemetryContextBase`
supplies both. This interface addition is documented in the 2.0
migration guide; legacy implementer compatibility is not claimed.

## Local validation

The Release build and focused `TelemetryExtensionsTests` fixture pass on
both target frameworks:

| Target | Result |
| --- | --- |
| net10.0 | Passed: 6, Failed: 0, Skipped: 0 |
| net48 | Passed: 6, Failed: 0, Skipped: 0 |

Both builds emit existing CA5394 warnings in
`NodeStateChildNameIndexTests.cs`, which is unchanged by this PR. The
full solution test suite was not run during this feedback pass.

## Related Issues

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

<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes #4483

---------

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 <marcschier@users.noreply.github.com>
2026-09-16 11:07:17 +02:00
romanett ea93c86da2 Fix the Opc.Ua.Client defects reported by the client stack audit (#4471)
# Description

Fixes the 78 `Opc.Ua.Client` findings of a read-only bug audit of
`Opc.Ua.Client` / `Opc.Ua.Types` / `Opc.Ua.SourceGeneration` taken at
a05a36055, plus the regressions a subsequent review of those fixes
turned up. Every behavioural change was checked against the OPC UA
specification, and the citations are in the code comments.

## Session, reconnect and failover

- The reconnect `finally` re-acquired `m_reconnectLock` with the
caller's token, so a cancelled reconnect left the session stuck in
`Reconnecting` forever and every later reconnect failed. It now uses
`CancellationToken.None`.
- An in-place session recreate tore down subscriptions it should not
have: `transferred` is all-or-nothing, so a single failed transfer reset
and recreated the ones the server had just taken over, orphaning them on
the new session. `TransferSubscriptions` now reports the
per-subscription outcome and only the subscriptions the server did not
take over are recreated. A token-reuse failover keeps the same server
session, so nothing is transferred or reset there at all.
- In-place recreate only tears down the channel it created itself; a
caller-supplied channel or a channel-manager lease is left to its owner.
- `Snapshot(out SessionState)` went through the `SessionOptions` record
copy constructor, which drops every member declared on `SessionState`,
so the paired `Snapshot`/`Restore` API could not round-trip.
- The user-token policy override restored the policy URI on failure but
had already disposed the ECC ephemeral key the previous identity needs,
so the identity the rollback claims to preserve could no longer be
reactivated. The key is now parked and restored.
- `SetTriggering` batching sliced the remove list past its end when a
call had more adds than removes (Part 5 §6.3.11 bounds the *sum* of both
arrays, so an individual list may be shorter than the limit).
- The certificate-less `None` attempt dropped the request header, and
with it the ECDH ephemeral key that travels in `additionalHeader` (Part
4 §7.1/§7.15).
- `Session.Equals` compared a `ConfiguredEndpoint` against an
`EndpointDescription`, so a session did not equal a session on the same
endpoint.

## Managed session, pool and connection state machine

- `ManagedSession.Dispose()` blocked on the connection state machine's
worker, which raises `ConnectionStateChanged` synchronously — disposing
from that handler deadlocked permanently. The machine now recognises its
own flow and completes the teardown as the worker's continuation.
- `ManagedSessionFactory` dropped `updateBeforeConnect`, `connection`
and `reverseConnectManager` on all four `CreateAsync` overloads.
- The session pool ran its shared connect on no token at all, so once
the only caller gave up nothing could abort it and
`RemoveAsync`/`DisposeAsync` waited out the whole reconnect budget. Each
entry now owns the token that aborts its connect, and removal closes
*and* disposes the session.
- An explicit `engineFactory` was silently ignored for
`DefaultSessionFactory` subclasses, which quietly downgraded the session
to the classic subscription engine.

## Subscriptions

- Fixed a lock-order inversion between `m_subscriptionLock` and the
partition locks in `CompleteAsync` and `DisposeAsync`.
- `OwnsSubscriptionId` stopped at the first registered subscription
carrying the id — but in the very case it guards, the asker still holds
its own retired id, so the answer depended on `HashSet` enumeration
order and the delete could still tear down a sibling that had been
handed the recycled id.
- `MessageProcessor`'s republish gap walk was unbounded; it is now
bounded by the server's retransmission queue (Part 4 §5.14.5).
- A publish acknowledgement is sent even when a notification handler
throws — the ack means "the server may delete the message" (Part 4
§5.14.5.2), not "I processed it".
- Loaded monitored items did not raise the process-wide client-handle
counter, so a fresh process minted handles that collided with the loaded
ones; a transfer-restored item was also stamped `BadOperationAbandoned`
and left at the default monitoring mode.
- `AutoSetQueueSize` was not idempotent, so the queue size ratcheted up
on every subscription state change.
- The snapshot dropped the partition and recovery options entirely, and
clamped the sampling interval and the secondary-partition idle timeout
to zero — turning the Part 4 §7.21 "use the publishing interval"
sentinel and "never delete an idle partition" into something else.
- The state stream is positional OPC UA binary and the reader cannot
detect an absent field, so appending the partition options would have
mis-decoded every previously saved file. The stream now carries a format
version and the first format is decoded through a frozen copy of its
shape.

## Browse, node cache and the remaining clients

- Managed browse wrote a retry pass's status into whatever node happened
to sit at that offset in the caller's list, and retried
continuation-point errors without any bound. It now keeps the caller's
slot and gives up on passes that make no progress — a fixed pass cap
would cut short a large browse against a conforming server with a small
continuation-point quota.
- Continuation points are released when a consumer stops enumerating
early, as Part 4 §5.9.3.2 requires.
- The node cache reordered its batched results relative to the inputs,
which mispairs them for every consumer that matches positionally; its
supertype walks could also spin forever on a server whose `HasSubtype`
chain loops back on itself.
- A browse error was cached as "this node has no references"; it is now
surfaced to the direct caller, while the bulk and hierarchy walks treat
the node as having no references for that one operation without caching
it, so a single un-browsable node cannot fail a whole batch.
- `WebApiClient` mutated a caller-supplied (typically
`IHttpClientFactory`-pooled) `HttpClient`, which throws once that client
has sent its first request; those instances are configured per request
instead.
- `PathCache` was not thread-safe.

## Review follow-ups

The last two commits are the result of reviewing the audit fixes
themselves. Worth calling out: the `Task.WaitAsync(CancellationToken)`
polyfill in `Opc.Ua.Types` never checked `IsCompleted`, so an
already-finished task plus an already-cancelled token reported
cancellation on every pre-net8.0 target where the BCL overload returns
the result. It is now aligned with the BCL and pinned by a test.

## Related Issues

- No tracking issue; this PR is the result of a read-only audit of the
client stack.

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

### Verification

35 regression tests were added under
`tests/Opc.Ua.Client.Tests/AuditRegressions/` (category
`AuditRegression`), one per fixed defect where the defect is observable
at unit level; most were mutation-checked by reintroducing the bug and
confirming the test fails.

Locally on **net10.0**: `Opc.Ua.Client.Tests` 2252/2252,
`Opc.Ua.Client.ComplexTypes.Tests` 2773/2773,
`Opc.Ua.Bindings.Https.WebApi.Tests` 346/346, and all 23 library and
test projects that reference `Opc.Ua.Client` build clean.
`Opc.Ua.Client` also builds clean for **netstandard2.1**, the target
that actually compiles the `WaitAsync` polyfill. The full **UA.slnx**
solution and the .NET Framework targets have not been run locally — left
to CI.

One pre-existing failure in `Opc.Ua.Types.Tests` is unrelated to this
branch:
`WotSpecFixtureManifestTests.EveryEmbeddedExampleMatchesItsRecordedHash`
(embedded WoT fixture hashes) fails identically with these changes
reverted.

### Notes for reviewers

- Two review findings were deliberately left unfixed and are documented
in the code: `ManagedSession.CreateAsync` has to write the engine choice
onto a caller-supplied `DefaultSessionFactory` subclass (the only
alternative is changing the `ISessionFactory.Create` contract), and the
connection state machine's "am I on my own worker" flag is an
`AsyncLocal`, so it also flows into work a `StateChanged` handler
spawns. The previous behaviour in that second case was a hard deadlock.
- `Session.Dispose()`'s single `GetAwaiter().GetResult()` bridge to
`CloseAsync` is left as is: `Dispose(bool)` must be synchronous and
`DisposeAsync` already exists.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 08:22:28 +02:00
Marc Schier 7d5f7a7076 Add shared stack APIs and fixes used by UaLens (#4464)
# Description

Extract shared stack, regression-test, sample and documentation changes
from draft #4462 into a separate prerequisite PR. The PR now contains
**19 shared paths**: the original eighteen extracted files plus a
focused user-management regression file added during review. It does not
include UaLens source, desktop dependencies, solution registrations or
application documentation.

## Changes

- Correct the V2 ConditionRefresh target and response validation. Bound
disposal-time remote subscription deletion with an independent
five-second TimeProvider-backed cancellation deadline, allowing local
cleanup after a server outage. This is a per-delete limit, not a global
application shutdown deadline.
- Preserve configured PubSub writer message/header settings, canonical
numeric JSON publisher matching, and flattened DataValue
quality/timestamps.
- Add asynchronous user-management client wrappers over the generated
standard proxy, and set file/directory method declaration IDs needed by
typed client workflows.
- Enable AI client NativeAOT compatibility and add focused AI client
regressions.
- Enable the ConsoleReferenceServer file-system node manager and
configure 1 ms publishing/sampling demonstrations. These sample defaults
do not promise hard real-time delivery.
- Document the subscription and PubSub behavior. Make the new
subscription/PubSub regression helpers portable to .NET Framework
without weakening their assertions.

## Review follow-up

Commit `812297ce1` aligns both public `ListUsersAsync` signatures with
`ValueTask<ArrayOf<UserManagementUser>>`. The implementation constructs
its result directly from an owned snapshot array, without an
intermediate list.

Five focused cases pass on **net10.0 and net48**:
`ListUsersReturnsArrayOfSnapshotsAsync` checks both interface and
concrete entry points and preserves snapshot order, values and flags;
`ListUsersReturnsEmptyArrayAsync` covers typed-empty and missing values;
`ListUsersPreservesBadReadStatusAsync` checks the exact server failure
status.

All **eight single-line XML summaries** were converted to multi-line
form. A complete scan of the fourteen changed C# files, including the
new test, finds zero remaining single-line summaries. Scoped
whitespace/line-ending verification passes.

The sole UaLens caller was adapted separately in draft #4462
(`4caf69f33`). A fresh combined dependency-graph build and all **924
UaLens net10.0 tests** pass with both changes. Each of the three review
threads received an action-taken reply and was resolved only after the
fixes were pushed and verified.

## Split boundary

The branch originated at `a45105375`; the subsequent master merge
`f209f3804` incorporates upstream `4633aa7a3`. The original shared
changes and review regression remain separate from the app. Draft #4462
retains `tools\Opc.Ua.Lens`, `tests\Opc.Ua.Lens.Tests`, and its six
solution/package/documentation integration files.

**Merge this prerequisite before the UaLens draft.** The draft consumes
the extracted APIs. Published history is preserved; neither PR has been
merged automatically.

## Initial split validation

The following results describe the initial split validation at
`fa7199afa`; current review-specific validation is recorded above.

| Scope | net10.0 | net48 |
|---|---:|---:|
| Client subscription/file-system tests | 168 passed | 168 passed |
| Server file-system/user-management tests | 244 passed | 244 passed |
| PubSub writer/reader regressions | 34 passed | 34 passed |
| ConsoleReferenceServer build | Passed | Passed |

The corrected portable regression helpers were rechecked on net10.0
after the net48 fixes. The combined UaLens application also passed its
924-case net10.0 suite before extraction.

**Focused NativeAOT: 16 passed, zero failed/skipped.** A session-local
TUnit runner linked the exact committed `AIClientAotTests.cs` and
referenced the real AI client project. It published without warnings or
suppressions and executed as a genuine Windows native binary with no CLR
header.

The canonical `Opc.Ua.Aot.Tests` project was blocked by pre-existing
CS8632 nullable-annotation errors in unchanged upstream
`SchemaAotTests.cs` (lines 82, 127, 129 and 132). Its existing nullable
configuration and all upstream tests were preserved; the focused result
is not a claim that the full AOT test project builds.

The net48 hosting selection uses a short NUnit WorkDirectory: the
existing fixture builds long PKI paths that otherwise exceed the
platform's path limit. The original full selection passed without test
exclusions, source changes or increased timeouts.

Scoped builds reported inherited warnings in unchanged upstream files:
CA1508 in `ReferenceBuilderExtensions.cs`, CA1861 in
`NodeSetImportIntegrationTests.cs`, and net48 CA1823 in
`DefaultNodeIdFactory.cs`. They were not suppressed or folded into this
split. Full-solution, coverage and CI/CodeQL gates remain unasserted.

## Related Issues

- Tracking/design review: #4461.
- Prerequisite extracted from draft #4462.

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

Tests and scoped results are documented above; measured coverage,
inherited-warning qualification, CLA status, full-solution results and
CI completion are not asserted by these checkboxes.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-09 19:22:06 +00:00
romanett ececea15c8 Reimplement the GDS ApplicationsNodeManager as a generated fluent node manager (#4434)
# Description

Builds on #4428, which is now merged; this branch is rebased onto
master.

The GDS `ApplicationsNodeManager` becomes a source-generated
`[NodeManager]`: the companion model, its loader and the fluent plumbing
are emitted, and what stays hand-written is the behaviour. Getting there
needed three things from the shared surface, which are the first two
commits.

## Commits

**1. Add the fluent hooks a hand-written manager needs at startup**

- `OnReadRolePermissions` / `OnReadUserRolePermissions` on the node
builder. Every other `NodeState` `On*` hook was already fluent; these
were not, and a server that grants access on something the static model
cannot express has to compute the permission set per request.
- `Node<TState>(TState)` returns a builder over a node the caller
already holds, for nodes materialised after `Configure` resolved its
graph.
- `OnAddressSpaceReadyAsync` on `FluentNodeManagerBase` — the one seam
between the address space existing and the wiring being applied, for
managers whose wiring depends on asynchronous setup. `Configure` is a
`partial void` and cannot await. `DiNodeManager` had already invented
the hook privately, so its declaration becomes an `override`.

**2. Let a `[NodeManager]` bind to a model a referenced assembly
supplies**

A model belongs to the assembly that emits its types, which leaves a
node manager for it homeless whenever that assembly cannot reference
`Opc.Ua.Server` — a model-only package shared with clients, for
instance. Binding was rejected twice over: the design was skipped before
the binding was matched (`MODELGEN010`), and fluent-accessors-only mode
refused to run alongside manager generation (`MODELGEN014`). The binding
is now matched before the skip decision, and a matched binding puts that
design into accessors-only mode. A design nothing binds to is still
skipped, so this cannot start duplicating types by accident.

The emitted manager also gains what a real manager needs:
`DefaultNamespaceUris()` plus a protected constructor taking a
replacement set; `GenerateDefaultConstructor=false` to suppress the
two-argument form; its own logger category; and the call to
`OnAddressSpaceReadyAsync`.

Generated fluent accessors also stop re-enabling nullable warnings — a
bare `#nullable enable` undid the shared header's `disable warnings`, so
any model with a structure-typed method argument reported CS8600 on
every generated `out T` in a project that builds warnings as errors.

**3. Reimplement the GDS `ApplicationsNodeManager` as a generated node
manager**

The design stays owned by `Opc.Ua.Gds.Common`; the server assembly only
binds a manager to it.

**NodeIds do not move.** `NamespaceIndexes[0]` must stay the application
record namespace — it is the one the application database, the
certificate request store and the base allocator mint ids in — so the
constructor passes the namespace order explicitly rather than adopting
the generated model-first default. A test pins both ends against a live
server.

Four overrides go, leaving `OnAddressSpaceReadyAsync` and
`Dispose(bool)` — no address-space overrides at all:

| Removed | Why it was unnecessary |
| --- | --- |
| `New` | The base allocator already mints into `NamespaceIndexes[0]`,
with a thread-safe counter. The override existed only because
`Create(assignNodeIds: true)` asks the factory with the declaration id
still set; `AssignInstanceNodeId` nulls it and re-asks, documented as
being there for exactly this allocator. |
| `AddBehaviourToPredefinedNodeAsync` | Its `AuthorizationServiceType`
branch re-wired a node that was already wired, and its
`KeyCredentialServiceType` branch had nothing to wire — the model ships
that folder empty. |
| `GetManagerHandleAsync`, `ValidateNodeAsync` | Verbatim
reimplementations of the base that also blocked the fluent base's
virtual-node support. |
| `DeleteAddressSpaceAsync` | A `// TBD` passthrough. |

Everything else — all sixteen directory methods with their
self-administration permissions, the certificate groups' concrete types
and trust-list flags, and the authorization service (created through
this PR's `Add` surface and wired in the same place) — is one
`Configure` pass.

**4. Bind the certificate groups from the `Configure` pass**

Acquiring a certificate group and binding it to its nodes are different
kinds of work: the first is I/O (`InitAsync` opens certificate stores
and creates CA certificates), the second only touches the address space.
Each now runs in the phase it belongs to, which puts the whole of the
wiring in one place.

Ownership no longer rides on the binding. A group joins
`m_ownedCertificateGroups` the moment it is constructed, before
`InitAsync` can throw, so a group that fails half way through is still
disposed — which the previous code could not manage, keying ownership
off a dictionary it filled only after initialization succeeded.

> Commit 3's message says this move is impossible and blames `PushTest`.
**That was a wrong diagnosis, and this commit supersedes it.** Two real
defects were hiding behind those failures:

- **`Create(..., NodeId.Null, ..., assignNodeIds: false)` does not null
the NodeId.** `NodeState.Create` overrides it only when handed a
non-null one, so a custom group node kept the namespace-0 id of its type
declaration. `AssignInstanceNodeId` hid this by re-asking the factory
with the id nulled; the builder validates instead, and rejected it. The
node is now created with an id in the manager's own namespace, as the
`Default` authorization service already was.
- **The custom certificate group node was not browseable.** It answered
`GetCertificateGroups` and `ReadNode`, but a type-filtered recursive
browse from the Objects folder never returned it — which OPC 10000-12
§7.8.2 requires of a certificate group. Staging the node through the
builder gives it the reference it was missing.

### Two `PushTest` methods change — please review these closely

The second defect had been masking what those tests actually did.
Selecting the group positionally, `groups[3]` resolved to the server's
**own `ServerConfiguration` group** while the custom one stayed
invisible, so `UpdateTrustListOfCustomGroupAsync` and
`AddRemoveCertOfCustomGroupAsync` were exercising a *transactional*
trust list under a custom-group name.

Browse results, before and after:

| # | Before (baseline) | After |
| --- | --- | --- |
| 0-2 | GDS Directory groups (`ns=3`) | GDS Directory groups (`ns=3`) |
| 3 | **`DefaultApplicationGroup` `i=14156`** — the server's own |
**`MyCustomGroup` `ns=2`** |
| 4 | — | `DefaultApplicationGroup` `i=14156` |

They now resolve the group by browse name — which asserts it is
browseable at all — and expect the immediate-apply semantics the GDS
actually gives the groups of its own `Directory`: `CloseAndUpdate`
writes the stores at once and reports `applyChangesRequired == false`,
so the `ApplyChanges` calls go away (they would return
`Bad_NothingToDo`). The read-back assertions are what prove the writes
landed, and they are unchanged.

This is a genuine loss of the *transactional* trust-list coverage those
two methods were accidentally providing — the server's own group is
still covered by the other `PushTest` methods, which target it
deliberately.

**5. Select `GetCertificates` results by type instead of by position**

`GetCertificatesAsync` checked entry zero of a parallel-array result.
Which entry that is depends on the order the server reports its
certificate types in, an empty result throws `IndexOutOfRange` rather
than failing an assertion, and a second type going missing or
unparseable would not be noticed. Every returned pair is now checked,
and a failure names the certificate type.

**6. Drop the `.Common` suffix from the three GDS projects**

Every other companion specification ships as `<Family>` /
`<Family>.Client` / `<Family>.Server` — Di, ISA95, Positioning,
Robotics, Vision, WotCon, XRegistry. GDS was the exception, with 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 the
assembly and package ids disagreed. Nothing compiling against these
assemblies changes a `using`.

The package ids do change, which is consumer-visible, so
`docs/migrate/2.0.x/packages.md` gains a renamed-packages section — it
is the document that undertakes to cover NuGet renames — and the
migration skill's package table gains the two rows a 1.5.378 consumer
needs.

Build infrastructure that names the projects or their output moves with
them: both solution files, `expected-packages.txt` (re-sorted — it is
maintained alphabetically), both signing manifests, the reference-server
Dockerfile, the metapackage nuspecs, and the analyzer's package
dependencies. `preview-pack.slnx` is derived from `UA.slnx` and needs no
edit. The migration analyzer's shim tree mirrors the source layout by
its own documented convention, so `Gds.Client.Common/` becomes
`Gds.Client/`.

Prose naming past work is left alone — plan 23 still records that its
deferral happened during the "GDS Client.Common modernization", which is
the name that effort had.

## Validation

`net10.0`, on master + these four commits, from a clean GDS PKI:

- `UA.slnx`: 0 errors, 0 warnings in changed files (the solution's 30
pre-existing warning lines are CA1861/CA1850/CA1307 in `XRegistry`,
`Types.Tests` and `WotCon.Tests` — same rules and projects as the base).
- `Opc.Ua.Gds.Tests`: 1,117 passed, 0 failed, 54 skipped — identical to
the base branch.
- `Opc.Ua.Server.Tests`: 4,961 passed, 0 failed, 5 skipped (measured
before commit 4, which touches only the GDS assembly and its tests).
- `Opc.Ua.SourceGeneration.Tests`: 169 passed.
- `Opc.Ua.SourceGeneration.Core.Tests`, NodeManager/Generators/Fluent:
84 passed; `GenerateCode`: 21 passed, 4 skipped.
- `Opc.Ua.MigrationAnalyzer.Core.Tests`: 10 passed, 3 skipped.
- After the rename: assemblies build as `Opc.Ua.Gds.dll` / `.Client.dll`
/ `.Server.dll`, with no `*.Common.dll` produced anywhere.
- Commits 1-3 build standalone.
- `Opc.Ua.Gds.Server.Common` also builds for `net48` and
`netstandard2.1` (checked before the rebase onto #4428).

Not yet run: the full `Opc.Ua.SourceGeneration.Core.Tests` suite
(~3,800), and the older target frameworks since the rebase.

## Checklist

- [x] Existing tests pass
- [x] New tests cover the new surface (role permissions, resolved-node
builder, referenced-model binding, constructor/namespace control, GDS
namespace invariant, custom-group node namespace)
- [ ] Full Core.Tests sweep and net48/netstandard2.1 re-run after the
rebase

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-09 10:41:06 +02:00
Marc Schier 7411654c8d Complete OPC UA Part 11 Historical Access (#4390)
# Description

Completes the OPC UA Part 11 Historical Access work requested by #4387
and
the historian framework integration requested by #4400.

The conformance/evidence catalog covers all 37 released UACore 1.05
Historical Access profiles: 15 Server facets and 22 Client facets,
together
with the standard aggregate functions and their conformance units.
Server
facets are advertised only when the resolved providers expose their
required
interfaces and capabilities.

## Server and provider support

- Completes raw, modified, at-time, processed, annotation, structured,
and event history.
- Dispatches best-effort, bulk, and transactional updates with aligned
statuses, old values, diagnostics, and rollback state.
- Supports mixed history-update detail types and batched
structured/annotation operations.
- Uses composite identities and exclusive cursors for same-timestamp
structured, modified, and event values.
- Captures live data and events, installs historical configuration
objects, and emits typed audit payloads.
- Derives aggregate monitoring filters from per-node historian
capabilities and rejects unsupported processed aggregates before
provider dispatch.
- Supports tri-state `Historizing`: set, clear, or preserve
provider-owned state.

## Client support

- Exposes modified values together with `ModificationInfo`.
- Adds event-history reads and event insert/replace/update/delete
operations.
- Adds structured and annotation batching with strict response
validation.
- Shares continuation handling and release-on-abandon across read
families.
- Accepts inherited standard event fields rooted at event subtypes.

## Review hardening

- Registers DI-selected historians before node-manager reconciliation
and capability publication, while preserving provider ownership.
- Uses committed Raft read barriers for linearizable `TryGetAsync` and
`ScanAsync`.
- Keeps claimed continuation state immutable until a fresh successor is
saved, restoring the original after transient failures.
- Uses exact-incarnation compare-and-delete so delayed cleanup cannot
remove a restored same-ID continuation.
- Cleans up indeterminate initial and replacement saves, and propagates
shutdown cancellation through cleanup resolution.
- Preserves legacy continuation envelopes/cursors and distinct
annotation parent/property identities.
- Deletes every structured entry at a requested timestamp and uses the
complete modified-history ordering tuple.
- Shares native/fallback at-time and AnnotationCount calculations,
including reverse endpoints and cross-framework fractional intervals.
- Primes modified aggregate items before buffered live delivery and
deduplicates history/live overlap using provider-specific identities.
- Keeps committed Modify results successful while reporting priming
failures
  through the monitored item's notification queue.
- Keeps protected-notification state live-only for queues larger than
one.
Queue size one retains the newest notification without pinning errors or
transferring priority during resizing. Durable restore retains ordinary
definitions, last values/errors, and raw queues without recreating
priority.
- Isolates aggregate calculation, modification preparation, and
historical/live
handoff in `AggregationFilterHandler`, retaining existing
provider/factory
wiring and supported past-start behavior. The larger-queue protection
policy
  is unchanged pending the open review discussion.
- Uses one `IMonitoringFilter` owner for protocol filters or the
aggregation
handler, eliminating parallel effective-filter storage and retaining the
current definition during preparation. The original client filter stays
distinct.
- Integrates current master, retaining custom monitored-item creation
decisions and per-node history callbacks.

## Redundant-server alignment and samples

- Adds a protected shared historian with immutable segments, manifest
CAS publication, writer fencing, and restart recovery.
- Persists portable continuation state before exposing it to clients and
transfers ownership during mirrored-session takeover.
- Rejects eventual-consistency and active/active multi-writer historian
configurations rather than silently degrading.
- ReferenceServer and ConsoleReferenceClient demonstrate discovery,
paging, raw/modified/at-time/processed reads, annotation/structured
CRUD, event capture, and event CRUD.
- RedundantServer and RedundantClient demonstrate active/passive
history, continuation recovery after replica termination, and
post-promotion writes.
- Updates Historical Access, High Availability, profile, migration, and
sample documentation.

## Compatibility

- New 2.0 collection boundaries use `ArrayOf<T>` and public byte
payloads use `ByteString`.
- Removes the legacy public virtual
`SamplingGroupManager.CreateMonitoredItem` /
`ModifyMonitoredItem` entry points and protected creation factory, as
agreed
  in review. Migration guidance points to the monitored-item manager and
  construction-decision APIs.
- Retains established persisted formats and current write layouts.
Retired interim
notification-state formats (sample v2 / shared v4) are rejected, as
agreed in review.
- Keeps the historian integration compatible with .NET Framework and
NativeAOT.

## Validation

### Latest CI source repair

`73c619435` forwards the existing cancellation token at both
predefined-node
creation calls in the PR-added History event fixture. The merged
lifecycle
overload exposed these two CA2016 errors, which blocked History jobs,
the
solution build matrix, and CodeQL on GitHub and Azure. The exact
compiler
failure was reproduced locally; the repaired fixture passes 4/4
integration
cases on net10.0 and 4/4 on net48 with clean scoped diagnostics. No
assertions,
tests, coverage thresholds, or CI configuration were weakened.

The repaired History jobs on Linux/macOS and all four Azure solution
builds
now pass. The Windows all-TFM GitHub job instead lost its hosted
runner's
connection; no build log was recoverable, and GitHub recorded a
runner-loss
annotation rather than a compiler/test diagnostic. One unchanged retry
was
requested. Remaining checks are monitored with
`gh pr checks --watch --fail-fast --interval 30`; CI is not yet reported
as green.

### Latest base integration

`bd52e5fea` merges upstream `master` at `da7412c42`. The lifecycle
conflict
retains upstream's shared cancellation-aware registration helper and the
historian behavior. The hosted-historian fixture forwards its existing
token
to the new lifecycle overload. GDS project renames and all three AOT
test
hosts are retained, with their project references resolving after the
merge.

| Integrated scope | net10.0 | net48 |
| --- | --- | --- |
| Server lifecycle, fluent authoring/import, historian, monitored-item
and hosting regressions | 732 passed | 732 passed |
| NodeState lifecycle regressions | 216 passed | 216 passed |
| Automatically merged Raft client fixtures | 28 passed | 28 passed |

No selected tests failed or were skipped. The manually resolved files
pass
scoped formatting/analyzer checks. One CA1861 warning remains in the
incoming
`NodeSetImportIntegrationTests.cs`; that file is byte-identical to
upstream
master and was not rewritten or suppressed during integration.

### Latest feedback follow-up

`102954eaa` replaces split filter references with the shared
`IMonitoringFilter`
contract implemented by protocol filters and `AggregationFilterHandler`.
Targeted server regressions pass **103/103 on net10.0 and 103/103 on
net48**;
scoped formatting/analyzers are clean, and the updated XML-documentation
inventory has zero gaps across 188 non-generated PR C# files.

`93c5b5db8` renames `AggregationFilterHandler`, makes it the owner of
the
server-revised aggregate filter while preserving the original client
filter,
and removes the retired interim notification-state readers. Server
regressions
pass **101/101** and shared-store regressions **64/64**, on both net10.0
and
net48, with no failures or skips. Runtime-file formatting/analyzers are
clean.

`5ccf45630` completes the public/internal XML-documentation sweep. The
Roslyn
inventory finds zero undocumented eligible declarations across all 186
non-generated PR C# files; the 110-file documentation slice changes only
XML
comments and spacing. `f17765475` applies the three requested Markdown
edits.

### Earlier feedback follow-ups

`a53950737` removes single-slot notification pinning and extracts
aggregate
implementation from MonitoredItem. Targeted sampling, aggregate
modification,
history/live handoff, queue/lifecycle, restore, and serialization cases
pass
**101/101 on net10.0 and 101/101 on net48**, without failures or skips.
Scoped formatting/analyzers are clean; the focused review found no
significant
issues. The final formatted monitored-item fixtures also pass 45/45 on
net48.

The preceding `b21555568` removes the legacy sampling API bridge and
durable
notification-priority state. Its targeted Server cases passed 97/97 and
shared
subscription-store cases passed 64/64 on each framework. The broader
evidence
below remains pinned to its original tested commit, not attributed to
these
follow-ups.

### Earlier integrated-implementation validation

Validated implementation commit: **`3efae597f`**. The following commit,
`f6dbefb1c`, only adds two explanatory comment lines for the final
standards
review; it changes no executable code.

| Scope | net10.0 | net48 |
| --- | --- | --- |
| Server | 5,187 passed; 56 skipped | Affected surface: 2,160 passed; 1
reproduced baseline failure; 56 skipped |
| Client | 2,202 passed; 2 skipped | 2,204 passed; 5 skipped |
| History integration | 519 passed; 21 skipped | 519 passed; 21 skipped
|
| Redundancy server | 641 passed | 641 passed |
| Redundancy client | 124 passed | 124 passed |
| Core shared-store regression suite | 23 passed | 23 passed |

All six affected samples passed non-incremental Release/net10.0 builds
with
zero warnings and errors: ConsoleReferenceClient,
ConsoleReferenceServer,
RedundantServer, RedundantClient, MinimalBoilerServer, and
PumpDeviceIntegrationServer.

The published self-contained win-x64 NativeAOT executable is 116,924,928
bytes and has no CLR header or managed runtime dependency. Direct
execution
passed **HistoryAotTests 3/3** and **RaftAotTests 2/2**.

Final standards and specification reviews found no confirmed outstanding
high/medium functional gaps or hard standards violations. The remaining
constructor-purpose clarification is addressed in `f6dbefb1c`.

### Reproduced baseline exceptions

The net48 hosted-startup timeout also reproduces in a pristine worktree
at
the previous PR head, `24cf1416d`, in

`RegisterPostStartRegistriesWiresHistorianAndAliasStoresOnPlainServerAsync`,
with the same subsequent certificate-leak assertion. The corresponding
renamed case fails before startup in the reviewed branch as well. This
is
reported separately from the net48 affected-surface results, not counted
as
a passing full Server suite. The net48 matrix selected the historian,
aggregate, monitored-item, fluent, and continuation surfaces and
excluded
the Hosting namespace.

`DurableDataValueQueueVerifyReferenceBatchingAsync` failed at item 1000
in
the net48 affected-surface run and passed its single isolated rerun. The
same
failure at the same assertion reproduces on pristine `849e16acc`; the
test,
sample queue, queue factory, and batch persistor are unchanged. The
original
failure remains in the table rather than being erased by the rerun.

### CI source repairs

- `4be7eb8f7` keeps diagnostics/configuration managers out of
distributed
address-space replication. Those managers inherit the source interface,
but their live server state and redundancy metadata must remain
replica-local.
  The ownership regression passes on net10.0 and net48, and all nine
  redundancy sample CI tests pass locally.
- `d17f467b3` completes the sample startup repair: core managers are
also
replica-local; the startup banner no longer advertises readiness before
initialization; the producer starts after distributed services; and
native
Raft startup no longer forces disruptive extra elections. The full
sample
suite passed 9/9, with initialized `RedundancySupport=Hot`, and focused
  Raft/address-space regressions passed 8/8 and 3/3.
- `81b0b72be` moves the unchanged distributed-historian NativeAOT
scenario
into a smaller, always-run companion executable to bound Apple's linker
input. All 137 AOT tests remain present; the moved test and helpers are
  unchanged. GitHub and Azure publish/run both native executables, with
separate result directories where they share a job. The companion passed
actual Windows NativeAOT execution. Linux and Intel macOS CI passed both
  executables, but this split alone did not fix ARM64 macOS linking.
The remaining historian graph is rooted by ReferenceServer startup, not
just the relocated test. No test exclusion or speculative linker setting
  was added to conceal it.
- `8a5842913` isolates the independent MCP test graph into an always-run
native host. The existing MCP source and assembly leak hooks are linked
  unchanged, and evaluated compiler inputs prove exactly one owner for
  every test source: 133 main + 3 MCP + 1 historian = 137 declarations.
All three hosts run on every AOT platform. The main Windows native
object
is 9.35% smaller; both changed publishes had zero diagnostics. MCP
passed
  3/3; the identical main executable passed 133/133 on one unfiltered
followup after two initial endpoint-discovery connection failures, which
  remain recorded in the local evidence.
- The previous Intel macOS job successfully published and executed its
native
tests, then failed on artifact-upload DNS (`ENOTFOUND`). No upload gate,
  shared environment setting, or cache configuration was disabled.

The first repair round also encountered the unrelated
`MetricsAreEmittedForChannelLifetimeAsync` timing failure on macOS Core.
Neither that test nor channel-manager code is changed by this PR, and
its
assertions remain intact. It passed unchanged on the final repair head.

On `d17f467b3`, the Linux and macOS redundancy sample jobs pass; the
remaining
failure was ARM64 Apple's `too many large addends` assertion. The
verified
upstream relocation-anchor fix (dotnet/runtime#124721, backport
dotnet/runtime#132171) is not present in the shipping ILCompiler
10.0.11.
An experimental multi-module probe failed and was rejected, not added to
CI.
On `8a5842913`,

[`aot-macos-latest`](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34102158680/job/101678934057)
successfully publishes and executes all three native hosts on ARM64:
136 main runtime cases, 1 historian case, and 3 MCP cases, with no
skips.
The previous Apple linker blocker is resolved. Linux NativeAOT also
passes.
No assertion, test, shared environment setting, cache, or required gate
was
weakened.

The same CI run reports a separate macOS Sessions teardown failure: all
662 executed tests pass, but the global leak assertion detects one
undisposed certificate. The MCP partition changes no runtime, Sessions,
or
test-fixture code. A single unchanged diagnostic rerun is queued once
GitHub
permits rerunning that job; the failure is not suppressed or counted as
green.
Other CI checks are still pending.

## Related issues

- Fixes #4387
- Fixes #4400

## Checklist

- [ ] I have signed the CLA and read the contributing guide.
- [x] I have added regression tests and directly related documentation.
- [x] I have retained working reference and redundant-server sample
wiring.
- [x] Final validation and review evidence is complete, including the
baseline exceptions above.
- [ ] All required CI checks pass; the fail-fast rerun for `73c619435`
is pending.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
2026-09-08 11:32:23 +02:00
Copilot 90840b3e48 Register HTTPS/WSS transport bindings in ConsoleReferenceServer (#4436)
## Description

The refserver container fails to start with `No OPC UA transport
listener is registered for endpoint scheme 'opc.https'`.
`StandardServer` now validates that every scheme configured in
`BaseAddresses` has a registered transport binding, but
`ConsoleReferenceServer`'s config files
(`Quickstarts.ReferenceServer.Config.xml`,
`Ctt.ReferenceServer.Config.xml`) list `opc.https`/`opc.wss` base
addresses while the sample never registered those bindings on the server
— it relied on the `opc.tcp`-only default.

### Fix
- `UAServer<T>.CreateServerWithTransportBindings()`: shared helper (used
by both `Create()` and `StartAsync()`'s lazy-creation fallback) that
assigns `Server.TransportBindings` before start, so both call sites
can't drift out of sync.
- `CreateTransportBindings()`: builds the registry by reusing the
existing production DI extensions
(`AddOpcUa().AddOpcTcpTransport().AddHttpsTransport().AddWssTransport()`)
via a small `ServiceCollection`, instead of duplicating
factory-registration logic.

### Lifecycle
- The backing `ServiceProvider` is kept alive in `m_transportServices`
for the life of the registry (not disposed immediately after resolving
it, which would invalidate factories relying on DI-owned services) and
is disposed in `StopAsync()` once the server has stopped.
- `CreateTransportBindings()` disposes any stale provider before
building a new one, guarding against repeated `Create()`/`StartAsync()`
calls without an intervening `StopAsync()`.

```csharp
Server = CreateServerWithTransportBindings();
// ...
private ITransportBindingRegistry CreateTransportBindings()
{
    m_transportServices?.Dispose();
    var services = new ServiceCollection();
    services.AddOpcUa().AddOpcTcpTransport().AddHttpsTransport().AddWssTransport();
    m_transportServices = services.BuildServiceProvider();
    return m_transportServices.GetRequiredService<ITransportBindingRegistry>();
}
```

## Related Issues

## 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.
- [ ] 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.

<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes #4435

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-09-08 05:57:05 +00: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
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
Copilot d525bc3f28 Bound reconnect cleanup and organize samples by domain (#4276)
# Description

This PR fixes bounded ManagedSession reconnect cleanup and reorganizes
the sample applications into domain-focused folders.

## Reconnect cleanup

- Bounds reconnect integration cleanup with independent 30-second close
and dispose limits.
- Sends `CloseSession` through the awaited transport request path during
`Session.DisposeAsync`.
- Preserves explicit `Session.CloseAsync` behavior that waits for or
cancels outstanding publish requests.
- Covers the disposal request path with a regression test.

## Sample organization

- `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer`
- `samples/Redundancy`: `RedundantClient`, `RedundantServer`,
`RedundantPubSub`
- `samples/Isa95`: `MinimalIsa95Server`
- `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`,
`MinimalClient`
- `samples/DI`: `PumpDeviceIntegrationServer`
- `samples/Lds`: `ConsoleLdsServer`
- `samples/Reference`: `ConsoleReferenceClient`,
`ConsoleReferenceServer`
- `samples/PubSub`: `ConsoleReferencePubSubClient`

`UA.slnx` now exposes the same requested domain groups. Project
references, tests, runtime sample probes, CI workflows, Dockerfiles,
Compose files, helper scripts, and documentation were updated to the new
paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`.

## Validation

- `dotnet build UA.slnx`
- 3 Redundancy short-haul sample tests
- 70 DI sample tests
- 914 OpenUSD tests
- 1 WotCon model-materialization test
- 137 AOT tests
- `docker compose config` for all 6 moved Redundancy Compose files

## Related Issues

- Fixes #4273

## 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.
- [x] I built the complete `UA.slnx` solution locally.
- [ ] I ran all tests locally 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: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2026-08-21 04:35:47 +00:00