Commit Graph
2317 Commits
Author SHA1 Message Date
Randy Armstrong 7adfef0e4f Fixes after merge + final testing. 2026-06-18 13:22:18 -07:00
Randy Armstrong 5a722a7a74 Merge branch 'master' into ecc-testing-fixes-2 2026-06-17 18:24:33 -07:00
Randy Armstrong 6e62d9b58e IOP Testing Fixes. 2026-06-17 18:15:36 -07:00
Marc Schier d8a95b4e9e dotnet format sweep (whitespace + IDE + RCS) (#3879)
# Description

Full-repo `dotnet format` sweep across all 73 projects in `UA.slnx` — no behavioral changes.
2026-06-16 06:07:17 +02:00
Marc Schier 1c312e5559 Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder (#3884)
# Description

Two coordinated fixes that close issue #3546 and remove the CI gap that
allowed the regression to slip past `dotnet test`:

1. **`BinaryDecoder` hardens matrix-dimension handling**
`Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs` — wraps the
matrix-construction switch in `ReadVariantValue` (all 24 BuiltInType
cases) and both `ReadEncodeableMatrix<T>` overloads in a try/catch that
converts the deliberate `ArgumentException` thrown by
`MatrixOf<T>(values, dimensions)` validation (overflow / negative /
length-mismatch) into a `ServiceResultException(BadDecodingError)`.
Attacker-controlled wire dimensions now flow through the standard
decoder rejection channel instead of escaping as an unhandled exception.
The `MatrixOf<T>` ctor validation itself (security hardening at
`MatrixOf.cs:178-217`) is unchanged.

2. **`FuzzCrashAssets` is now assertive**
`Fuzzing/Common/Fuzz.Tests/FuzzTargetTestsBase.cs` — replaces silent
log-and-count with `Assert.That(failures, Is.Empty, …listing failed
assets)`. CI now breaks loud the moment a residual `Assets/crash-*`
reproduces on any target. `.github/agents/fuzz-tester.agent.md` doc note
rewritten to match.

The originally reported "DataTypeNode / VariableNode `IsEqual`
non-determinism" symptom turned out to already be implicitly resolved by
the modern `Equals` overhaul (`ArrayOf<T>` value-equality,
`ExtensionObject.Equals` deep-compare, `IEquatable<T>` on
`ReferenceNode` / `RolePermissionType`). The 8 new synthetic round-trip
tests + 6 permanent corpus seeds confirm this end-to-end.

## Regression coverage

* `Tests/Opc.Ua.Types.Tests/Encoders/BinaryDecoderTests.cs` — 3 new
tests lock in the `BadDecodingError` contract for malformed matrix
dimensions (overflow, negative, length-mismatch).
* `Fuzzing/Opc.Ua.Encoders.Fuzz.Tests/Issue3546RoundTripTests.cs` — 8
synthetic round-trip tests for populated `DataTypeNode` and
`VariableNode`, exercising the implicated fields (non-empty References,
RolePermissions, `DataTypeDefinition` ExtensionObject body, `Value`
Variant carrying ExtensionObject, `ArrayDimensions` empty / null /
non-empty).
* `Fuzzing/Opc.Ua.Encoders.Fuzz.Tools/Encoders.Testcases.cs` +
`Fuzzing/Opc.Ua.Encoders.Fuzz.Corpus/Testcases.{Binary,Json,Xml}/` —
`DataTypeNodeMessage` / `VariableNodeMessage` seed generators plus 6
generated corpus files. These feed `FuzzGoodTestcases` automatically on
every test run.

## Verification

| Suite | TFM | Result |
|---|---|---|
| `Opc.Ua.Encoders.Fuzz.Tests` | net10.0 | **4625** / 0 |
| `Opc.Ua.Encoders.Fuzz.Tests` | net48   | **4625** / 0 |
| `Opc.Ua.Types.Tests` (BinaryDecoder / Matrix / Variant /
EncodeableMatrixRoundTrip subset) | net10.0 | **985** / 0 |

The single pre-existing
`Assets/crash-0483107a22caf892cff4f795c96ab14c581a8f55` no longer
escapes any fuzz target — the matrix-dimensions fix turns it into a
clean parser-rejected input.

## Related Issues

- Fixes #3546

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran the affected tests locally (`Opc.Ua.Encoders.Fuzz.Tests` on
net10.0 + net48, targeted `Opc.Ua.Types.Tests` subset on net10.0); full
UA.slnx still pending CI.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-15 15:55:04 +02:00
03ed5c82d5 Port session activation race condition fix from master378 to master (#3846)
# Description

Fixes a null reference exception that occurs when a session expires
between `CreateSession` and `ActivateSession` requests. The issue stems
from a race condition where the server doesn't properly validate session
existence during activation when the session has already timed out.

The fix is a minimal, targeted change in
`StandardServer.ActivateSessionAsync`: a null-coalescing throw ensures
the session exists after lookup, returning `BadSessionIdInvalid` if it
has expired.

Example flow that previously failed:
```csharp
// Session created successfully
var createResp = await client.CreateSessionAsync(...);

// Session expires before activation
await Task.Delay(sessionTimeout);

// Previously: NRE from null session
// Now: BadSessionIdInvalid returned correctly
var activateResp = await client.ActivateSessionAsync(...);
```

## Note on scope

Earlier revisions of this port also modified
`SessionManager.ValidateRequestAsync` (removing `ActivateSession` from
the early-return bypass) and `Session.ValidateRequest` (allowing
`ActivateSession` on non-activated sessions). These changes were
reverted because they cause the secure channel id check in
`Session.ValidateRequest` to run on reactivation, breaking valid OPC UA
reconnect-on-alternate-channel scenarios — the same regression flagged
in the source PR #3844. Symptoms included `BadSecureChannelIdInvalid`
failures in tests such as
`ApplicationCertificateRotationRehandshakesOpenManagedChannelsAsync`,
`ReconnectJWTAsync`, and `ReconnectSessionOnAlternateChannelAsync`.

## Related Issues

Ports the fix from PR #3844 which addressed this issue on the master378
branch.

## Checklist

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
2026-06-15 08:29:51 +02:00
Marc Schier 0f710adc8e Split MigrationGuide.md, add What's New in 2.0, rewrite Profiles.md and Landing page (#3745) (#3874)
# Description

This PR is a documentation overhaul for OPC UA .NET Standard 2.0. It
splits the monolithic `Docs/MigrationGuide.md` into per-version sub-docs
(so a coding agent can load only the relevant section), adds a new
"What's New in 2.0" landing doc, rewrites `Docs/Profiles.md` and the
repo-root `README.md` to reflect what master actually implements, and
renames the misnamed `Docs/ALiasNames.md` to `Docs/AliasNames.md`.

The branch is `docupdates` and contains **6 commits** stacked on top of
`master`. All changes are doc-only except for `HelpLinkUri` string
updates in `Opc.Ua.MigrationAnalyzer` (all 119 analyzer tests still pass
× 5 TFMs).

## What changed

### `Docs/MigrationGuide.md` is now a thin landing page

Before: a single ~144 KB / 1,549-line document covering 22 distinct
migration topics in one file. After: a small 3.8 KB / 64-line landing
page that introduces general principles, lists a per-version table, and
keeps the tiny `1.05.377 → 1.05.378` and `1.04 → 1.05` legacy sections
inline.

The 1.5.378 → 2.0 content moves to a new **`Docs/migrate/2.0.x/`**
folder with a README index plus 12 thematic sub-docs, mapped 1-to-1 to
the existing H3 sections:

| Sub-doc | Folded H3 sections |
| --- | --- |
| `telemetry.md` | Telemetry and Logging |
| `packages.md` | Package, Target Framework and Dependency Changes |
| `source-generation.md` | Source Generation |
| `types.md` | Improved Type safety |
| `encoders.md` | Encodeable Factory + Encoders/Decoders + Complex Types
|
| `node-states.md` | Node States + INodeCache changes |
| `identity.md` | User Identity Token Handlers + User Identity Providers
+ Secrets |
| `certificates.md` | Certificate Management + ICertificateProvider |
| `configuration.md` | Configuration + Session/Browser State Persistence
|
| `sessions-subscriptions.md` | GDS Client API modernisation +
ManagedSession + Subscriptions/Transports |
| `alarms-model-change.md` | Alarms and Conditions + Address-space model
change tracking |
| `timeprovider.md` | Time and Timer abstraction (TimeProvider) |

Each sub-doc starts with a **"When to read this"** lead paragraph
(analyzer rules / symptoms / API areas it covers) and ends with a
sibling-links footer. `Docs/migrate/2.0.x/README.md` has a symptom →
sub-doc index table reused as-is in the agent skill.

### `opcua-v20-migration` agent skill is now context-efficient

`.agents/skills/opcua-v20-migration/SKILL.md` gains a "Migration sub-doc
index — load only what you need" section near the top with an explicit
*"Do not load `Docs/MigrationGuide.md` into context"* instruction and a
table keyed by symptom / API area / analyzer rule ID, so an agent loads
at most one sub-doc per migration scenario. Path references in
`README.md`, `references/analyzer-rules.md`,
`references/migration-patterns.md`, and `references/source-generator.md`
are updated to point at the new sub-doc locations.

### New `Docs/WhatsNewIn2.0.md`

Developer-facing narrative tour of the 1.5.378 → 2.0 delta (~320 lines,
hybrid grouping: cross-cutting themes first — type system &
immutability, async/cancellation/TimeProvider, DI & hosting, Native AOT,
source generators, companion specs Part 9/11/13/16/17/18/20/100 + WoT +
LDS, performance, security — then by layer: Server,
Client/Sessions/Subscriptions, PubSub, GDS, Tooling, CI). Paragraph per
feature, no PR/commit references, links to the deeper per-feature docs.
Linked from `Docs/README.md` between `Profiles.md` and
`MigrationGuide.md`.

### `Docs/Profiles.md` rewritten to reflect master

Adds Functional Facets for newly-implemented features (Part 9 Alarms,
Part 11 Historical Access, Part 13 Aggregates, Part 16 State Machines,
Part 17 Alias Names, Part 18 Role Management, Part 20 FileSystem server,
Part 100 Device Integration / Software Update, OPC 10100-1 WoT, Node
Management, Local Discovery Server, Auditing, Model Change Tracking).
Expands Client Profiles (classic `Session` vs `ManagedSession`, V2
subscription engine, `FileSystemClient`, `HistoryClient`, `AlarmClient`,
NodeSet export, source-generated typed `ObjectType` proxies). Adds a
Reference Applications section listing all of `Applications/*`
(Quickstarts/Ctt/Mono configs, Reference Client, LDS, MCP, Minimal* /
Pump* samples). Security policies and transport profiles confirmed
against `Stack/Opc.Ua.Core/Security/Constants/*` — no changes needed
there.

For new facets whose canonical URI could not be confidently verified
against `profiles.opcfoundation.org` (the registry is fully
SPA-rendered), the doc describes the facet by name and points readers at
the OPC Foundation registry rather than fabricating URIs. The
`ServerProfileArray` in the reference-server configs is intentionally
left unchanged to avoid CTT-compliance regressions from advertising an
unverified URI.

### Repo-root `README.md` cleaned up

Front page now makes the 2.0 / `master378` context explicit. New
5-section structure with one emoji per heading: 🆕 version callout → 📦
What it is → 🚀 Getting started (grouped sample list linking to each
sample's own README) → 🔧 Migrating from 1.5.378 → 🤝 Contributing &
license → 📚 Further reading. Removes the "New in 1.04.368 / 1.05.373 /
.374 / .375 / .376 / .378 / 2.0" laundry list, the dated "Key Features
and Updates in OPC UA 1.05" section, inline VS / `dotnet run` build
instructions, the "Start the server" walkthrough, the "Remarks"
(self-signed certs + LDS) notes, and the inaccurate `.NET Standard 2.1`
overview claim. Trims the 11 status badges down to 5 essentials
(release, NuGet downloads, build, tests, codecov).

### File rename — `Docs/ALiasNames.md` → `Docs/AliasNames.md`

The on-disk filename had a stray capital `L` while every link in
`Docs/README.md`, `Docs/Profiles.md`, etc. used the lowercase spelling.
Renamed via case-only `git mv` (Windows two-step). Existing links across
the docs now resolve on GitHub's case-sensitive filesystem.

### `Opc.Ua.MigrationAnalyzer` `HelpLinkUri` updates

- `Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticIds.cs` —
`HelpLinkUriBase` repointed from `Docs/MigrationGuide.md#` to
`Docs/migrate/2.0.x/README.md#` so the rule-ID help links land on the
symptom → sub-doc index instead of a generic landing page.
- `Tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs`
— UA0021 / UA0022 message strings reference the new
`Docs/migrate/2.0.x/certificates.md`.
-
`Tools/Opc.Ua.MigrationAnalyzer/Analyzers/UA0021CertificateValidatorRenameAnalyzer.cs`
— XML-doc path updated.
- `Tools/Opc.Ua.MigrationAnalyzer/NugetREADME.md` and
`AnalyzerReleases.Unshipped.md` — references repointed.

## Verification

- `Opc.Ua.MigrationAnalyzer` builds with **0 Warning(s), 0 Error(s)** in
Release.
- `Opc.Ua.MigrationAnalyzer.Tests`: **119/119 tests pass × 5 TFMs**
(net48, net472, net8.0, net9.0, net10.0).
- `ConsoleReferenceServer` builds with **0 Warning(s), 0 Error(s)** in
Release on net10.0 (verifies the reference-server config XML still
parses cleanly after our doc-touching).
- **Every relative markdown link** in the new / changed docs (the new
`Docs/migrate/2.0.x/*`, `Docs/MigrationGuide.md`,
`Docs/WhatsNewIn2.0.md`, `Docs/Profiles.md`, `Docs/Sessions.md`,
`Docs/IdentityProviders.md`, the agent skill files, and the new
`README.md`) was walked programmatically — **0 broken links**.
- Deep heading anchors verified to match GitHub-generated slugs
(`MigrationGuide.md#encoders-and-decoders`,
`Sessions.md#1-session--the-opc-ua-session-primitive`,
`Sessions.md#3-managedsession--the-connection-state-machine-facade`,
`migrate/2.0.x/identity.md#user-identity-providers`,
`migrate/2.0.x/identity.md#user-identity-token-handlers`,
`migrate/2.0.x/encoders.md#encoders-and-decoders`,
`migrate/2.0.x/node-states.md#inodecache-changes`,
`migrate/2.0.x/certificates.md#certificatemanager-and-segregated-interfaces`,
`migrate/2.0.x/sessions-subscriptions.md#managedsession-and-automatic-reconnection`).
- No `ALiasNames` (capital L) references remain anywhere in the repo
(case-sensitive search).

## Commit history (6 commits)

1. `548d0b5ed` — Docs: rename `ALiasNames.md` to `AliasNames.md`
2. `8a8db8c30` — Docs: add `WhatsNewIn2.0.md` (1.5.378 → 2.0 highlights)
3. `e69b2169e` — Docs: rewrite `Profiles.md` to reflect master
implementation
4. `5fb8a282b` — Merge remote-tracking branch `origin/master` into
`docupdates` (resolves overlap with #3854's `Docs/Subscriptions.md`
rename of `StreamingSubscription.md`)
5. `d853fbc74` — Docs: split `MigrationGuide.md` into per-version
sub-docs (#3745)
6. `65d6e6851` — Docs: rewrite repo-root `README.md` for 2.0

## Related Issues

- Fixes #3745 (MigrationGuide.md document structure)

## 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. *(N/A — documentation change;
existing `Opc.Ua.MigrationAnalyzer.Tests` (119 tests × 5 TFMs) still
pass after the `DiagnosticDescriptors` / `DiagnosticIds` text updates.)*
- [x] I have added all necessary documentation. *(this PR is the
documentation; all internal links verified)*
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. *(both `Opc.Ua.MigrationAnalyzer` and
`ConsoleReferenceServer` build with 0 warnings / 0 errors)*
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. *(only the
migration-analyzer test suite was rerun, since this PR is doc + a few
`HelpLinkUri` strings; full UA.slnx run pending CI)*
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings. *(pending CI)*
- [ ] I have addressed **all** PR feedback received.
2026-06-14 17:48:27 +02:00
Marc Schier f358078b1c [Client] Central channel manager: ref-counted shared channels, coalesced reconnect, IRetryBudget (#3288) (#3852)
# Description

Introduces a central `IClientChannelManager` that owns client-side
transport channels with reference counting, sharing, coalesced
reconnect, and asynchronous participant notification. Sessions,
discovery clients, and registration clients targeting the same
`ConfiguredEndpoint` (with the same reverse-connect identity) now share
a single underlying `ITransportChannel`; reconnect is transparent to
callers; and the previous `AttachChannel`/`DetachChannel` +
`SessionReconnectHandler` patterns are obsoleted.

## What lands

### Core types (`Stack/Opc.Ua.Core/Stack/Client/`)

- `ManagedChannelKey`, `ChannelState`, `ChannelStateChange`,
`ParticipantReconnectResult`
- `IReconnectParticipant`, `IManagedTransportChannel`,
`IClientChannelManager`
- `IChannelReconnectPolicy` + `ExponentialBackoffChannelReconnectPolicy`
(default mirrors historical `SessionReconnectHandler`: 500 ms → 30 s
exponential)
- `IRetryBudget` + `RetryBudget` — shared retry deadline so the
two-level retry no longer compounds multiplicatively
- `ClientChannelManager` — single non-partial sealed class with
SOLID-extracted internal types under
`Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/`: `ChannelEntry`
(behind `IChannelEntryHost`), `ManagedTransportChannelLease`,
`ClientChannel`, `ClientChannelManagerMetrics` (IMeter),
`ClientChannelManagerCertRotation` (auto `ReconnectAllAsync` on
`CertificateManager` rotation), `ClientChannelManagerDiagnostics`
(ActivitySource + EventSource). Refcount, coalesced reconnect, and
transparent faulted-entry recovery via `SwapFaultedEntryAsync` (swaps a
lease's underlying entry on subsequent `ReconnectAsync` when the prior
cycle Faulted/Closed, preserves participant, back-off via policy
`GetDelay(SwapCount)`)

### Three-state lifecycle gate

`Disconnected` → `TransportConnecting` / `TransportReconnecting` →
`TransportConnectedSessionReactivating` → `Ready` (or `Faulted` /
`Closed`). Only `Ready` releases the service-call gate; the manager
bypasses the gate internally for participant `ActivateSession` traffic
during reactivation via an `AsyncLocal` scope.

### Session integration (`Libraries/Opc.Ua.Client/Session/`)

- New `Session.CreateAsync(IClientChannelManager, …)` overload
- `Session` implements `IReconnectParticipant`; reactivation handler
distinguishes `Reactivated` / `RequiresSessionRecreate` /
`TransientFailure` / `FatalForParticipant` / `FatalForChannel`
- `Session.ReconnectAsync(ct)` automatically delegates to the channel
manager when wired
- `Session.RecreateInPlaceAsync` swaps managed-channel leases for
failover-to-different-endpoint via a new atomic
`IClientChannelManager.GetAsync(endpoint, participantFactory, …)`
overload; same-key recreates delegate to manager reconnect;
explicit-channel/connection callers retain legacy behavior
- KA failure routes through
`IClientChannelManager.ReconnectAsync(channel)`

### ManagedSession integration

- `WithChannelManager(...)` builder + ctor param; DI-resolved
automatically
- Two-level retry: channel-mgr handles transparent reconnect; outer
`ConnectionStateMachine` + `IReconnectPolicy` only triggers on terminal
channel `Faulted`. Outer-state churn suppressed during channel-mgr
cycles via `_channelReconnectInProgress` flag wired through
`IManagedTransportChannel.StateChanged`
- New `ManagedSession.ChannelStateChanged` event surfaces transparent
reconnects
- `ConnectionStateChangedEventArgs.UnderlyingChannelState` populated
when outer reconnect was triggered by a channel fault
- Shared `IRetryBudget` propagated so the layers enforce a single
max-total-time

### Discovery / Registration / GDS

Channel-manager-aware `CreateAsync` overloads on `DiscoveryClient`,
`RegistrationClient`, `ServerPushConfigurationClient`,
`LocalDiscoveryServerClient`, `GlobalDiscoveryServerClient`. Each
registers a minimal `IReconnectParticipant`; co-located sessions and
discovery probes targeting the same endpoint share a channel.

### HTTPS transport (`Stack/Opc.Ua.Bindings.Https/`)

- New `IOpcUaHttpClientFactory` abstraction
- HTTPS / OPC-HTTPS channels migrated to `IHttpClientFactory` +
`Microsoft.Extensions.Http.Resilience` (10.6.0; AOT-compatible on net10
via source generators; legacy direct-HTTP path retained for older TFMs /
no-DI consumers)

### DI

`AddOpcUa().AddClient(...)` registers `IClientChannelManager` and the
named HTTPS `HttpClient` + standard resilience handler automatically.
`ManagedSessionBuilder` resolves the manager from DI when present.

### Obsoletions (functional, `[Obsolete]` with guidance)

- `IClientBase.AttachChannel` / `DetachChannel`
- `SessionReconnectHandler`
- `SessionExtensions.ReconnectAsync(connection, ct)` and `(channel, ct)`
legacy overloads

### Tests

- Core: `ClientChannelManagerManagedTests`,
`ClientChannelManagerCertRotationTests`, `RetryBudgetTests`,
`OpcUaHttpClientFactoryTests` — **56 / 56 pass**
- Client: `ManagedSession*Tests` updated; legacy
`SessionReconnectHandlerTests` + `SessionExtensionsTests` preserved with
`#pragma warning disable CS0618` — **129 / 129 pass**
- Sessions integration: `ChannelManagerSharing` / `TransparentReconnect`
/ `SessionLifecycle` / `CertRotation` — **7 pass, 1 skipped**
(`ChannelManagerExhaustionEscalatesAndRecoversWhenServerReturns`
un-`[Ignore]`d and implemented in this PR via `SwapFaultedEntryAsync`;
remaining skip is end-to-end `RequiresSessionRecreate` plumbing —
carry-forward)
- Stress (consolidated `Tests/Opc.Ua.Stress.Tests/Subscriptions/` +
`Channels/`): chaos / contract / soak / lifecycle / leak-accuracy / gap
/ participant-result-aggregation tests merged from the
previously-separate `Opc.Ua.Channels.Stress.Tests` project; nightly run
via `.github/workflows/stress-test.yml`

Builds clean (0 warnings / 0 errors) across all six TFMs: `net472`,
`net48`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`.

### Docs

- `Docs/Sessions.md` new § 4 documents the channel manager, three-state
gating, two-level retry with shared `IRetryBudget`, HTTPS resilience
layering
- `Docs/MigrationGuide.md` migration recipes for the four obsoleted API
groups
- `Docs/DependencyInjection.md` channel-manager and HTTPS factory
registration

## Carry-forward (separate PRs)

- End-to-end `ParticipantReconnectResult.RequiresSessionRecreate` →
`Session.RecreateAsync` lifecycle wiring (currently incomplete — flagged
in the 1 remaining skipped integration test)

## Related Issues

- Fixes #3288
- Builds on #3282 (PR that introduced the initial
`ITransportChannelManager` skeleton)

## 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.
*(Channel-manager-specific tests + ManagedSession/client tests verified
across all 6 TFMs; full UA.slnx run is pending CI.)*
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-13 16:34:48 +02:00
Marc Schier 7728b65e8d CI: Fix pipeline cannot be cancelled regression and flaky tests (#3872) 2026-06-13 08:33:29 +02:00
Marc Schier 87f253bb47 Support unbounded monitored items per subscription via automatic partitioning (#3869)
# Description

Adds **unbounded monitored items per subscription** to the V2
`ManagedSession` subscription engine via automatic, transparent
partitioning. Top-level public API and runtime semantics for callers
that stay under the server's `MaxMonitoredItemsPerSubscription` cap are
unchanged; callers that exceed the cap continue to succeed instead of
failing with `BadTooManyMonitoredItems`.

## Approach (OPC UA Part 4 1.05.07 conformant)

- The public `ISubscription` is now a `LogicalSubscription` that owns
one **primary** and zero-or-more **secondary** `Subscription`
partitions, each a real server-side subscription.
- `CompositeMonitoredItemCollection` aggregates the per-partition
`MonitoredItemCollection` views so `Items`, indexer access, and
enumeration look unsharded to the caller.
- `PartitionPlacementPolicy` decides the target partition at
`TryAddMonitoredItem` time:
- **`StrictAffinity`** — keep items tagged with the same `Affinity`
co-located until the partition fills, then split with a one-time
warning.
- **`BestEffortAffinity`** (default) — prefer co-location but spill
silently when the partition is full.
- No affinity → fall back to the least-loaded partition (or the
primary).
- `PartitionForwardingHandler` proxies notifications from secondary
partitions to the caller's handler with the **primary partition's**
`ISubscription` identity, so handler code never observes the split.
- **Durable subscriptions (Part 4 §5.13.9):** when `SetAsDurableAsync`
runs before any items are added, the wrapper records the intent and
installs an `OnAfterCreateAsync` hook on every existing partition. Each
newly-minted secondary inherits the hook so `SetSubscriptionDurable` is
invoked between `CreateSubscription` and `CreateMonitoredItems` — the
only order the standard allows.
- **Snapshot/restore** preserves the partition layout so reconnect and
failover restore items into the same partition they were created in,
with affinity tags intact.
- Secondary partitions that go idle (no items, no pending
`CreateMonitoredItems`) are deleted to release server resources.

## Performance

Hot-path dispatch goes through the primary partition unchanged for the
unsharded case (single-partition fast path); the forwarding handler only
allocates on the multi-partition path. The performance smoke test in
`Tests/Opc.Ua.Stress.Tests` shows no regression.

## New configuration

```csharp
session.AddSubscription(handler, new SubscriptionOptions
{
    MaxMonitoredItemsPerPartition = 5000,          // optional cap per server-side subscription
    PartitionPlacementPolicy =                      // optional placement policy
        PartitionPlacementPolicy.StrictAffinity
});

sub.TryAddMonitoredItem(
    name,
    nodeId,
    o => o with { Affinity = "machine-A" },         // optional co-location hint
    out _);
```

Defaults preserve pre-change behaviour (`MaxMonitoredItemsPerPartition =
uint.MaxValue`, `BestEffortAffinity`).

## New types (Libraries/Opc.Ua.Client/Subscription)

| File | Purpose |
| --- | --- |
| `CompositeMonitoredItemCollection.cs` | Aggregate collection view |
| `ILogicalSubscription.cs` / `IPartitionedSubscription.cs` | Internal
contracts used by tests and infrastructure |
| `LogicalSubscription.cs` | Wrapper that owns the partitions |
| `PartitionForwardingHandler.cs` | Notification proxy |
| `PartitionPlacementPolicy.cs` | Affinity-aware item placement |

Modified: `ISubscriptionNotificationHandler`, `MonitoredItemManager`,
`MonitoredItemOptions` (adds `Affinity`), `Subscription`,
`SubscriptionManager`, `SubscriptionOptions` (adds
`MaxMonitoredItemsPerPartition`, `PartitionPlacementPolicy`),
`SubscriptionManagerSerializer`, `MonitoredItemStateSnapshot`,
`SubscriptionStateSnapshot`, `ManagedSessionExtensions`.

## Documentation

- New: `Docs/UnboundedSubscriptions.md` — feature overview, affinity
semantics, snapshot/durable interaction.
- Cross-links added in `Docs/Sessions.md`, `Docs/MigrationGuide.md`,
`Docs/README.md`.

## Validation

| Suite | Result |
| --- | --- |
| Unit tests for new types — `Opc.Ua.Client.Tests.Subscription.*`
(net10.0 + net48) | **39 / 39** |
| All `Opc.Ua.Client.Subscriptions.*` unit tests (net48) | **152 / 152**
|
| Integration — `Opc.Ua.Subscriptions.Tests.UnboundedSubscriptionTests`
(net10.0 + net48) | **3 / 3** (1 durable-only skipped) |
| Integration — `Opc.Ua.Subscriptions.Durable.Tests.*V2Async` (net10.0)
| **6 / 6** (incl. new `DurableLifetimeAppliesToAllPartitionsV2Async`) |
| Adjacent — `Opc.Ua.Sessions.Tests` (net10.0) | **690 / 690** |
| Adjacent — `Opc.Ua.Subscriptions.Classic.Tests` (net10.0) | **32 /
32** |
| Adjacent — `Opc.Ua.Features.Tests` (net10.0) | **128 / 128** |
| Adjacent — `Opc.Ua.Stress.Tests` (net10.0) | **159 / 159** |
| NativeAOT smoke — `Opc.Ua.Aot.Tests.SubscriptionAotTests` (win-x64) |
**5 / 5** (incl. new `UnboundedSubscriptionWithCapAotAsync`) |

## Compatibility

- **No public type removed**, **no obsoletions added**.
- Public surface on `ISubscription` / `ManagedSession` unchanged.
- New options non-breaking by default (`MaxMonitoredItemsPerPartition =
uint.MaxValue` preserves prior behaviour; `BestEffortAffinity` matches
the closest analogue to the prior single-partition semantics).
- Classic V1 subscription engine unchanged.

## Related Issues

- _No tracking issue filed — please reference the appropriate one if any
exists._


## Security review

Scoped against Microsoft Service Tree
``59eec07a-6f20-42b9-b41b-d20e0a6322da`` (Azure Industrial IoT) using a
combination of the security-agent MCP (code-integrity, secrets-usage,
tls, policy-governance, ai-chat) and the ``security-review`` SAST
sub-agent on the PR diff. Bar: SDL secure-coding for a client library +
OPC UA Part 2 / Part 4 §5.5 / §5.13 spec.

Fixed in commit ``c6997689``:

| # | Sev | Title | Fix |
| --- | --- | --- | --- |
| 1 | CRITICAL | Unbounded partition fan-out under hostile server
(CWE-400/770) | Added ``SubscriptionOptions.MaxPartitionCount`` (default
``32``), enforced in ``PartitionPlacementPolicy``, surfaced as
``PlacementDecision.RejectMaxPartitionCountReached`` |
| 2 | HIGH | Snapshot ``LogicalGroupId`` option-consistency not
validated (CWE-502) | ``ValidateAndSortGroup`` now asserts all snapshots
in a group agree on every subscription-wide option |
| 3 | MEDIUM | Restore-path secondaries bypassed
``PartitionForwardingHandler`` (CWE-362) |
``AttachSecondaryPartitionAsync`` now reuses the wrapper's forwarding
handler when present |

Findings explicitly assessed and dismissed (sub-agent confirmed):
* Affinity-index growth — uses ``StringComparer.Ordinal`` (no
CVE-2022-29117 class); growth is application-controlled, not
attacker-controlled.
* Idle-timer race vs ``DisposeAsync`` — race exists; worst case is a
swallowed ``ObjectDisposedException``. No attacker trigger.
* Cross-wrapper notification routing — each ``Add`` allocates its own
forwarding handler; ``BindLogical`` rejects re-binding; no structural
path for crosstalk.
* OPC UA Part 4 §5.13.6 (``SetTriggering`` cross-partition) — enforced
by ``LogicalSubscription.SetTriggeringAsync``.
* OPC UA Part 4 §5.13.9 (durable hook ordering) — installed uniformly on
every minted partition.

Validation: ``Opc.Ua.Client`` builds clean on net10.0; 43 V2 unit tests
+ 3 unbounded integration tests + 6 V2 durable integration tests all
pass.
## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-12 18:57:38 +02:00
Marc Schier 736b92ac7e Fix #3768: stop emitting unimplemented Optional children on singleton instances (#3828)
# Description

Singleton instances declared in `StandardTypes.xml` and
`Opc.Ua.NodeSet2.xml` (`Server`, `Server.ServerConfiguration`,
`HistoryServerCapabilities`, `OPCUANamespaceMetadata`, the well-known
role objects, the Part 17 `Aliases` category, etc.) were emitting
Optional Variable/Method children that the SDK does not implement — for
example `ServerConfiguration.ApplicationUri`, `ProductUri`,
`ApplicationType`, `HasSecureElement`, `CancelChanges`,
`ResetToServerDefaults`, `Server.UrisVersion`,
`Server.EstimatedReturnTime`, `Server.LocalTime`,
`Server.ServerCapabilities.ConformanceUnits` and friends.

The cause was the source generator promoting every type-definition
child's modelling rule into the singleton-instance factory whenever
`UseTypeDefinitionModellingRules` was enabled (#3799), which made every
Optional child unconditionally appear in the singleton's `CreateXxx`
body.

This PR fixes the regression at the generator level (without touching
`StandardTypes.xml`) and lets the SDK opt back in to just the Optional
children it actually implements.


## New: chainable `Add{Child}` API with configure-callback overloads

Per review feedback ("the existing predefined node is used. A user adds
optional children using `.AddXY(NodeId? nodeId = default)`. The Add
calls should be possible to chain. … Instead of AddAndGet, emit an Add
overload with a callback to configure the created node state further,
add another Add to take a condition predicate that decides whether the
Create is executed or not"), the source generator now emits **five**
chainable `Add{Child}` overloads per Optional child of every typed state
class. All return the **owner** (`this`) so multi-child wiring collapses
into a single fluent chain:

```csharp
// (1) base — idempotent ensure-child
public TOwner Add{Child}(ISystemContext context, NodeId? nodeId = default);

// (2) Action — always run, mutate the just-created child in-line
public TOwner Add{Child}(ISystemContext context, Action<TChild> configure, NodeId? nodeId = default);

// (3) Action — conditional; when false: no Add, no callback
public TOwner Add{Child}(ISystemContext context, bool condition, Action<TChild> configure, NodeId? nodeId = default);

// (4) Func — always run, transform/replace the typed slot
public TOwner Add{Child}(ISystemContext context, Func<TChild, TChild> configure, NodeId? nodeId = default);

// (5) Func — conditional
public TOwner Add{Child}(ISystemContext context, bool condition, Func<TChild, TChild> configure, NodeId? nodeId = default);
```

The previous `AddAndGet{Child}(context, NodeId?)` helper is **removed**;
every call site is migrated to one of the new overloads. The Action
overloads cover the common "set Value / wire OnCallAsync" case in-line;
the Func overloads allow callers to replace the typed slot with a
wrapper or subclass instance. The boolean overloads short-circuit when
the condition is `false`.

When `nodeId` is `null`: if `context.NodeIdFactory` is non-null the
helper mints a fresh NodeId via `context.NodeIdFactory.New(context,
child)`; otherwise it leaves the type-level NodeId assigned by the
generated factory unchanged (the caller can patch later).

`NodeState.Create(context, source)` stays authoritative (the loaded
NodeSet XML / passive source is the source of truth for what children
exist). The new chainable `Add{Child}` overloads are the
runtime-extension API for SDK code and downstream users that want to add
Optional children programmatically after load.

A new `Tokens.OwnerClassName` carries the owning typed state class name
through the per-child template loop so `Add{Child}` can return `this`
typed as the owner; the template engine's outer-scope lookup cascades
the value from `AddNodeStateClassTypeReplacements` /
`AddNodeStateClassMethodTypeReplacements` into the per-child template
scope.

### SDK migration in `Libraries/Opc.Ua.Server`

- `DiagnosticsNodeManager` add-back helpers collapse into single fluent
chains; the `SetSubscriptionDurable` conditional folds into the Server
chain via the bool-condition overload
(`AddSetSubscriptionDurable(context, m_durableSubscriptionsEnabled, _ =>
{ }, MethodIds.Server_SetSubscriptionDurable)`); the `OperationLimits`
add-back inlines via
`.AddOperationLimits(...).OperationLimits!.AddMax...()`.
- `ConfigurationNodeManager` chains
`AddGetCertificates(context).AddCreateSelfSignedCertificate(context)`
before `Create(context, passiveNode)`.
- `HistoricalDataConfigurationInstaller.PopulateProperties` is now a
single fluent chain using the conditional Action overload —
`.AddMaxTimeInterval(context, capabilities.MaxTimeInterval > 0, c =>
c.Value = capabilities.MaxTimeInterval)` replaces the `if (cond) {
config.AddAndGetMaxTimeInterval(context).Value = ...; }` ceremony.
- `KeyCredentialPushSubject` folds `OnCall = null; OnCallAsync = ...`
into Action callbacks on the chain: `state.AddUpdateCredential(context,
c => { c.OnCall = null; c.OnCallAsync = OnUpdateCredentialAsync;
}).AddDeleteCredential(...)`.
- `WotConnectivityNodeManager.WireManagementMethods` chains
`.AddDiscoverAssets(ctx, c => c.OnCallAsync =
...).AddCreateAssetForEndpoint(...).AddConnectionTest(...).AddSupportedWoTBindings(...)`.
- `AssetRegistry` and `AlarmConditionTypeHolder` updated from `state.X
??= state.AddX(context)` to plain `state.AddX(context)` + null-forgiving
access on the typed slot (idempotent `Add{Child}` makes the `??=`
ceremony obsolete).

## Generator fix

`Tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs`:

`NodeToGenerate` gains a structural `IsUnderSingletonInstance` flag, set
`true` on the singleton-instance root in `CollectNodesToGenerate` and
propagated to every descendant in `GetChildren`. Mirrors the existing
`RootIsTypeDefinition` propagation pattern; the gate becomes an O(1)
lookup.

```csharp
bool isSingletonInstanceContext =
    m_context.Options.UseTypeDefinitionModellingRules &&
    node.IsUnderSingletonInstance &&
    node.Parent != null &&
    instance is VariableDesign or MethodDesign;

ModellingRule effectiveRule = isSingletonInstanceContext
    ? GetEffectiveModellingRule(node, instance)
    : instance.ModellingRule;
```

The `topLevelInstanceException` is preserved for **TYPE-DEF** roots —
its `UseTypeDefinitionModellingRules` gate only fires when the parent is
an `InstanceDesign` singleton. Without this, the type-def factories
(e.g. `CreateNamespaceMetadataType`) stop emitting their Optional
children which breaks deep code paths that rely on the type-def factory
shape (e.g.
`ConfigurationNodeManager.CreateNamespaceMetadataStateAsync`).

### Why the gate is transitive but narrowed to Variables/Methods

The gate applies **transitively** to every Variable/Method descendant of
a singleton-instance subtree. Deep Optional Variable/Method descendants
such as `Server.ServerCapabilities.MaxArrayLength`,
`Server.ServerCapabilities.OperationLimits.MaxNodesPerRead`,
`Server.ServerRedundancy.RedundantServerArray`, `Server.UrisVersion`,
`Server.EstimatedReturnTime`, `Server.LocalTime`,
`PublishSubscribe.AddConnection` / `RemoveConnection`, the Optional
methods on every well-known role, the OPCUANamespaceMetadata Default*
properties, and `Aliases.LastChange` are now all suppressed by the
generator.

Optional **Object** instances under singletons are intentionally exempt:
they are still emitted by the singleton-specific factory with their
correct instance-level NodeIds. Their subtrees use well-known descendant
NodeIds (e.g.
`ServerConfiguration.CertificateGroups.DefaultHttpsGroup.TrustList.Open
= NodeId 14095`) that the SDK and `ConfigurationNodeManager` bind
against directly. Optional Variable/Method children of those Objects are
still gated transitively (e.g. `OperationLimits.MaxNodesPerRead` is
suppressed even though `OperationLimits` itself stays emitted).

### EURange regression analysis

The earlier attempt at this fix regressed `AnalogItemType.EURange`
(which is `Mandatory` on `AnalogItemType` per the OPC UA spec — a
type-level promotion of `BaseAnalogType.EURange`).
`IsUnderSingletonInstance` is `false` on type-def and synthetic
type-instance roots, so the gate cannot fire on either type-template
tree. Combined with the topLevelInstanceException preserved for type-def
roots, every type-def factory continues to emit its full child set.

## SDK programmatic Add* (Opc.Ua.Server)

`DiagnosticsNodeManager.AddSdkImplementedOptionalChildren` now
dispatches per loaded predefined node type. Each lazy-add call patches
the resulting NodeId (and Method `InputArguments` / `OutputArguments`
NodeIds where applicable) to the well-known singleton-instance value via
`ObjectIds.*` / `MethodIds.*` / `VariableIds.*`. The generator-emitted
`Add{Child}` extension uses the type-level factory which assigns the
type NodeId; the SDK overrides each NodeId after construction.

- **`Server` (ServerObjectState)**: `GetMonitoredItems`, `ResendData`,
`SetSubscriptionDurable` (when enabled), `Namespaces`, `UrisVersion`,
`EstimatedReturnTime`, `LocalTime`.
- **`Server.ServerCapabilities`** (14 Properties): `MaxArrayLength`,
`MaxStringLength`, `MaxByteStringLength`, `MaxSessions`,
`MaxSubscriptions`, `MaxMonitoredItems`, `MaxSubscriptionsPerSession`,
`MaxMonitoredItemsPerSubscription`, `MaxSelectClauseParameters`,
`MaxWhereClauseParameters`, `MaxMonitoredItemsQueueSize`,
`ConformanceUnits`, `RoleSet`, `OperationLimits`. Required to keep
`ServerInternalData.CreateServerObjectAsync`'s non-null assertions
working and to keep `Session.FetchOperationLimitsAsync` round-trip
values matching master.
- **`Server.ServerCapabilities.OperationLimits`** (12 Properties): every
`MaxNodesPerXxx` and `MaxMonitoredItemsPerCall`.
- **`Server.ServerRedundancy.RedundantServerArray`**: re-added for
`DefaultServerRedundancyHandler` (client-side) compatibility.
- **`HistoryServerCapabilities.ServerTimestampSupported`**.
- **6 modifiable well-known roles** (Observer, Operator, Engineer,
Supervisor, ConfigureAdmin, SecurityAdmin) × 11 RoleType children
(`ApplicationsExclude`, `Applications`, `EndpointsExclude`, `Endpoints`,
`CustomConfiguration`, `AddIdentity`, `RemoveIdentity`,
`AddApplication`, `RemoveApplication`, `AddEndpoint`, `RemoveEndpoint`).
The three immutable roles (Anonymous, AuthenticatedUser,
TrustedApplication) have no well-known instance NodeIds for these
methods in the spec and are left untouched.
- **`OPCUANamespaceMetadata`** (i=15957, standard Part 6 namespace
metadata loaded from `Opc.Ua.NodeSet2.xml`): `DefaultRolePermissions`,
`DefaultUserRolePermissions`, `DefaultAccessRestrictions`.
- **Part 17 `Aliases`** (i=23470): `LastChange`. Required by
`AliasNameResolverRefreshMode.AutoOnLastChangeMonitoredItem` in
`Opc.Ua.Client` which subscribes to LastChange to invalidate its cache.

`ConfigurationNodeManager` continues to assign
`CreateSelfSignedCertificate = new
CreateSelfSignedCertificateMethodState(activeNode)` before `Create(...)`
(it is no longer emitted by the singleton factory).

## Related Issues

- Fixes #3768
- Follow-up to #3799

## Verification

- **Generated code inspection**: `CreateServerConfiguration` does
**not** emit `ApplicationUri` / `ProductUri` / `ApplicationType` /
`HasSecureElement` / `CancelChanges` / `ResetToServerDefaults` at
`forInstance=true`. `CreateServer_ServerCapabilities` no longer emits
the 14 SDK-handled Optional Properties at `forInstance=true`; the SDK
re-adds them with the correct instance NodeIds.
`CreateServerConfiguration_CertificateGroups` still emits
`DefaultHttpsGroup` and `DefaultUserTokenGroup` (Object exemption)
preserving the subtree NodeIds that `ConfigurationNodeManager` binds
against. `CreateNamespaceMetadataType` (the type-def factory) emits all
Optional Properties unconditionally so
`ConfigurationNodeManager.CreateNamespaceMetadataStateAsync` keeps
working for runtime-created metadata instances.
- **Strengthened regression test**
`ServerConfiguration_OptionalUnimplementedChildren_NotInAddressSpaceAsync`:
- Negative: 8 expected-suppressed Variable/Method NodeIds
(ServerConfiguration + PublishSubscribe) are absent.
- Positive: all SDK-added children resolve at their well-known
singleton-instance NodeIds — 14 ServerCapabilities Properties + 12
OperationLimits Properties + RedundantServerArray + 3 Server Optional
Variables + the existing Server.Namespaces / GetMonitoredItems /
ResendData / HistoryServerCapabilities.ServerTimestampSupported set.
- Regression guard:
`Server_ServerConfiguration_CertificateGroups_DefaultApplicationGroup_CertificateTypes`
resolves at its well-known NodeId, and the Optional `DefaultHttpsGroup`
/ `DefaultUserTokenGroup` roots remain emitted (Object exemption).
- WellKnownRole regression guard: one method per modifiable role
resolves at its well-known NodeId (Observer.AddIdentity,
Observer.ApplicationsExclude, Observer.AddApplication,
Operator.AddEndpoint, Engineer.RemoveIdentity,
Supervisor.RemoveApplication, ConfigureAdmin.RemoveEndpoint,
SecurityAdmin.AddIdentity).
- `Opc.Ua.Server.Tests`: **1683 / 1683** pass on net10 (5 pre-existing
skips).
- `Opc.Ua.Core.Security.Tests`: **547 / 547** pass on net10 (68
pre-existing skips). Includes the RoleManagementTests,
SecurityRoleServerBase2Tests, and SecurityRoleServerDefaultPermsTests
fixtures.
- `Opc.Ua.Features.Tests`: **128 / 128** pass on net10 (22 pre-existing
skips). Includes the RoleManagementClient and
MonitoredItemAliasNameRefreshStrategySignAndEncryptTests fixtures.
- `Opc.Ua.InformationModel.Tests`: **1052 / 1052** pass on net10 (24
pre-existing skips).
- `Opc.Ua.Gds.Tests`: **875 / 875** pass on net10 (29 pre-existing
skips).
- `Opc.Ua.SourceGeneration.Core.Tests`: **3514 / 3514** pass on net10 (8
long-running compile-cycle integration tests skipped by the framework).
- No `AnalogItemType.EURange` regression.

## 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.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-12 15:13:41 +02:00
Marc Schier 59374fc81d Generate MatrixOf<T> properties for OneOrMoreDimension types (#3625) (#3868)
# Description

Closes the original PR #3619 TODO and extends the `MatrixOf<T>` opt-in
to every place the source generator previously fell back to `Variant`
for `ValueRank="OneOrMoreDimensions"`.

## Generator changes (`Tools/Opc.Ua.SourceGeneration.Core`)

### Site 1 — DataType structure fields (`DataTypeGenerator.cs`)

Fields declared with matrix rank now render as
`global::Opc.Ua.MatrixOf<T>` instead of `Variant`, mirroring the
existing `ArrayOf<T>` treatment for `Array` rank. Encode/decode is
routed per the field's `DataType`:

| Field `DataType` | Property | Encode | Decode |
| ------------------------------------------------- |
--------------------------------------- |
----------------------------------------------------------- |
-----------------------------------------------------------------------
|
| primitive (`Boolean`, `Int32`, `String`, …) | `MatrixOf<bool>` etc. |
`WriteVariant(name, Variant.From(field));` |
`ReadVariant(name).GetBooleanMatrix();` (etc.) |
| `Structure` / abstract structure | `MatrixOf<ExtensionObject>` |
`WriteVariant(name, Variant.From(field));` |
`ReadVariant(name).GetExtensionObjectMatrix();` |
| concrete `IEncodeable` (e.g. `Vector`) | `MatrixOf<Vector>` |
`WriteEncodeableMatrix(name, field);` |
`ReadEncodeableMatrix<Vector>(name);` |
| typed enum | `MatrixOf<MyEnum>` | `WriteVariant(name,
Variant.From(field));` |
`ReadVariant(name).GetEnumerationMatrix<MyEnum>();` |
| `BaseDataType` / `Number` / `Integer` / `UInteger`|
`MatrixOf<Variant>` | `WriteVariant(name, Variant.From(field));` |
`ReadVariant(name).GetVariantMatrix();` |

### Site 2 — VariableType State classes (`NodeStateGenerator.cs`)

Replaces the original `// TODO: matrixOf;` from #3619. Concrete matrix
variable types now inherit the typed `<MatrixOf<T>>` chain. Standard
model:

- `XYArrayItemState :
ArrayItemState<MatrixOf<XVType>>.Implementation<StructureBuilder<XVType>>`

Abstract bases (`ArrayItemType`, `CubeItemType`, `ImageItemType`,
`NDimensionArrayItemType` — all `DataType="BaseDataType"`) keep the
existing `<T>` early-return.

### Site 3 — Property / Variable instances
(`ModelDesignExtensions.GetNodeStateClassName`)

Adds a `OneOrMoreDimensions` branch after the existing `Array` branch
and refines `IsTemplateParameterRequired` / `IsIndeterminateType` so
matrix-rank concrete types are no longer treated as indeterminate.
Standard model:

- `EnumDictionaryEntries: PropertyState<MatrixOf<NodeId>>`
- `FailureSystemIdentifier: BaseDataVariableState<MatrixOf<byte>>`

### Site 4 — Method parameters (`ClientApiGenerator` /
`ServerApiGenerator`)

`CollectParameters` opts in to `useMatrixTypeInsteadOfVariant` via
`SupportsMatrixOf()`. Zero standard-service impact (no OPC UA service
declares matrix args).

## Encoder / decoder API

Adds the parameterless overload `MatrixOf<T>
ReadEncodeableMatrix<T>(string? fieldName) where T : IEncodeable, new()`
to `IDecoder` and all implementations (`BinaryDecoder`, `JsonDecoder`,
`XmlDecoder`, `XmlParser`, `PubSubJsonDecoder`), mirroring the existing
`ReadEncodeableArray<T>(string?)` shape so the generator does not need a
runtime encoding-id lookup.

## Consumer migration

-
`Applications/Quickstarts.Servers/ReferenceServer/ReferenceNodeManager.cs`
— `CreateXYArrayItemVariable` updated from
`Variant.FromStructure(ArrayOf.Wrapped(...))` to `new XVType[] { …
}.ToMatrixOf(5)` to match the new typed `XYArrayItemState.Value`
surface.

## Tests added

- **Generator snippet tests**:
`GenerateMatrixValueDataType_EmitsMatrixOfPropertiesAndCalls` (custom
data-type matrix fields) and
`NodeStateGeneratorEmitsMatrixOfTemplateParameterForStandardModel`
(standard-model VariableType + instance types).
- **Encoder round-trip**: new `EncodeableMatrixRoundTripTests` exercises
`WriteEncodeableMatrix` ↔ `ReadEncodeableMatrix<T>(string?)` through
Binary, XML, and parameterless overloads with null-matrix and populated
payloads.
- **Extension tests**: parameterised
`GetDotNetTypeName_OneOrMoreDimensionsValueRankWithBasicDataTypes_ReturnsCorrectMatrixOfType`
and `IsTemplateParameterRequired_OneOrMoreDimensionsWith…` tables;
`SupportsMatrixOf` coverage including `DiagnosticInfo → false` and
`ArgumentNullException` for null receiver.
- **Test resource models** (`TestDataDesign.xml`, `DemoModel.xml`,
`TestModel.xml`): new `MatrixValueDataType` /
`SampleStructureWithMatrixFields` exercise every branch end-to-end
through the existing `GenerateAndCompileDesignFileAsync` integration
test.

## Documentation

`Docs/MigrationGuide.md` — new sub-section "Generated data type fields
with ValueRank=OneOrMoreDimensions" with a per-`BasicDataType` mapping
table, the `IDecoder.ReadEncodeableMatrix<T>(string?)` overload note for
custom decoder implementations, and a sibling block covering
VariableType State classes, property-state instances, and service
parameters with before/after code samples.

## Verification

| Suite (net10.0) | Result |
| --------------- | ------ |
| `Opc.Ua.Types.Tests` | 7553 passed |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3623 passed, 8 skipped
(analyzer-conditional) |
| `Opc.Ua.SourceGeneration.Stack.Tests` | 90 passed |
| `Opc.Ua.SourceGeneration.Tests` | 40 passed |
| `Opc.Ua.Server.Tests` (focused on `ReferenceServer`/`ArrayItem`) | 37
passed |
| Full `UA.slnx` build | 0 errors (42 pre-existing unrelated CA2007
warnings) |

The `// TODO: matrixOf;` comment is gone from `NodeStateGenerator.cs`.

## Related Issues

- Fixes #3625

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-12 12:45:39 +02:00
Marc Schier 4e6c3303dd Add V2 subscription triggering (SetTriggering) with N:M support and recreate replay (#3854)
# Description

Adds **triggered-item (SetTriggering) support to the V2 (modern)
subscription engine** with a hybrid API, batched apply pipeline, and
automatic replay on recreate/reconnect — closing the gap from #3834 and
adding store/load round-trip for triggering relationships.

## Public API

| Concern | Surface |
|---|---|
| Declarative | `MonitoredItemOptions.TriggeredByNames`
(`IReadOnlyList<string>`) — set at item-add time |
| Imperative | `ISubscription.SetTriggeringAsync(IMonitoredItem
triggeringItem, IReadOnlyCollection<IMonitoredItem>? linksToAdd,
IReadOnlyCollection<IMonitoredItem>? linksToRemove, CancellationToken
ct)` returning `SetTriggeringResult` with per-link statuses |
| Fluent helpers | name-based overloads `SetTriggeringAsync(this
ISubscription, string triggeringItemName, params string[]
triggeredItemNames)` (and `add`/`remove`/`ct` variant) |
| N:M navigation | `IMonitoredItem.TriggeringItems` (items that trigger
this) and `IMonitoredItem.TriggeredItems` (items I trigger), both
on-demand projections |
| Result type | `public sealed record SetTriggeringResult(IMonitoredItem
TriggeringItem, IReadOnlyList<(IMonitoredItem Item, StatusCode Status)>
AddResults, IReadOnlyList<(IMonitoredItem Item, StatusCode Status)>
RemoveResults, StatusCode ServiceResult)` |

No client handles surface on the public API. Validation in the
imperative path is by **reference identity** under the manager lock —
items from a different subscription with a matching name cannot sneak
through, and a removed-then-re-added item with the same name does not
silently re-target.

## Engine — batched apply

A new manager-level operations queue + a batched apply step in
`MonitoredItemManager.ApplyMonitoredItemChangesAsync`:

1. After Create / Modify / SetMonitoringMode, drain the operations
queue.
2. Group by triggering item (reference identity).
3. Re-queue ops whose triggering item is not yet `Created` (bounded by
`MaxTriggeringRetryCount`; permanently failed items propagate the
create-failure status to the operation TCS).
4. Fold per-edge across operations targeting the same triggering item —
last-intent wins; same-edge add+remove resolves to one net operation.
5. Pre-resolve: triggered items with `Created=false` resolve to
`Bad_MonitoredItemIdInvalid` (adds) or `Good` no-op (removes, per Part 4
§5.13.1.6 server auto-cleanup).
6. Issue **one `SetTriggering` RPC per triggering item** carrying both
`linksToAdd` and `linksToRemove` (the server processes remove before add
per §5.13.5.2 — atomic swap semantics).
7. Map per-link results back to contributing operations; complete each
TCS with its own `SetTriggeringResult` subset.

Imperative writes mutate the per-item runtime `DesiredTriggeredByNames`
synchronously so navigation (`TriggeringItems`/`TriggeredItems`) and
`Snapshot()` reflect intent before the RPC fires. Per-link failures roll
back the optimistic mutation so the client view matches server reality.

## State ownership

`MonitoredItemOptions.TriggeredByNames` is the **initial declarative
input**. The canonical runtime source of truth is a per-item
`DesiredTriggeredByNames` field, initialized from options and mutated by
both the imperative API and subsequent options changes. The field is
preserved across `Reset` (recreate path), which is what enables
automatic SetTriggering replay after recreate.

## Save / load (closes #3834)

- `MonitoredItemStateSnapshot.TriggeredByNames` (`ArrayOf<string>`)
replaces the old `TriggeringItemClientHandle` field (V2-only — not yet
shipped, so a clean break; classic snapshot format unchanged).
- `Snapshot()` reads from runtime `DesiredTriggeredByNames`;
`ApplyLoadState` writes loaded names into both options and runtime state
without leaking through the OnChange tracker.
- **TransferSubscriptions** success path: only the local view is
restored — trusting the spec (`§5.13.5`) that servers preserve
triggering links across subscription transfer. No SetTriggering RPC is
issued.
- **Recreate-fallback** (transfer rejected), **in-session
`RecreateAsync`**, and **Reconnect** paths: `Reset` preserves
`DesiredTriggeredByNames` and enqueues an add-delta per preserved
triggering name; the batched apply pass replays SetTriggering after
items finish (re)creating.

## Error handling

Per OPC UA Part 4 §5.13.5.3/§5.13.5.4 (Tables 73/74):

| Condition | Engine response |
|---|---|
| Per-link `Bad_MonitoredItemIdInvalid` | Surfaced in
`SetTriggeringResult.AddResults`/`RemoveResults`; optimistic
`DesiredTriggeredByNames` mutation rolled back |
| Service-level `Bad_SubscriptionIdInvalid` | Operations re-queued, not
terminal — replays after the subscription state machine recreates |
| Service-level `Bad_TooManyOperations` | Surfaced as
`ServiceResultException` to all contributing TCSs (no auto-chunking in
this version) |
| Triggering item never reaches `Created` | After
`MaxTriggeringRetryCount` re-queues, op completes with the triggering
item's propagated error (no infinite loop) |
| Communication errors | Routed through the existing retry policy with
bounded retry count |

## Spec consultation

Cross-checked against **OPC UA Part 4 v1.05.07 §5.13.5** (verified Table
73 / Table 74 directly via the OPC Foundation spec site) and §5.13.1.6
(deletion side-effects, monitoring-mode interactions). The only per-link
spec-specific status code is `Bad_MonitoredItemIdInvalid`;
`linksToRemove` is processed server-side before `linksToAdd` (driving
the single-RPC-per-trigger batch shape).

## Tests

- **9 new unit tests** in `MonitoredItemManagerTriggeringTests.cs`
(single op, multi-op merge into one RPC, per-trig grouping,
requeue-when-not-Created, per-link rollback, `Bad_SubscriptionIdInvalid`
recovery, deletion races, last-intent same-edge conflict resolution).
- **Updated** integration tests `SetTriggeringTrackingV2Async` and
`SnapshotCapturesTriggeringForReplayAsync` to use the new API.
- **2 new integration tests** in `SubscriptionTests.cs`:
`DeclarativeTriggeredByNamesV2Async` (options-driven path end-to-end)
and `NToMTriggeringV2Async` (one triggered item linked to two trigger
items, per-link removal).
- **AOT smoke test** in `SubscriptionAotTests.cs` for the
`MonitoredItemStateSnapshot.TriggeredByNames` binary encoder/decoder
round-trip.
- All **344 V2 subscription/monitored-item unit tests** and **71 V2
subscription integration tests** pass locally (net10).

## Documentation

- New: `Docs/Subscriptions.md` — *Subscriptions and Monitored Items
Service Set*. Merges the previous `StreamingSubscription.md` and
`SubscriptionTriggering.md` into a single guide; leads with a
Classic-vs-V2 engine overview (recommending V2 for new code) and links
to `Sessions.md` §4 for the full engine comparison. Covers the V2
callback API, declarative + imperative SetTriggering with N:M,
save/load/restore behavior, error handling, and the
`IStreamingSubscription` async-enumerable surface.
- Updated: `Docs/README.md`, `Docs/AlarmsAndConditions.md`,
`Docs/StateMachines.md`, `Docs/Profiles.md` — all point to the new
merged doc.
- Updated: `plans/26-v2-subscription-parity.md` — rows 49
(`SetTriggeringAsync`) and 111 (`TriggeringItems` plural /
`TriggeredItems`).

## Related Issues

- Fixes #3834 — `TransferSubscription does not automatically
restore/retain links between trigger and monitored items`. The fix is
two-pronged: (a) transfer preserves links server-side per spec and the
client snapshot captures `TriggeredByNames` so the local view restores
automatically; (b) when transfer fails and the engine falls back to
recreate (or when `RecreateAsync` is invoked explicitly), `Reset`
enqueues per-triggering-name add-deltas from the preserved runtime state
and the batched apply pass replays `SetTriggering` after re-creation.

## 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.
2026-06-12 10:38:41 +02:00
Marc Schier 4d613d11dc CI: migrate to netstandard Managed DevOps Pool + per-TFM build matrix (#3866)
# Description

End-to-end migration of the Azure DevOps CI pipelines to the
`netstandard` Managed DevOps Pool with the build matrix re-shaped to fit
the new agents and a handful of test/project fixes that fell out of the
migration.

**Pipeline routing & defaults**

- `azure-pipelines.yml` and `azure-pipelines-preview.yml` default
`managedPool=true`, routing Windows/Linux jobs to the `netstandard`
Managed DevOps Pool with `ImageOverride` demands; flipping the
queue-time boolean to `false` falls back to Microsoft-hosted agents.
- Per-platform images come from the root yml (single source of truth):
- Managed pool: `windows-2022-g2` / `ubuntu-22.04-g2` (Mac stays hosted:
`macOS-15`).
  - Hosted: `windows-2025-vs2026` / `ubuntu-24.04` / `macOS-15`.
- `.azurepipelines/test.yml` and `test-aot.yml` declare `hosted` as a
typed `boolean` parameter so `not(parameters.hosted)` actually evaluates
correctly (an untyped value left it as the string `'false'`, which is
truthy and silently routed managed-pool jobs to Microsoft-hosted
`vmImage` demands).
- macOS test jobs always run on the Microsoft-hosted `macOS-15` image
regardless of `managedPool`; the four per-framework test stages queue a
dedicated `hosted: true` Mac invocation unconditionally and the main
matrix carries only Windows+Linux.
- Test-template prep jobs (which only run a PowerShell matrix script)
run on the managed pool whenever a `poolName` is set, even when the
matrix `hosted: true`, so the Microsoft-hosted parallel-job allotment
isn't blocked.

**Build stage — per-(TFM × configuration) matrix**

- `.azurepipelines/get-matrix.ps1` gained a `-Tfms` parameter that fans
the matrix across target frameworks alongside the existing agent /
configuration dimensions and emits a per-entry `targetTfm`.
- `.azurepipelines/sln.yml` accepts a `tfms` parameter and threads
`$(targetTfm)` into both `dotnet restore` and `dotnet build` as
`/p:CustomTestTarget=$(targetTfm)` so each matrix job builds a single
TFM and fits on the Managed DevOps Pool's small (~16 GB) temp disk.
- `azure-pipelines.yml` `build` stage passes `tfms: 'net10.0,net48'` for
PR builds and the full
`net472,net48,netstandard2.0,netstandard2.1,net8.0,net9.0,net10.0` set
for master / scheduled / manual / fork-PR runs.
- `sln.yml`'s restore step uses `command: custom` (`custom: restore`)
because `command: restore` silently drops the `arguments` input —
without it `/p:CustomTestTarget` never reached restore and
`obj/project.assets.json` was populated with the wrong TFM set.
- `sln.yml` no longer uses the `MSBuild@1` task; it now uses
`DotNetCoreCLI@2` with `dotnet build` so the .NET 10 SDK's bundled
MSBuild (which understands `.slnx`) is used instead of Visual Studio
2022's MSBuild on the managed-pool image.

**Project-level fixes that fell out of the matrix split**

- `Applications/McpServer`, `Applications/MinimalBoilerServer`,
`Applications/MinimalCalcServer`, and `Tests/Opc.Ua.Aot.Tests` each get
a `Directory.Build.targets` that turns the project into a no-op (cleared
`Compile` / `ProjectReference` / `PackageReference` / etc.,
`OutputType=Library`) when `CustomTestTarget` restricts the matrix to a
legacy TFM (`net4*` / `netstandard2.*`) the project can't target. Target
overrides are deliberately avoided — same-named targets in
`Directory.Build.targets` ALWAYS replace the SDK definitions, which
would silently break `dotnet publish` / `dotnet test` in the
unrestricted GitHub Actions flows.
- `Tools/Opc.Ua.MigrationAnalyzer.csproj`'s `ProjectReference` to
`Opc.Ua.MigrationAnalyzer.Core` no longer hard-pins
`SetTargetFramework=net10.0`; the pin follows `CustomTestTarget` (and
falls back to `net10.0` when unset).

**Test reliability**

-
`Tests/Opc.Ua.PubSub.Tests/IntervalRunnerTests.FakeTimeProviderDrivesDeterministicSchedulingAsync`
no longer calls a single `fake.Advance(300)` expecting three timers to
fire — it advances one 100 ms interval at a time and waits for each fire
(the runner only registers the next `Delay` after the previous callback
resumes on the thread pool; on slower CI agents the race surfaces
consistently).
- `Tests/Opc.Ua.Server.Tests/Fluent/SimulationBuilderExtensionsTests`
(four "expect ≥ N ticks in 150 ms" tests) replace fixed
`Task.Delay`/`Thread.Sleep` waits with a `WaitForAsync` polling helper,
eliminating thread-pool cold-start flakiness on slow agents (observed on
the hosted `macOS-15` runner).

## Related Issues

_No tracking issue — pipeline tuning to standardize on the netstandard
Managed DevOps Pool, plus the test/project follow-ups required for the
matrix to be green on that pool._

## 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. _(N/A — pipeline +
test-reliability fixes; the two test fixes themselves were verified to
pass 5× in stress runs locally.)_
- [x] I have added all necessary documentation. _(Comments inline in the
new `Directory.Build.targets`, `sln.yml`, and `azure-pipelines.yml`
explain the unusual MSBuild/`DotNetCoreCLI@2` interactions for future
maintainers.)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(Verified
`dotnet build UA.slnx -p:CustomTestTarget=net10.0 -c Release` and
`-p:CustomTestTarget=net48 -c Debug` both complete with 0 errors;
targeted test runs for the two modified test files pass 5/5.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 17:19:41 +02:00
Marc Schierandagent a81d981755 dotnet format sweep (whitespace, IDE, RCS) + code-style enforcer agent fix (#3865)
# Description

Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).

Six commits, applied bottom-up in safety order so each batch leaves the
build green:

1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
   197 hits across ~30 files; pure mechanical cleanup.

2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.

3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
   Roslynator formatting (RCS0xxx) blank-line and punctuation rules.

4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.

5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.

6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
   - Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.

### What this PR does **not** touch

- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.

## Related Issues

_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._

## 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. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] 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: agent <agent@local>
2026-06-10 08:35:44 +02:00
Marc Schier e7d9a153b9 [Test] Unit tests for ManagedSession revalidation loop (follow-up to #3849) (#3864)
# Description

Follow-up unit tests for the debounced server-certificate revalidation
loop added in PR #3849 (now merged as `15eb55c46`). Codecov flagged
`ManagedSession.CertificateChanges.cs` as the lowest-coverage file in
that PR (34% patch coverage) — the loop body that drives the debounce /
at-most-one-in-flight / re-arm semantics is the heart of the trust/CRL
revalidation behaviour and was previously untested.

## Changes

*
**`Libraries/Opc.Ua.Client/Session/ManagedSession.CertificateChanges.cs`**
— extract `RunRevalidationLoopAsync` into a pure `internal static`
overload that takes a `ChannelReader<int>`, `TimeProvider`, debounce
window, validate callback, logger, and cancellation token. The instance
overload now forwards to it. This gives unit tests a seam without
standing up a connected `ManagedSession` (whose private constructor +
`CreateAsync` triggers a real connect).

*
**`Tests/Opc.Ua.Client.Tests/Session/ManagedSessionRevalidationLoopTests.cs`
(new)** — 5 unit tests covering the loop body in isolation:
- `BurstOfSignalsCollapsesToSingleValidationAsync` — 50 signals →
exactly 1 validation (bounded-1 channel + DropWrite).
- `SignalDuringInflightValidationReArmsExactlyOnceAsync` — re-arm
semantics during in-flight validation.
- `NoSignalMeansNoValidationAsync` — idle loop never invokes the
validator.
- `CancellationDuringDebounceAbortsPendingValidationAsync` —
cancellation cleanly aborts a pending validation.
- `ValidationExceptionsAreSwallowedAndLoopContinuesAsync` — transient
validation failures don't break the loop.

Uses real `TimeProvider.System` with a 50 ms debounce + 250 ms settle
window so each test runs in well under a second. A `FakeTimeProvider`
seam was considered but rejected — the loop's `ReadAsync → Delay →
drain` sequence is racy to drive with a fake clock because the timer is
not created until after the first signal has been consumed.

## Related Issues

- Follow-up to #3562 / #3160 (both fixed in #3849, merged).

## 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. _(None needed — pure
test addition + non-public testing seam.)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran focused tests locally using the **UA.slnx** projects against
.net **10**, and all 5 new tests passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 08:35:08 +02:00
Marc Schier b2b4dbb13f [Server] Part 13 Aggregates compliance fixes, full test coverage and docs (#3863)
# Description

Brings the server-side **OPC UA Part 13 (Aggregates) v1.05.07** support
to full conformance: fixes the three genuine non-compliances found by a
spec review, completes value-validated test coverage for **all 37
standard aggregate functions**, and adds documentation. The existing
aggregate engine is a faithful port and was already largely compliant;
this PR closes the remaining gaps.

## Compliance fixes

- **`TreatUncertainAsBad` default (§4.2.1.2).** The server-default
`AggregateConfiguration` (used when a `ReadProcessedDetails` request
sets `UseServerCapabilitiesDefaults = true`) now defaults
`TreatUncertainAsBad` to **`True`** as the spec mandates, in both
`AggregateManager.GetDefaultConfiguration` and the `HistorianDispatcher`
fallback. Behavior change noted in `MigrationGuide.md`.
- **`AnnotationCount` (§5.4.3.20).** Previously counted raw data values.
It now counts **Annotations** per interval, computed in
`HistorianDispatcher.DispatchProcessedReadAsync` from
`IHistorianAnnotationProvider` (endTime-exclusive bucketing;
reverse-time and `ProcessingInterval == 0` handled). Returns
`Bad_AggregateNotSupported` when the provider has no annotation history.
- **StandardDeviation/Variance use all n Good raw values
(§5.4.3.37/.39).** `ComputeStdDev` previously computed over the N−1
sub-regions returned by `GetRegionsInValueSet`, silently dropping the
last raw value. It now computes the mean/variance directly over all Good
raw values in the interval (UseBounds = None), differing only by the
divisor (sample n−1, population n). The population spec-example tests
that used to add a duplicate trailing value as a workaround were updated
to the natural 5-value datasets matching §A.35/.37.

## Tests

- **Calculator coverage is complete for all 37 aggregates** — added
`StatusAggregateCalculatorTests` (DurationGood/Bad, PercentGood/Bad,
WorstQuality/WorstQuality2), the Min/Max `*2` simple-bound variants
(`Minimum2`/`Maximum2`/`Range2`/`MinimumActualTime2`/`MaximumActualTime2`),
and `Interpolative`.
- **End-to-end integration** in `HistoryClientIntegrationTests` over the
live in-process ReferenceServer: value-validated
`Average`/`Minimum`/`Maximum` processed reads and an **AnnotationCount**
write→read round-trip.
- **Dispatcher-level** `AnnotationCount` tests (counts per interval;
`Bad_AggregateNotSupported` without an annotation provider).
- **Tightened `AggregateBaseTests`**: dropped `allowAnyResult` from
`StandardDeviationSample`/`StandardDeviationPopulation`/`NumberOfTransitions`
basecases (now deterministic on seeded data after the D3 fix).
- **Validation tests** for `AggregateConfiguration` per §4.2.1.2:
`PercentDataBad`/`PercentDataGood > 100` and the invalid relationship
`PercentDataGood < (100 - PercentDataBad)`.
- **Cross-cutting**: reversed time-range test (start > end).

## Verification

- Builds: **0 warnings / 0 errors**.
- Aggregate + historian dispatcher + History integration suites green on
**.NET 10** and **.NET Framework 4.8** (cross-TFM).
- Black-box (MCP) check against a standalone `ConsoleReferenceServer`:
both `Server.ServerCapabilities.AggregateFunctions` and
`HistoryServerCapabilities.AggregateFunctions` advertise all 37
functions (§4.2.2.1).

## Notes / out of scope

- AnnotationCount advertisement remains the full standard set with
per-node `Bad_AggregateNotSupported`. This is spec-acceptable: §4.2.2.1
lists what the server *can* support; per-node refinement is the optional
`HistoricalConfiguration.AggregateFunctions`. Conditional advertisement
could be added later as a quality refinement.
- A native-vs-framework-fallback parity test would be useful once a
third-party `IHistorianProcessedProvider` implementation exists; the
bundled `InMemoryHistorianProvider` uses only the framework fallback
today.

## Related Issues

_No existing tracking issue; this addresses OPC 10000-13 (Part 13,
Aggregates) v1.05.07 conformance gaps. Maintainers: please link or open
a tracking issue if one is required._

## 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. _(Ran the
aggregate/historian/integration suites on net48 + net10; full-solution
run pending CI.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 08:34:09 +02:00
Marc Schier 15eb55c46e Cert rotation: force renegotiate (#3562) + auto-detect on client (#3160) (#3849)
# Description

Implements the OPC UA Part 12 §7.10.9 contract on the server (fixes
#3562) AND the client-side counterpart for immediate detection of
own-cert / trust-list / CRL changes (fixes #3160).

## Server side (#3562)

Per Part 12 §7.10.9 the server _shall_ "force existing SecureChannels
affected by the changes to renegotiate" once the `ApplyChanges` method
response has been delivered. Previously the server reloaded the
certificate but did not cut affected channels; clients kept using the
old cert until natural rekey, and the code carried an explicit `TODO`
next to a 1-second `Task.Delay` hack in
`ConfigurationNodeManager.ApplyChanges`.

Key server changes:

* **New optional capability interface**
`Opc.Ua.ITransportListenerCertificateRotation` (single method
`CloseChannelsForCertificate(Certificate)`) — implemented by
`TcpTransportListener` and `HttpsTransportListener`. Custom transport
listeners can opt in without breaking `ITransportListener`.
* **TCP path**: new internal
`TcpListenerChannel.TryCloseForCertificateRotation(oldThumbprint, out
globalChannelId)` matches the channel's negotiated `ServerCertificate`
by thumbprint, sends `BadCertificateInvalid`, and calls
`ChannelFaulted()` to actually close the socket.
`TcpTransportListener.CloseChannelsForCertificate` snapshots
`m_channels` outside `m_lock` to avoid lock inversion, invokes the
per-channel helper, returns cut global channel ids. **The listener
socket stays bound** — clients can reconnect immediately.
* **HTTPS path**: HTTPS owns the cert at the Kestrel/HTTP.sys listener
level, so a `Stop()`+`Start()` cycle is the only practical
force-renegotiate. Moved out of `CertificateUpdate` (which now does only
the soft fan-out) into `CloseChannelsForCertificate` so the staging
event no longer disrupts in-flight requests prematurely.
* **`ConfigurationNodeManager.ApplyChanges`** rewritten:
- Captures the per-group pre-transaction certificate (preserved across
multi-stage transactions per Part 12 §7.10.2) and the staged
`UpdateCertificateData`.
- Schedules a deferred apply: waits for the response to flush, re-syncs
the registry via `CertificateManager.UpdateAsync`, then calls
`CloseChannelsForCertificate` on every listener that implements the
capability.
- **Configurable grace period**: new
`IConfigurationNodeManager.ApplyChangesGracePeriod` (default 250 ms,
negative clamped to zero). Hosts running over higher-latency
administrative links can raise it.
- Exposes
`IConfigurationNodeManager.DrainPendingApplyChangesAsync(CancellationToken)`
for tests/hosts to deterministically await rotation.
  - Removes the explicit 1-second `Task.Delay` TODO hack.
* **`Opc.Ua.Server.ITransportListenerRegistryProvider`** — narrow opt-in
interface on `ServerInternalData` exposing the live transport listener
list. Lets `ConfigurationNodeManager` drive the fan-out without
expanding `IServerInternal` (binary compat for mocked test fixtures).
* **Sessions are NOT closed**. Per spec, the cut is on the SecureChannel
only; existing `Session.IsSecureChannelValid` + `Session.Activate`
rebind handles transfer over the new channel.

## Client side (#3160)

The client SDK previously had no automatic detection of changes to its
own certificate, trust list, or CRL — users had to call
`Session.ReloadInstanceCertificateAsync` manually and trigger a
reconnect. The spec-mandated `ApplicationCertificateUpdated`,
`TrustListUpdated`, and `CrlUpdated` enum values existed but only the
first was being emitted by `CertificateManager`.

Key client/shared changes:

* **`CertificateManager` now emits `TrustListUpdated` and `CrlUpdated`**
from `WriteTrustListAsync` (per mask) and from
`TrustListTransaction.CommitAsync` (after the atomic apply). The new
internal `NotifyTrustListChanged` also invalidates cached
peer/user/HTTPS validation cores so subsequent validations pick up the
fresh trust state.
* **`ManagedSession.AutoReconnectOnCertificateChange`** opt-in property
(default `false` for backwards compat). When `true`, `ManagedSession`
subscribes to `CertificateManager.CertificateChanges` on construction
and, on the three relevant kinds:
- Calls `Session.ReloadInstanceCertificateAsync` on
`ApplicationCertificateUpdated` so the next ActivateSession is signed
with the rotated client cert.
- Triggers a reconnect via the state machine so the new state is
honoured within milliseconds rather than at the next
`SecurityTokenLifetime` rekey.
* **`ManagedSession.ApplicationCertificateChanged`** event surfaces
every observed change regardless of the auto-reconnect policy for
diagnostics or custom rotation logic.
* Subscription is disposed by the existing `Dispose` / `DisposeAsync`
paths.

```csharp
using var managed = await ManagedSession.CreateAsync(
    configuration, endpoint, sessionFactory);
managed.AutoReconnectOnCertificateChange = true;

managed.ApplicationCertificateChanged += (sender, e) =>
{
    Console.WriteLine($"observed {e.Kind} on {e.TrustList}");
};
```

## Tests

| Suite | net10.0 | net48 |
|---|---|---|
| `TransportListenerCertificateRotationTests` (new, 6 tests) | ✅ | ✅ |
| `CertificateManagerTests` (4 new event tests) | ✅ 22/22 | ✅ 4/4 |
| `PushCertManagement` | 50/53 ✅ | 50/53 ✅ |
| `GDS PushTest` (focused subset) | 15/15 ✅ | 16/16 ✅ |
| `Opc.Ua.Server.Tests` (focused subset) | 38/38 ✅ | 38/38 ✅ |
| `GDS PushTest` (full) | 313/338 ✅ | — |

Skips are pre-existing (cert-type mismatches, optional methods) — no
regressions.

New integration test
`PushTest.ApplyChangesForcesChannelRenegotiateAsync` positively asserts
the stale channel fails with a recognized channel-invalidation status
after `ApplyChanges`, and that a fresh reconnect succeeds with the
rotated cert.

## Documentation

- `Docs/CertificateManager.md` — new sections:
- "Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9)" —
`ITransportListenerCertificateRotation` contract + custom listener
example.
- "Client-Side Auto-Detection of Certificate Changes (issue #3160)" —
`AutoReconnectOnCertificateChange` + `ApplicationCertificateChanged`
event + the three covered scenarios.

## Related Issues

- Fixes #3562
- Fixes #3160

## 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 ran focused tests locally using the **UA.slnx** projects against
.net **framework 4.8** and .net **10**, and all passed. _(Full UA.slnx
test run delegated to CI.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-08 12:19:49 +02:00
Marc SchierandCopilot bc4c30034d Fix CTT conformance issues #3719, #3720, #3723, #3841 (#3851)
# Description

Fixes four CTT (OPC UA Compliance Test Tool) conformance gaps that the
existing NUnit parity suite was silently passing while the CTT JS
scripts kept failing:

| Issue | Area | Failing CTT script |
|---|---|---|
| #3719 | Base Info Core Structure 2 | `001.js` |
| #3720 | Base Info FiniteStateMachine + StateMachine Instance |
`initialize.js` + `001.js` |
| #3723 | Security None | `007.js` (DoS, partially used channels) |
| #3841 | Alarms & Conditions | Acknowledge Test_003, Alarm Test_002,
Confirm Test_001 / Test_003 / Err_004, Enable Test_002, Non-Exclusive
Limit Test_002 |

Across all four, the underlying pattern was the same — the NUnit parity
tests were checking weaker invariants (DataType attribute instead of
encoded Variant BuiltInType; `Assert.Ignore` instead of `Assert.Fail`;
stub `GetEndpoints` instead of the real DoS scenario; method-call + Read
instead of subscribe-and-validate-event-payload). So the suite gave
false confidence on changes that would have failed CTT. Each commit
below both **strengthens the NUnit parity test** to encode the same
invariant CTT verifies, **and** fixes the server/model defect that the
strengthened test surfaces.

## Commits

### `a1db2984c` Fix #3719 (BaseInfoCoreStructure)

- **Test**: `BaseInfoServerTests.ReadConformanceUnitsAsync` now reads
the **value** of `i=24101` and asserts
`WrappedValue.TypeInfo.BuiltInType == BuiltInType.QualifiedName` AND
`ValueRank == ValueRanks.OneDimension` — not just the DataType
attribute.
- **Test**: `BaseInformationTests.BrowseRequiresMandatoryMethodsAsync`
now `Assert.Fail`s (not `Assert.Ignore`) when an expected mandatory
method is missing on `i=15443` / `i=25440`.
- **Server**: `Server_ServerCapabilities_ConformanceUnits` initialised
with `Array.Empty<QualifiedName>().ToArrayOf()` so the encoded Variant
carries QualifiedName type info even when empty.
- **Server**: Added `AddSecurityGroup` / `RemoveSecurityGroup` (on
`i=15443`) and `AddPushTarget` / `RemovePushTarget` (on `i=25440`)
method nodes with input/output argument metadata and in-memory handlers
in `Quickstarts.Servers/ReferenceServerConfigurationNodeManager`.

### `86d3041e5` Fix #3723 (Security None DoS off-by-one)

- **Server**: Correct off-by-one in
`Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs` channel eviction:
- Condition was `MaxChannelCount == channelCount` (only triggered at
exact equality); now `MaxChannelCount > 0 && channelCount >=
MaxChannelCount` with a loop that evicts idle channels until the count
is below the limit or no more idle channels remain.
- **Server**: `UsedBySession` now properly tracks active sessions and
clears on `CloseSession` (`TcpServerChannel`, `TcpListenerChannel`).
- **Note**: the NUnit reproducer for this issue (`SecurityNoneDosTests`)
stays on the `cttunit` branch (PR #3750) because it depends on the
`TestFixture.CloseSharedFixtureSessionAsync` helper that only exists in
`Tests/Opc.Ua.Conformance.Tests/`. The server fix on this branch is
sufficient to make the CTT JS script pass.

### `2c98390c9` Fix #3720 (BaseInfoStateMachine GeneratesEvent)

- **Model**: Add `GeneratesEvent` forward reference on
`StateMachineType` (i=2299) and `FiniteStateMachineType` (i=2771) →
`AuditUpdateStateEventType` (i=2315) in the design XML
(`Tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml`);
regenerated `Stack/Opc.Ua.Core/Schema/Opc.Ua.NodeSet2.xml`.
- **Server**: Remove the runtime workaround in `DiagnosticsNodeManager`
that previously injected this reference at load time — now redundant
because the reference ships in the NodeSet at compile time.
- **Test**: New
`BaseInfoStateMachineInstanceTests.StateMachineTypeHasGeneratesEventReferenceAsync`
walks the type hierarchy looking for `GeneratesEvent` with
`IncludeSubtypes=true` and asserts the target resolves to a
`BaseEventType` subtype (matches the CTT walk).
- Per OPC UA Part 16 §4.4.5 / Part 5 §6.4.2.

### `09f551449` Strengthen A&C conformance event tests (#3841)

- **Test fixture**: New `AlarmEventCollector` (476 LOC) — subscribes to
events on `i=Server` with a full A&C event filter (`Comment`, `Retain`,
transition times, all condition-state Ids); per-condition event log;
`WaitForEventAsync(conditionId, predicate, timeout)` for
synchronisation. Uses async `IAsyncDisposable` cleanup.
- **Tests rewritten** to subscribe to events and validate event payload
(`Comment.Text`, `Retain`, `TransitionTime`):
- `Acknowledge Test_003` — empty comment + locale propagates to event
payload; `Retain=false` afterwards
- `Alarm Test_002` — full `ActiveState` cycle Active → Normal with
Acknowledge + Confirm; every transition event validated
- `Confirm Test_001` — unique comment reaches event payload;
`ConfirmedState/Id=true`
- `Confirm Test_003` — Acknowledge → wait event → Confirm with empty
comment
- `Confirm Err_004` — **two concurrent sessions**; second Confirm
asserts specifically `BadConditionBranchAlreadyConfirmed` (not a generic
`BadInvalidState`)
- `Enable Test_002` — Disable + Enable via both type method and
discovered instance method; NO events during disabled period;
`TransitionTime` within call window; `Retain` flips
- `Non-Exclusive Limit Test_002` — mirror for
`NonExclusiveLimitAlarmType`
- **Server fixes** in
`Stack/Opc.Ua.Core.Types/State/AcknowledgeableConditionState.cs`,
`Quickstarts.Servers/Alarms/AlarmHolders/*`, and `AlarmNodeManager` for:
Comment propagation, Retain clearing, TransitionTime updates, and a
dedicated ack-only condition instance
(`AcknowledgeableConditionHolder`).

## Verification

- `dotnet build UA.slnx` — **0 errors**, 42 pre-existing warnings.
- `dotnet test Tests/Opc.Ua.InformationModel.Tests` filtered for the new
+ master `StateMachineGeneratesEvent` / `BaseInfoServer` /
`BaseInformation` tests — **6/6 pass**.
- `dotnet test Tests/Opc.Ua.History.Tests --filter AlarmsAndConditions`
— **141/144 pass**; the 3 occasional failures in
`DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync` pass
when re-run in isolation, consistent with the known A&C suite CI
flakiness pattern.

## Related Issues

- Fixes #3719
- Fixes #3720
- Fixes #3723
- Fixes #3841

Related: PR #3750 (`cttunit`) carries the same fixes plus the
conformance-test-project versions of the strengthened NUnit tests.
Either this PR or that one will land first; the other should be rebased
afterwards.

## Checklist

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-08 07:47:48 +02:00
Marc Schierandmarcschier 0fbc900919 Add opt-in SubscriptionRecoveryPolicy for unsolicited Good_SubscriptionTransferred (#3540) (#3847)
# Description

Adds an opt-in `SubscriptionRecoveryPolicy` for both the Classic V1 and
V2 subscription engines that lets the client recover from server-emitted
`Good_SubscriptionTransferred` notifications which arrive against a
freshly created subscription (observed against Kepware after a server
re-initialise, very likely a leak from the pre-restart session's pending
status notifications onto the new session — abetted by Kepware re-using
`SubscriptionId = 1` across generations).

Per [OPC UA Part 4
§5.14.7](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.7),
`Good_SubscriptionTransferred` is supposed to be delivered to the *old*
Session whose subscription was transferred away via
`TransferSubscriptions`. The existing V1 behaviour of stopping the
per-subscription publish loop on receipt is therefore
**spec-compliant**; the issue is that some servers deliver the
notification on a Session that is still actively using the subscription,
leaving the per-subscription dispatch silently disabled.

The fix is **opt-in** and **non-breaking**:

- New `SubscriptionRecoveryPolicy` enum (`ReportOnly` default,
`RecreateOnUnsolicitedTransfer`).
- V1 Classic `Subscription.RecoveryPolicy` property; V2
`Subscriptions.SubscriptionOptions.RecoveryPolicy` init property.
- When the policy is `RecreateOnUnsolicitedTransfer`, the SDK drops
queued acks for the dead subscription id (avoiding
`BadSubscriptionIdInvalid` ack errors on servers that re-use ids) and
recreates the subscription in place on the same Session.

## What changed

**Public API additions (non-breaking, defaults preserve current
behaviour):**
- `SubscriptionRecoveryPolicy` enum (`ReportOnly`,
`RecreateOnUnsolicitedTransfer`).
- V1 `Subscription.RecoveryPolicy` property.
- V2 `Subscriptions.SubscriptionOptions.RecoveryPolicy` init property.
- Internal `IMessageAckQueue.DropPendingForSubscription(uint)` and
protected `MessageProcessor.AckQueue` accessor.

**V1 Classic —
`Libraries/Opc.Ua.Client/Subscription/Classic/Subscription.cs`:**
- Replaces the inline `Good_SubscriptionTransferred` branch with
`HandleGoodSubscriptionTransferred`. `ReportOnly` preserves the original
behaviour (stop publish timer/worker); `RecreateOnUnsolicitedTransfer`
triggers `TriggerUnsolicitedTransferRecovery`, guarded by
`m_recreateAfterTransferInProgress` for idempotency.
- Recovery drains pending acks via the new
`ClassicSubscriptionEngine.RemoveAcknowledgementsForSubscription`
**before** resetting the subscription, then runs `DeleteSubscription()`
+ `CreateAsync()` on the same session so the publish worker restarts via
`StartKeepAliveTimer`.
- Discriminator (`Session != null && !Session.Reconnecting && Created`)
defers to the reconnect pipeline if it is already running.

**V2 — `Libraries/Opc.Ua.Client/Subscription/Subscription.cs`:**
- Completes the `OnStatusChangeNotificationAsync` TODO: now always fires
`OnPublishStateChanged(mask)` so `PublishState.Transferred` actually
reaches handlers (previously the no-op override silently swallowed the
flag).
- Under `RecreateOnUnsolicitedTransfer`, calls
`AckQueue.DropPendingForSubscription(deadId)` then existing
`ResetToRecreateAsync` on a worker task — no service calls from the
dispatch path.
- V2 `SubscriptionManager` implements `DropPendingForSubscription` via
drain-and-refill on the prioritised ack channel.

**Tests — `Tests/Opc.Ua.Client.Tests/Subscription`:**
- V1 in `Classic/SubscriptionUnitTests.cs`:
`ReportOnlyPolicyDoesNotRecreateOnUnsolicitedTransferAsync`,
`RecreatePolicyRecreatesSubscriptionOnUnsolicitedTransferAsync`,
`RecreatePolicyCollapsesConcurrentNotificationsAsync`,
`RecreatePolicyDoesNotRecreateWhileSessionReconnectingAsync`.
- V2 in `SubscriptionTests.cs`:
`GoodSubscriptionTransferredRaisesPublishStateTransferredAsync`,
`GoodSubscriptionTransferredRecreatesSubscriptionWhenPolicyEnabledAsync`,
`GoodSubscriptionTransferredCollapsesConcurrentRecreatesAsync`.
- `FakeMessageAckQueue` gained a `DroppedSubscriptions` recorder so the
V2 test can assert ack pruning ran.

**Docs — `Docs/TransferSubscription.md`:**
- New "Recovering from unsolicited `Good_SubscriptionTransferred`"
section with spec context, usage examples for both engines, and the
caveat that recovery is recreate-based — **not** lossless failover; the
server-side retransmission queue and triggering relationships tied to
the invalidated subscription id are not preserved.

## Validation

| Scope | Result |
| --- | --- |
| `Libraries/Opc.Ua.Client` build (`net10.0`, `net48`) | 0 warnings, 0
errors |
| `Tests/Opc.Ua.Client.Tests` build (`net10.0`, `net472`) | 0 warnings,
0 errors |
| New tests on `net10.0` and `net472` | 7/7 pass on each |
| Full subscription unit-test suite (`SubscriptionUnitTests` +
`SubscriptionTests` + `MessageProcessorTests` +
`SubscriptionManagerTests`) | 110/110 pass on each TFM |
| Entire `Opc.Ua.Client.Tests` suite (`net10.0`) | 1268/1268 pass |
| Classic transfer integration tests
(`Tests/Opc.Ua.Subscriptions.Classic.Tests`, `net10.0`) | 20/20 pass |
| V2 transfer compliance tests (`Tests/Opc.Ua.Subscriptions.Tests`,
`net10.0`) | 9/9 pass |

> Marked as **draft** because the maintainer wanted to first confirm
whether the stack is spec-compliant before deciding which knobs to ship;
this PR documents the investigation (stack is compliant), and surfaces
an opt-in recovery policy that addresses the field symptom without
changing the default behaviour. Convert to ready-for-review once the
design intent is acknowledged.

## Related Issues

- Fixes #3540

## Checklist

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

---------

Co-authored-by: marcschier <marc.schier@github.com>
2026-06-07 17:38:58 +02:00
Suciu Mircea Adrian f69ff78702 Use request.SessionId instead of request.Session.Id in the ReportAuditCancelEvent call from CancelRequests (#3850)
Use request.SessionId instead of request.Session.Id in the
ReportAuditCancelEvent call from CancelRequests to avoid accessing
Session in case ActivateSession

# Description

Cancel service is returning BadUnexpectedError because audit reporting
crashes on request.Session.Id in case the ActivateSession service call
is in-flight and its cancelation is requested becasue Session is null.

- Fixes #3848 

## Checklist

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

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] 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.
2026-06-05 21:27:15 +03:00
Marc Schier 2966f693b2 2.0 Migration Analyzer and codefixer with Agent update (#3820)
Introduces `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` — a
single NuGet package that ships **three Roslyn components + a runtime
shim** to make the 1.5.378 → 2.0 upgrade as close to "install package,
run `dotnet format`, remove package" as possible:

| DLL | Where | Purpose |
|-----|-------|---------|
| `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | 19 Roslyn
analyzers (`UA0001`–`UA0022`) covering every breaking-change pattern in
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md).
Built against the stable Roslyn 4.14 analyzer API — loads cleanly in
csc.exe (no `Microsoft.CodeAnalysis.Workspaces` reference). |
| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | 14
companion `CodeFixProvider`s; loaded only by Workspaces-aware hosts
(Visual Studio, `dotnet format analyzers`). |
| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` |
Roslyn `IIncrementalGenerator` that emits per-consumer `internal sealed
[Obsolete] class <Name>Collection : List<TElement>` shims for every
`<Type>Collection` wrapper the consumer references — including
**arbitrary model-compiled `<UserType>Collection` patterns**, not just
the 30 well-known built-in ones. |
| `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib/<tfm>/` for `net472, net48,
netstandard2.1, net8.0, net9.0, net10.0` | 1.5.378 → 2.0 runtime
compatibility shim. Re-exposes the obsolete extension surface so
1.5.378-style call sites keep compiling against 2.0 with warnings
instead of errors. |

### Migration workflow the package enables

1. **Bump every `OPCFoundation.NetStandard.Opc.Ua.*` PackageReference to
`2.0.*-*`.**
2. **Add the migration package** as a build-only dependency:
   ```xml
<PackageReference
Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer"
                     Version="2.0.*-*"
                     PrivateAssets="all" />
   ```
3. **Run `dotnet build`.** The Core shim DLL re-supplies obsolete
extension members; the source generator covers `<Type>Collection`
references; what remains are `[Obsolete]` warnings + `UA00xx` analyzer
diagnostics + (occasionally) `MIG01` for unresolvable model-compiled
element types.
4. **Apply auto-fixes** in the IDE or from the command line:
   ```bash
   dotnet format analyzers <solution>.sln \
       --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \
                     UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \
       --severity warn
   ```
5. **Walk the residual** `UA0001` / `UA0011` / `UA0015` / `UA0018` /
`UA0021` diagnostics manually — they cover patterns that require human
judgement.
6. **Remove the migration package.** You are now on clean 2.0 with no
shim dependency.

### Source generator (`Opc.Ua.MigrationAnalyzer.Generator.dll`)

Per-consumer Roslyn `IIncrementalGenerator` that emits `.g.cs` files for
every `<Name>Collection` reference in the consumer's compilation that
fails to bind:

- **Catalog override (30 entries)** — pins element types that renamed
across the 1.5.378 → 2.0 boundary: `DateTime`→`DateTimeUtc`,
`Guid`→`Uuid`, `byte[]`→`ByteString`, `XmlElement`→`Opc.Ua.XmlElement`,
plus the primitives and built-in opc types whose element name didn't
rename.
- **Semantic lookup** — for model-compiled `<UserType>Collection`
patterns the model compiler historically emitted for any user-defined
complex type. Strips the `Collection` suffix, runs
`Compilation.GetSymbolsWithName` against the unique element short name,
emits the shim with the fully-qualified type reference.
- **`MIG01` diagnostic** — fires when the element type can't be uniquely
resolved (zero or multiple candidates). User adds the missing `using` or
migrates the site manually.

Each generated file emits a class shaped exactly like the 1.5.378
wrapper:

```csharp
[global::System.Obsolete("'Int32Collection' was removed in 2.0. Use 'List<int>' or 'ArrayOf<int>' instead. (UA0002)")]
internal sealed class Int32Collection : global::System.Collections.Generic.List<int>
{
    public Int32Collection() { }
    public Int32Collection(int capacity) : base(capacity) { }
    public Int32Collection(global::System.Collections.Generic.IEnumerable<int> collection) : base(collection) { }
    public static implicit operator global::Opc.Ua.ArrayOf<int>(Int32Collection? value)
        => value is null ? default : value.ToArrayOf();
}
```

**Design choice — `internal` (not `public`):** the generated shim never
leaks through the consumer's public API surface. Consumers with *public*
methods or properties returning a `<Type>Collection` will hit `CS0050:
Inconsistent accessibility` — the intended signal that the public
surface must migrate to `List<T>` / `ArrayOf<T>` first; internal call
sites keep compiling under the shim so iteration is incremental.

### 19-rule analyzer reference

| Rule | Replaces | Default | Auto-fix |
|------|----------|---------|----------|
| `UA0001` | `Utils.Trace` / `Utils.LogX` | Info | No (telemetry
plumbing requires manual review) |
| `UA0002` | Removed `<Type>Collection` wrappers | Warning | Yes (→
`List<T>` / `ArrayOf<T>`); the source generator keeps the call site
compiling in the meantime |
| `UA0003` | `x == null` on now-struct built-ins | Warning | Yes (→
`x.IsNull`) |
| `UA0004` | `?.` on now-struct built-ins | Warning | Yes (drop the `?`)
|
| `UA0005` | `byte[]` where `ByteString` is expected | Warning | Yes
(append `.ToByteString()`) |
| `UA0006` | `new Variant(object\|DateTime\|Guid\|byte[])` | Warning |
Yes (→ `Variant.From(...)`) |
| `UA0007` | `new NodeId(string)` / `new ExpandedNodeId(string)` |
Warning | Yes (→ `Parse`) |
| `UA0008` | `Session.Call(..., params object[])` | Warning | Yes (wrap
with `Variant.From`) |
| `UA0009` | `[DataContract]` / `[DataMember]` on config extensions |
Warning | Yes (→ `[DataType]` / `[DataTypeField]`) |
| `UA0010` | `using` / `Dispose` on `CertificateIdentifier` /
`UserIdentity` / `IUserIdentityTokenHandler` | Warning | Yes (drop the
disposable) |
| `UA0011` | Sync
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` | Info | No
(async promotion required) |
| `UA0012` | `CertificateFactory.*` static helpers | Warning | Yes (→
instance methods) |
| `UA0014` | `DataValue.IsGood(dv)` static helper | Warning | Yes (→
`dv.IsGood`) |
| `UA0015` | Sync / APM members on GDS / LDS clients | Info | No (async
promotion required) |
| `UA0018` | `CertificateIdentifier.Certificate` getter | Info | No (→
`LoadCertificate2Async`) |
| `UA0019` | `new DataValue(StatusCode[, ts])` | Warning | Yes (→
object-initializer form) |
| `UA0020` | `EncodeableFactory.GlobalFactory` / `Create()` | Warning |
Yes (→ `ServiceMessageContext.Factory` / `Fork()`) |
| `UA0021` | `CertificateValidator` / `CertificateValidationEventArgs`
(structural rename) | Info | No (see migration guide §Certificate
validation) |
| `UA0022` | `ApplicationConfiguration.CertificateValidator` /
`ServerBase.CertificateValidator` property rename | Warning | Yes (→
`.CertificateManager`) |

Plus one generator-only diagnostic:

| Rule | Source | Default | Triggers |
|------|--------|---------|----------|
| `MIG01` | Generator | Warning | Generator can't uniquely resolve the
element type for a `<Foo>Collection` reference (zero or multiple
candidates). The user adds a `using` for the namespace defining `Foo`,
or migrates the call site manually. |

### Runtime shim coverage (`Opc.Ua.MigrationAnalyzer.Core.dll`)

C# 14 `extension` members so 1.5.378 call sites continue to compile:

- **Moved obsolete extensions** the 2.0 libraries no longer carry
inline: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session`
sync helpers, `Subscription` sync helpers, `ApplicationInstance`
helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM,
`ChannelBase` static factory methods, etc.
- **New shims for genuinely-removed members**:
`EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate`
(throws), sync wrappers for
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM
wrappers for the GDS / LDS client APIs, `GlobalDiscoverySampleServer`
1.5.378-shape ctor (in-tree `Libraries/Opc.Ua.Gds.Server.Common/`).

> ⚠ The sync shims wrap their `*Async` counterparts via
`Task.Run(…).GetAwaiter().GetResult()`. Intended as a **migration aid
only** — port to `await` before shipping.

### Repo layout

```
Tools/Opc.Ua.MigrationAnalyzer/                  # analyzer DLL
  Analyzers/UA00xxAnalyzer.cs                    # 19 DiagnosticAnalyzer impls
  Diagnostics/{DiagnosticIds,Descriptors,WellKnownProperties}.cs
  Helpers/{SymbolExtensions,UaSymbols}.cs
  Opc.Ua.MigrationAnalyzer.nuspec                # ships all 3 analyzer DLLs + 6-TFM Core
  OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props

Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/        # code-fix DLL
  CodeFixes/UA00xxCodeFix.cs                     # 14 CodeFixProvider impls

Tools/Opc.Ua.MigrationAnalyzer.Generator/        # source generator DLL  (NEW)
  MigrationGenerator.cs                          # IIncrementalGenerator
  CollectionShimCatalog.cs                       # 30-entry element-type overrides
  Diagnostics/GeneratorDiagnostics.cs            # MIG01

Tools/Opc.Ua.MigrationAnalyzer.Core/             # runtime shim (multi-TFM)
  Client/, Configuration/, Core/, Gds.Client.Common/, Types/, Marker/

Tools/Roslyn.slnx                                # source-gen + migration projects

Tests/Opc.Ua.MigrationAnalyzer.Tests/            # 142 tests
  Analyzers/UA00xxTests.cs                       # 107 analyzer + code-fix tests
  Generators/MigrationGeneratorTests.cs          # 35 generator tests (NEW)
Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/       # 13 runtime-shim tests
```

### Documentation

-
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md)
— §3 Collection Type Migration gains a tip pointing at the generator so
CS0246 is replaced with `[Obsolete]` warnings + UA0002.
-
[`.github/agents/opcua-v20-migration.agent.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/agents/opcua-v20-migration.agent.md)
— Strategy section mentions the three Roslyn components + runtime shim
explicitly, plus the MIG01 diagnostic and the "internal-by-design"
CS0050 hint.

### Central package pinning

`Directory.Packages.props` pins `Microsoft.CodeAnalysis.*` (Common,
CSharp, CSharp.Workspaces, Workspaces.Common) to **4.14.0** — the stable
analyzer API surface. The earlier 5.3 pin was the *csc-internal*
version, which is not safe for analyzer hosts.

### Dogfood story

The full 42-sample dogfood against
`OPCFoundation/UA-.NETStandard-Samples` surfaced a **critical
analyzer-loader bug** in an earlier iteration: the analyzer DLL
initially co-shipped its code-fix providers in one assembly, which
transitively referenced `Microsoft.CodeAnalysis.Workspaces.dll`.
csc.exe's analyzer host ships only `Microsoft.CodeAnalysis.dll` +
`CSharp.dll` in its bincore and **silently swallows** load failures —
net result: zero diagnostics across all samples. Fix landed in commit
`861fa6ee1`:

1. **Split** the assembly: analyzer in `Opc.Ua.MigrationAnalyzer.dll`
(Workspaces-free, csc-safe), code-fixes in
`Opc.Ua.MigrationAnalyzer.CodeFixer.dll` (Workspaces-aware, IDE-only).
2. **Downgrade** to Roslyn 4.14 — the stable analyzer API.
3. `/p:ReportAnalyzer=true` confirms all 19 analyzers initialise and
execute on real consumer code.

This PR's new generator was smoke-tested against a minimal consumer that
exercises all 30 catalog entries + a model-compiled element type + the
unresolvable case:
- 30 catalog patterns + model-compiled `MyThingCollection` → all emit
shims, fire CS0618 with `(UA0002)` marker
- Element-type renames verified: `DateTimeCollection` → `DateTimeUtc`,
`ByteStringCollection` → `ByteString`, `GuidCollection` → `Uuid`, etc.
- `NeverHeardOfItCollection` → `MIG01` fires with help link to the
migration guide

Known compatibility gaps (documented in the migration agent):
- Legacy WinForms `.Net4` sample projects use pre-SDK MSBuild XML and
**ignore** `Directory.Build.targets` `<PackageReference>` injection —
consumers in this format must add the migration package to their csproj
inline.

### Packaging design notes

- Hand-authored nuspec ships 3 DLLs in `analyzers/dotnet/cs/` + 6 TFMs
of Core (shim) DLLs under `lib/`. All 6 OPC UA runtime package
dependencies are declared per-TFM-group.
- `build/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props` is
auto-imported by NuGet (file name must match package id exactly) and
injects all three analyzer DLLs as `<Analyzer>` items.
- `DiagnosticIds.cs` / `DiagnosticDescriptors.cs` /
`WellKnownProperties.cs` / `Helpers/*.cs` are **shared via `<Compile
Link=…>`** between the analyzer and code-fixer projects to break what
would otherwise be a NuGet restore cycle. The generator has no compile
dependency on the analyzer; the 30-entry override table is duplicated in
`CollectionShimCatalog.cs` (the analyzer table also drives diagnostic
messages, so the duplication is small and intentional).
- `Opc.Ua.MigrationAnalyzer.Core` is multi-TFM (`net472, net48,
netstandard2.1, net8.0, net9.0, net10.0`) and **non-packable on its
own**; the parent migration package's nuspec consumes each TFM's DLL by
path.
2026-06-05 12:01:53 +02:00
CopilotandRoman Ettlinger 0c2e2f9849 Fix CTT alarm configuration to reference alarms under AnalogSource and BooleanSource nodes (#3830)
# Description

Fixes the issue where alarms could not be properly initialized via the
CTT (`--ctt`) flag. The fix addresses both the server-side
initialization and the CTT configuration node references.

The fix involves two complementary changes:

1. **Async Node Manager Registration**: The root cause was that the
`AlarmNodeManager` and other async node managers were never being
registered when the Reference Server started. Without the
`AlarmNodeManager` being initialized, the Alarms namespace was not
registered, making it unavailable when `ApplyCTTModeAsync` attempted to
use it.

2. **Correct CTT Configuration References**: The CTT configuration file
now includes explicit namespace references for alarm types that
correctly reference their actual location in the address space. After
the A&C implementation changes, alarms are now children of
`AnalogSource` and `BooleanSource` nodes (not direct children of the
Alarms folder), and the CTT configuration has been updated to reflect
this structure.

## Changes Made

**ConsoleReferenceServer startup
(`Applications/ConsoleReferenceServer/Program.cs`)**
- Added explicit registration of async node managers before server
startup
- Ensures that the `AlarmNodeManager` and all other async factories are
properly initialized
- Follows the same pattern already used for GDS node manager
registration
- Added registration loop to process all async node manager factories

**CTT Configuration (`Applications/UAReferenceServer.ctt.xml`)**
- Updated explicit namespace references for 8 alarm type Input Nodes to
point to the correct source nodes:
  - **Analog-based alarms** → `ns=7;s=Alarms.AnalogSource`:
    - DiscrepancyAlarmType
    - LimitAlarmType
    - ExclusiveLimitAlarmType
    - ExclusiveDeviationAlarmType
    - NonExclusiveDeviationAlarmType
    - AlarmConditionType
  - **Boolean-based alarms** → `ns=7;s=Alarms.BooleanSource`:
    - OffNormalAlarmType
    - SystemOffNormalAlarmType
- Minimal surgical changes: only the namespace mappings were updated to
reference the correct source nodes

The async node managers are now registered between the sync node manager
registration (`server.Create()`) and the server startup
(`server.StartAsync()`), ensuring all namespaces are available when CTT
initialization occurs.

## Testing

- ✅ Server with `--ctt` flag now properly initializes the Alarms
namespace
- ✅ CTT alarms can be enabled without "namespace does not exist" error
- ✅ Explicit namespace mappings correctly reference alarm types under
their respective source nodes
- ✅ Follows existing code patterns for consistency
- ✅ No build or analyzer warnings introduced
- ✅ Build succeeds with 0 warnings, 0 errors

## Related Issues

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
2026-06-04 13:33:31 +02:00
Marc Schier 81b3c9b371 [Server] Implement NodeManagement service set and fix DeleteAddressSpace (#3839)
# Description

Adds the OPC UA NodeManagement service set (AddNodes / DeleteNodes /
AddReferences / DeleteReferences) to the server SDK and fixes a
long-standing shutdown bug where `NodeState.Delete` callbacks never
fired on address-space teardown.

### NodeManagement service set (#3736)

- New optional `INodeManagementAsyncNodeManager` facet interface (added
to `INodeManager.cs` alongside other facets, discovered via `as`,
**not** part of the `IAsyncNodeManager` composite). Opt-in per
NodeManager via `AllowNodeManagement` — default is `false` so existing
NodeManagers preserve binary compatibility.
- Dispatcher in `MasterNodeManager` routes:
- **AddNodes** by `RequestedNewNodeId.NamespaceIndex` when supplied,
otherwise to the parent's owning NodeManager. When the parent's owner is
read-only (e.g. `ObjectsFolder` owned by `CoreNodeManager`), the
dispatcher falls back to the first opted-in NodeManager so conformance
flows work out of the box.
- **DeleteNodes** / **AddReferences** / **DeleteReferences** to the
owning NodeManager of the target or source node, returning
`BadUserAccessDenied` when that NodeManager has not opted in.
Cross-NodeManager `AddReference` / `DeleteReference` best-effort mirrors
the inverse edge on the target's NodeManager.
- `AsyncCustomNodeManager` provides a default implementation: per-item
validation, `RequestedNewNodeId` honored, browse-name dedup for both
local and cross-NodeManager parents, `DeleteTargetReferences` semantics,
and a NodeId allocator fallback when an `INodeIdFactory.New` override
returns a null `NodeId` (e.g. parent-based hierarchical naming with no
parent attached).
- `StandardServer` adds the four service overrides plumbed through
`OperationLimits.MaxNodesPerNodeManagement`, audit events, and
`OnRequestComplete`; new `ReportAuditAddReferencesEvent` /
`ReportAuditDeleteReferencesEvent` helpers in `AuditEvents`.
- Quickstarts `ReferenceNodeManager` opts in via `AllowNodeManagement =>
true`; the legacy partial-class override in
`ReferenceServer.NodeManagement.cs` is removed.

### `NodeState.Delete` on shutdown (#3762)

- `CustomNodeManager.DeleteAddressSpace` and
`AsyncCustomNodeManager.DeleteAddressSpaceAsync` now snapshot root
`PredefinedNodes` (skipping children whose parent is in the snapshot to
prevent double-fire), invoke `NodeState.Delete(SystemContext)` on each
root (which recurses into children and raises `OnBeforeDelete` /
`OnAfterDelete` / `NodeStateChangeMasks.Deleted`), and then clear the
map.
- Derived overrides in Quickstarts `ReferenceNodeManager`,
`SampleNodeManager`, and `FileSystemNodeManager` chain to `base` so the
callback fires for their nodes too.

### Tests

- **21 new dispatcher unit tests** in
`Tests/Opc.Ua.Server.Tests/NodeManager/AsyncCustomNodeManagerNodeManagementTests.cs`
covering `AllowNodeManagement` default/opt-in, AddNode validation
(`BadBrowseNameInvalid`, `BadParentNodeIdInvalid`, `BadNodeIdRejected`,
`BadNodeIdExists`, `BadBrowseNameDuplicated`, `BadNodeClassInvalid`),
RequestedNewNodeId paths, DeleteNode + `DeleteTargetReferences`,
AddReference duplicate detection (`BadDuplicateReferenceNotAllowed`),
and DeleteReference (`BadNoMatch`).
- **2 new DeleteAddressSpaceAsync tests** in
`AsyncCustomNodeManagerTests` verifying `NodeState.Delete` invocation
and no double-fire on children.
- Updated InformationModel
`NodeManagementAddRefTests.AddReferenceBetweenExistingNodesSucceedsAsync`
to accept `BadUserAccessDenied` / `BadNotSupported` when the source NM
has opted out.
- **Full `Opc.Ua.Server.Tests` on net10.0: 1709 passed, 0 failed.**
- **All 26 InformationModel `Category=NodeManagement` conformance tests
pass** against the reference server (1 skipped where source NM opts
out).

### Docs

- New [`Docs/NodeManagement.md`](Docs/NodeManagement.md) with
architecture, opt-in pattern, dispatch and routing rules,
request-handling table, error-code table, audit events, and
custom-behavior examples.
- `Docs/MigrationGuide.md` gets two new sections — *NodeManagement
service set* (downstream migration steps) and *DeleteAddressSpace
invokes NodeState.Delete (#3762)* (shutdown behavior change) — with TOC
entries.
- `Docs/README.md` links the new `NodeManagement.md`.

## Related Issues

- Fixes #3736
- Fixes #3762

## Checklist

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

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-04 13:02:32 +02:00
Marc Schier 4756e7c245 DI (OPC 10000-100) with samples (Pump, SW Update), fluent API gap closures and model dependency implementation (#3808)
This PR implements the **OPC UA Device Integration (DI, OPC 10000-100)**
companion-spec library trio (`Opc.Ua.Di` / `Opc.Ua.Di.Server` /
`Opc.Ua.Di.Client`) plus a worked example
(`Applications/MinimalPumpServer`) demonstrating the OPC 40223 Pumps
companion specification end-to-end with full simulation. It also closes
11 fluent-API gaps in `Opc.Ua.Server.Fluent`, three source-generator
bugs, integrates the DI layer into the unified `services.AddOpcUa()`
Microsoft.Extensions DI hosting pattern, and adds 250+ new tests.

**Worked example — `Applications/MinimalPumpServer/`**
- AOT-compatible console host using `Host.CreateApplicationBuilder` +
`AddOpcUa()`.
- `PumpNodeManager` inherits from `DiNodeManager`; multi-namespace (DI +
Machinery + Pumps) via runtime NodeSet2 loading and
`ModelLoaderBuilder`.
- Demonstrates declarative second-pump creation via
`.ConfigureDevicesFor<PumpNodeManager>(...)` and software-package
seeding from the same configurator.
- Full simulation: pressure, temperatures, power, flow, efficiency,
level, cavitation/overheat fault injection, alarm setup, NAMUR-style
supervision.

**Documentation**
- `Docs/Opc.Ua.Di/` — README, DeviceBuilder, Hosting, LockService,
SoftwareUpdate, ClientHelpers (6 new developer guides).
- `Docs/Opc.Ua.Di/ComplianceMatrix.md` — every DI declaration mapped to
In-v1 / Generated / Deferred / Excluded.
- `Docs/CompanionSpecLibraries.md` — packaging guide for new
companion-spec libraries.
- `Docs/FluentApiGaps.md` — historical record of G1–G11.
- `Docs/SourceGeneratedNodeManagers.md` updated for richer node
managers.
- `Docs/plans/StateMachineBuilder.md` — deferred state-machine builder
design.
2026-06-04 05:57:57 +02:00
Suciu Mircea Adrian 82993d0b6f Allow application certificate lookup to use entries configured with abstract ApplicationCertificateType only as fallback candidates. (#3752)
## Proposed changes

Tries to rectify inconsistent behavior:
`SecurityConfiguration::BuilldSupportedSecurityPolicies()` advertises
RSA policies if ApplicationCertificateType is set as CertificateType,
while ApplicationCertificateType is not used when finding actual
certificates.

Use CertificateIdentifier::SetCertificate(certificate) to recalculate
the CertificateType whenever the identifier receives a X509Certificate2.

Removed stale if case` if (certType ==
ObjectTypeIds.ApplicationCertificateType)` from
`SecurityConfiguration::FindApplicationCertificateAsync`, since
`CertificateIdentifier.MapSecurityPolicyToCertificateTypes(securityPolicy)`
never returned 'ApplicationCertificateType' and it was confusing and
misleading because it suggested support for abstract
ApplicationCertificateType, but normal policy lookup did not actually go
through it.

Allow application certificate lookup to use entries configured with
abstract ApplicationCertificateType as fallback candidates.
The fallback loads the candidate certificate first, then validates it
against the concrete type required by the selected security policy.

'ApplicationCertificateType' is not a concrete certificate profile and
should not be prefered in configuration, even if older configurations
may use it. Previously, such entries could be missed during certificate
lookup because the selected security policy maps to a concrete type.

## Related Issues

- Fixes #3644

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR._

- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)
- [ ] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to
ask. We're here to help! This is simply a reminder of what we are going
to look for before merging your code._

- [ ] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [ ] I ran tests locally with my changes, all passed.
- [ ] I fixed all failing tests in the CI pipelines. 
- [ ] I fixed all introduced issues with CodeQL and LGTM.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added necessary documentation (if appropriate).
- [ ] Any dependent changes have been merged and published in downstream
modules.

## Further comments

If this is a relatively large or complex change, kick off the discussion
by explaining why you chose the solution you did and what alternatives
you considered, etc...
2026-06-04 05:56:37 +02:00
Randy Armstrong 5665fab2d4 Add ECC-Testing-README.md 2026-06-03 19:54:42 -07:00
Randy Armstrong eb8527ecf1 Fix merging issues. 2026-06-03 16:46:18 -07:00
Randy Armstrong 0b0c6a099a Add CryptoTrace, fix ECC testing 2026-06-03 15:18:07 -07:00
Marc Schier dbc09b8e22 Adopt System.TimeProvider across the stack (fixes #2458) (#3829)
# Description

Adopt
[`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider)
across the stack so that every place that creates timers, runs duration
math, or schedules periodic work is mockable, deterministic in tests,
and immune to wall-clock changes.

The migration touches Core, Core.Types, Server, Client, PubSub,
Configuration, Gds (server + client), Lds, and the Quickstarts/Console
reference apps. Per-component changes follow the same pattern:

1. Add a `private readonly TimeProvider m_timeProvider;` field.
2. Accept a nullable `TimeProvider? timeProvider = null` as the **last**
constructor parameter (new overload; legacy constructor stays for binary
compatibility and delegates with `timeProvider: null`).
3. Initialise the field with `m_timeProvider = timeProvider ??=
TimeProvider.System;`.
4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`,
`DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in
timing loops with the matching `m_timeProvider` API (`CreateTimer`,
`GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`,
`Task.Delay(delay, m_timeProvider, ct)`).
5. Wire the provider through DI: `AddOpcUaServerBuilder` /
`AddOpcUaClientBuilder` register `TimeProvider.System` via
`TryAddSingleton<TimeProvider>()` so tests can swap in
`Microsoft.Extensions.Time.Testing.FakeTimeProvider`.

`HiResClock` itself is kept (so the
`ApplicationConfiguration.DisableHiResClock` schema flag still
round-trips), but **every public member** is now `[Obsolete]` with a
single shared message pointing to the new migration-guide section. The
few internal call sites that still need to set/read `HiResClock` are
wrapped with `#pragma warning disable CS0618`.

### Highlights per component

- **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel
pipeline (`UaSCBinaryClientChannel`, `ChannelToken`,
`UaSCBinaryChannel`), `ApplicationConfiguration`, certificate
validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills
(`GetTimestampMilliseconds`, `GetTickCount`,
`CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay,
provider, ct)` polyfill for older TFMs).
- **Server** — session lockout, subscription pipeline
(publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm
sampling rate, `IntervalRunner`, server diagnostics, configuration state
machines, hosted services.
- **Client** — `Session`, managed/V2 subscription engines, reconnect
handlers, channel keep-alive, periodic publishing, complex-type
discovery polling.
- **PubSub** — `IntervalRunner` pipeline, message-receive timers,
MQTT/UDP keep-alive, publishers/readers.
- **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry
housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`,
`ConnectionStateMachine`, classic subscription engine.
- **Quickstarts / Console samples** — `SampleNodeManager`,
`ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`,
`MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via
`(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System`
so existing ctor signatures stay binary-compatible.
- **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing
`SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off
`HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package
for `FakeTimeProvider` (project-load warning surfaces on net48/net472
only — runtime is .NET 8+).

### Migration guide

New section [**Time and Timer abstraction
(`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider)
documents:

- The canonical replacement table (`HiResClock.UtcNow` →
`timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` →
`timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`,
`Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …).
- The `TimeProvider? timeProvider = null` + `??=` constructor pattern.
- DI wiring and how to swap in `FakeTimeProvider` in tests.
- Before/after snippets for `HiResClock.TickCount64`,
`HiResClock.UtcNow`, and `new Timer(...)`.

## Related Issues

- Fixes #2458

## 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.
2026-06-03 22:38:32 +02:00
Copilot 43a8358eac Update Profiles.md to document v2 improvements and new security profiles (#3832)
The Profiles.md documentation was outdated and did not accurately
reflect the significant improvements and new features added in v2, as
well as containing an inaccuracy regarding server-side aggregate
support. This PR updates the document to provide an accurate and
comprehensive profile of current functionality.

## Changes Made

- **Overview**: Updated to highlight v2 improvements and clarified OPC
UA specification version to 1.05.07
- **Server Features**: Added three new Additional Features sections
documenting:
  - Alias Names (OPC UA Part 17) with wildcard search
  - WoT Connectivity (OPC 10100-1) for IoT asset integration
  - Model Change Tracking for address space change notifications
- Added reference to fluent API and dependency injection patterns for
feature configuration
- **Client Enhancements**: Expanded Client Profiles section with new
capabilities:
- Streaming Subscriptions integrated into subscription management
(IAsyncEnumerable-based API)
  - Model Change Tracking (address space invalidation)
  - File System Operations (System.IO-style async client)
  - Alarms and Conditions (full subscription support)
- **Security Profiles**: Significantly expanded security policy
documentation to include v2.0 additions:
  - Modern ECC Curves: Curve25519 and Curve448 with AEAD ciphers
  - AES-GCM and ChaCha20-Poly1305 variants for traditional ECC curves
  - RSA Diffie-Hellman key agreement policies
  - Updated platform requirements to reflect modern cipher support needs
- **GDS Expansion**: Added Authorization Service and Key Credential
Service to GDS section for token and credential issuance
- **Restructuring**: Removed "Currently Not Supported (Server)" section
to provide clearer distinction between fully implemented features and
those with limited coverage, and integrated streaming subscriptions as a
subscription management capability rather than a separate profile
- **Documentation**: Added 8 new entries to Related Documentation
linking to comprehensive guides for all v2 features
- **Aggregate Correction**: Corrected the inaccurate claim that
server-side aggregates are not implemented. Server-side aggregates
(Average, Total, Minimum, Maximum, and more) are fully implemented via
streaming fallback and provider push-down. Removed "Aggregates Server
Facet" from the "Currently Not Supported" section.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-03 12:43:28 +02:00
Marc Schier 49305d9085 V2 subscription engine close gap for test and feature parity with classic engine (#3824)
# Description

V2 subscription engine feature parity round to make the classic engine
eligible for deprecation. Splits the test surface so the classic and V2
engines can evolve independently, ports the gaps the classic-engine
integration tests had been exercising, and lands the public V2 API
additions needed to close them.

- Splits `Tests/Opc.Ua.Subscriptions.Tests` into a classic
(`Opc.Ua.Subscriptions.Classic.Tests`) and a V2
(`Opc.Ua.Subscriptions.Tests`) project to shrink CI time and let the two
engines diverge cleanly.
- Adds the V2 public-surface gaps required by the test ports
(`Subscription.ServerId` on the class, `SetSubscriptionDurableAsync`,
`SendInitialValuesOnTransfer`, unified `OnSubscriptionStateChangedAsync`
handler callback, per-item `ConditionRefreshAsync`, fluent
`LoadSubscriptionsAsync(Stream, …)` and `Subscription.Snapshot()` /
`RestoreAsync()` for save/load + transfer).
- Lands handler-centric design (no `PublishingStopped` property handlers
maintain derived state); always-sequential per-subscription delivery
documented + tested.
- **Adds short and long haul stability tests for the V2 `ManagedSession`
+ subscription engine. Closes #3744** (see "Issue #3744 Haul tests"
below).
- **Migrates SubscriptionStateSnapshot / MonitoredItemStateSnapshot to
`[DataType]` source-generator codecs**.
- `SubscriptionDurableV2Tests.cs` 5 V2 ports of the classic durable test
patterns.
- `V2FollowUpCoverageTests.cs` handler state-change callback, fluent
stream-based `LoadSubscriptionsAsync`, `SendInitialValuesOnTransfer`,
snapshot edge cases (empty / `DataChangeFilter` round-trip / concurrent
  mutation).
- `SubscriptionFailoverV2Tests.cs` forced channel-break with and without
`WithTransferSubscriptionsOnRecreate`.
- `MonitoredItemConditionRefreshLiveV2Tests.cs` ΓÇö live
`ConditionRefresh` against the reference server's event source; asserts
`RefreshStart` / `RefreshEnd` events flow through the handler.

## Fixes

- #3744 Haul tests (V2 ManagedSession + subscription engine)
`Tests/Opc.Ua.Sessions.Tests/ManagedSessionStabilityTest.cs` adds the
short/long haul coverage requested by
[#3744](https://github.com/OPCFoundation/UA-.NETStandard/issues/3744)
plus the bonus fault-injection variant:

| Test | Duration | Category | Purpose |
|---|---|---|---|
| `ShortHaulManagedSessionV2Async` | 2 minutes | `ManagedSessionHaul`
(`[Explicit]`) | Quick local sanity check of the V2 monotonicity
contract. |
| `LongHaulManagedSessionV2Async` | 90 minutes (configurable via
`TEST_DURATION_MINUTES`) | `ManagedSessionHaul` (`[Explicit]`) |
Long-running V2 stability + monotonicity. |
| `LongHaulManagedSessionWithFaultInjectionV2Async` | 90 minutes
(configurable) + fault every `FAULT_INJECTION_INTERVAL_SECONDS` (default
60s) | `ManagedSessionHaul` (`[Explicit]`) | **Bonus** ΓÇö force-closes
the subscriber transport channel periodically; verifies V2 manager
transfers/recreates the subscription and continues monotonic delivery
across reconnects. |

**Pattern.** A writer `ManagedSession` increments a `uint32` counter and
writes it into `Scalar_Static_Mass_UInt32_UInt32_00` every 250ms. A
subscriber `ManagedSession` runs a V2 subscription with a single
monitored item on the same node. `MonotonicCounterHandler` asserts each
received sample is *strictly greater than* the previous one. V2
`ISubscriptionNotificationHandler` per-subscription ordering guarantee.
Skipped values (sampling rate < write rate) are tolerated; duplicates
and reorderings are violations.
2026-06-03 04:39:08 +02:00
Copilot b3518b53df [Server]: Queue Events from the Server Node using multiple channel consumers (#3823)
# Description

This pull request introduces a scalable consumer task model for the
`MonitoredNode2` class, optimizing the handling of event notifications
for notifier nodes. Any notifier node can opt into (or out of) dynamic
scaling of consumer tasks based on the number of event monitored items,
improving throughput and responsiveness under high event loads. The
Server node (`ObjectIds.Server`) is always auto-opted-in. The
implementation includes safe scaling up and down of consumers, robust
disposal logic, Fluent API integration, and tests to verify correct
delivery of events.

**Scalable consumer task management:**

* Added an `enableMultipleEventConsumers` constructor parameter to
`MonitoredNode2` that enables dynamic scaling of consumer tasks to match
the number of event monitored items. The Server node
(`ObjectIds.Server`) always auto-opts-in regardless of this parameter.
* Added `AllowMultipleEventConsumers(NodeId, bool enable = true)`
protected method on `AsyncCustomNodeManager` with an internal
`MultiConsumerNodeIds` registry, so any node manager can
programmatically opt in or out specific nodes.
* Added `IsMultipleEventConsumerNode(NodeId)` to the `IAsyncNodeManager`
interface, allowing consumers to query multi-consumer opt-in status
through the interface without casting to `AsyncCustomNodeManager`.
* Both `MonitoredNodeMonitoredItemManager` and
`SamplingGroupMonitoredItemManager` use the `IAsyncNodeManager`
interface method when creating `MonitoredNode2` instances.

**Fluent API integration:**

* Added `AllowMultipleEventConsumers(bool enable = true)` method to
`INodeBuilder` and its `NodeBuilder` implementation, allowing any
notifier node to be opted in or out via the fluent builder pipeline
(e.g. `builder.Node("myNotifier").AllowMultipleEventConsumers()`).
* The `NodeManagerBuilder` routes the registration to the owning
`AsyncCustomNodeManager`'s multi-consumer registry, supporting both
opt-in and opt-out.

**Testing and verification:**

* Added unit tests to verify multi-consumer behavior for both Server
node (auto-opt-in) and non-Server nodes (explicit opt-in), as well as
default single-consumer behavior for nodes without opt-in.
* Fixed race condition in permission cache tests
(`OnMonitoredNodeChanged_DefaultPermissionsChanged_CacheInvalidated` and
`InvalidatePermissionCacheForSession` tests) that caused flaky CI
failures. The tests now signal on `QueueValue` (which fires after the
cache is populated) instead of `ValidateRolePermissionsAsync` (which
fires before), ensuring cache invalidation operates on a populated
cache.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-02 07:14:36 +02:00
Marc Schier 0000513cde Pluggable user-identity provider model (#3807)
# Description

Introduces a pluggable user-identity provider model covering all OPC UA
Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on
both client and server, with full
`Microsoft.Extensions.DependencyInjection` integration, plus the Part 12
§9 `AuthorizationService` two-phase token-request flow **including
refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a
clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The
work is split across phase-by-phase commits so it can be reviewed
independently; nothing here changes wire-level behaviour or forces
existing applications to migrate.

### Documentation

All under `Docs/`:

- `IdentityProviders.md` — full developer guide: three-layer model,
recipes, configuration reference, "what gets auto-wired" matrix, JWS
algorithm table, identity-augmenters section, **"Implementing your own
provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore
(sibling files merged in per review feedback).
- `DependencyInjection.md` — `Identity` sections with `appsettings.json`
examples, identity-extension matrix, `WithAuthorizationService` +
`WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented.
- `RoleBasedUserManagement.md` — claim-criteria callout describing
`IIdentityClaims` probe surface.
- `AuthorizationService.md` — Architecture diagram +
`IAccessTokenProvider` conceptual narrative restored from master; modern
`StartRequestToken` / `FinishRequestToken` flow leads; refresh-token
recipe added.
- `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge
decision matrix + Hybrid (Push + Pull) section; explicit experimental
warning for the bridge.
- `GDS.md` — KeyCredential Push, `StartRequestToken`,
`GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with
rotation policy and audit semantics** all documented.
- `MigrationGuide.md` — User Identity Providers section listing
`[Obsolete]` markers + mechanical recipes; SelfAdmin migration via
`IIdentityAugmenter`; `EnableRefreshTokens` default note.
- `README.md` index entries.

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-06-02 01:14:54 +02:00
Marc Schier 01d7162313 Part 9 Alarms & Conditions: complete server + client implementation (#3805)
Complete implementation of **OPC UA Part 9 ΓÇö Alarms & Conditions** for
both server and client, **OPC UA Part 16 ΓÇö State Machines** (generic
extensible client + server API), and the supporting infrastructure
(source-generated event records, source-generated ObjectType proxies,
`IAsyncEnumerable` streaming subscriptions, address-space model change
tracking, per-node `INodeCache` invalidation, unified DI integration).

### Server-side (Part 9 complete)

- **`AlarmConditionState`** ΓÇö every Part 9 method wired automatically
in `OnAfterCreate`: `Silence`, `Suppress`/`Suppress2`,
`Unsuppress`/`Unsuppress2`, `RemoveFromService`/`RemoveFromService2`,
`PlaceInService`/`PlaceInService2`, `Reset`/`Reset2`,
`GetGroupMemberships`. Each method generates the spec-mandated audit
event and supports an optional veto-delegate hook.
- **Latched alarms** ΓÇö `ActiveState` reflects real process state;
`LatchedState` persists until explicit `Reset` (with full precondition
validation).
- **Re-alarming** ΓÇö
`ProcessReAlarm`/`ResetReAlarmRepeatCount`/`IsReAlarmEnabled` helpers
(opt-in, no hidden timer).
- **Audible alarms** ΓÇö `UpdateAudibleState` for
`AudibleEnabled`/`AudibleSound`/`SilenceState` lifecycle.
- **Suppression + groups** ΓÇö `AlarmGroup`, `AlarmSuppressionEngine`
(with first-in-group), `AlarmRateTracker` (for `AlarmMetricsType`).
- **Spec-compliance fixes** ΓÇö `SuppressedOrShelved` now includes
`OutOfServiceState`; `GetRetainState` includes `LatchedState`;
`EffectiveDisplayName` composition includes `OutOfServiceState` +
`LatchedState`.

### Client-side Part 9 API

- **`AlarmClient`** ΓÇö typed Part 9 operations. Each method delegates
to the matching source-generated `*TypeClient` proxy
(`ConditionTypeClient`, `AcknowledgeableConditionTypeClient`,
`AlarmConditionTypeClient`, `DialogConditionTypeClient`,
`ShelvedStateMachineTypeClient`) ΓÇö one source of truth for every
method NodeId.
- **Interface hierarchy** — `IConditionOperations` →
`IAcknowledgeableConditionOperations` → `IAlarmOperations` +
`IDialogConditionOperations`.
- **Source-generated event records** ΓÇö new `EventRecordGenerator`
(alongside `ObjectTypeProxyGenerator`) emits `{Type}TypeRecord` per OPC
UA event type: `ConditionTypeRecord` →
`AcknowledgeableConditionTypeRecord` → `AlarmConditionTypeRecord` →
`LimitAlarmTypeRecord`/`ExclusiveLimitAlarmTypeRecord`/… all the way
to vendor subtypes. **Vendor alarm types get records automatically.**
- **`AlarmEventDecoder`** ΓÇö decodes `EventNotification.Fields` into
the most-specific generated record.
- **`AlarmEventFilterBuilder`** ΓÇö fluent builder for Part 9
`EventFilter`s.
- **`ISession.GetAlarmClient(telemetry)`** convenience extension.

### Part 16 state machines (generic, extensible)

- **Client**: extension methods on the source-generated
`StateMachineTypeClient` / `FiniteStateMachineTypeClient` ΓÇö
`GetCurrentStateAsync`, `ObserveFiniteTransitionsAsync`
(`IAsyncEnumerable<FiniteStateSnapshot>`), `WaitForStateAsync`,
`GetAvailableStatesAsync` / `GetAvailableTransitionsAsync`. **Vendor
proxies inherit the API automatically** through the generated
inheritance chain.
- **Server**: `StateMachineBuilder` (fluent: `AddState` /
`AddTransition` / `OnCause` / validation) +
`FluentFiniteStateMachineState` ΓÇö declare a Part 16 state machine
without subclassing the abstract base.
- **Alarms alignment**: `AlarmClient.GetShelvingStateAsync` /
`ObserveShelvingTransitionsAsync` surface the API for every alarm's
`ShelvingState` child.

### IAsyncEnumerable streaming subscription

- **`IStreamingSubscription`** with
`SubscribeDataChangesAsync`/`SubscribeEventsAsync` returning
`IAsyncEnumerable<T>` (Channel-backed).
- **`ManagedSession.DefaultStreaming`** ΓÇö lazy shared subscription;
disposed with the session.
- **Helpers** ΓÇö `TakeUntilAsync`, `WithTimeoutAsync`, `TakeAsync`,
`BufferedAsync` for state-machine waits and bounded observation windows.
- **`AlarmStreamExtensions`** ΓÇö
`SubscribeAlarmsAsync`/`SubscribeConditionsAsync`/`SubscribeDialogsAsync`
returning the source-generated record types.

### Address-space model change tracking

- **Server**: `RaiseGeneralModelChangeEvent` on both `CustomNodeManager`
and `AsyncCustomNodeManager`; `ModelChangeAggregator` (in
`Opc.Ua.Server.NodeManager`, used by every node manager) batches changes
per service call; **auto-emit** from `CreateNode`/`DeleteNode` (opt-out
via `ModelChangeEmissionEnabled = false`).
- **Client**: `IModelChangeTracker` subscribes to
`BaseModelChangeEventType` via the streaming subscription, decodes
`ModelChangeStructureDataType` payloads, invalidates the `INodeCache`
per-node.
- **`INodeCache.InvalidateNode(NodeId)`** ΓÇö new interface member (true
per-node eviction in `NodeCache`).
- **`ManagedSession.EnableModelChangeTrackingAsync()`** +
`ManagedSessionOptions.ModelChangeTracking` +
`ManagedSessionBuilder.WithModelChangeTracking()` fluent method.

### Dependency Injection integration

- **`services.AddOpcUa().AddClient(...).AddAlarms()`** ΓÇö registers
`AlarmClientFactory` so DI-hosted apps can resolve a Part 9 client per
session without `new`-ing one manually.
- Cross-link added to the unified DI surface (`AddServer` / `AddClient`
/ `AddComplexTypes` / `AddGdsServer` / `AddLdsServer` /
`AddWotConServer`).

### Quickstart server modernization

- `Quickstarts.Servers/Alarms/AlarmNodeManager.cs` ported to
`AsyncCustomNodeManager` and demonstrates the new `AlarmGroup` +
`AlarmSuppressionEngine` end-to-end via a writable `MaintenanceMode`
boolean that suppresses every member alarm.

### Documentation

- New: `Docs/AlarmsAndConditions.md`, `Docs/StreamingSubscription.md`,
`Docs/ModelChangeTracking.md`, `Docs/StateMachines.md` ΓÇö full
developer guides.
- Updated: `Docs/DependencyInjection.md` (`AddAlarms()` row + section),
`Docs/README.md` index, `Docs/MigrationGuide.md` (Part 9 / Part 16 /
model change tracking sections covering behavior changes + breaking API
surface).
- Sample: `Applications/ConsoleReferenceClient/AlarmClientSample.cs`.
2026-05-29 23:24:53 +02:00
Marc Schier 8d2402c995 Bump version to 2.0 and land 1.5.378 to 2.0 migration guide (#3819)
Prepares the repository for a 2.0 release: bumps every version reference
from 1.6 to 2.0, and lands the migration guide describing the 1.5.378 →
2.0 upgrade path. The 1.6 line never shipped, so this is a straight
rename with no historical 1.6 migration content to preserve.

The PR is split into two commits:

1. **`Bump version to 2.0 (next release is a major version change)`** —
version metadata, NuGet metadata, the side documentation, and the
migration agent file.
2. **`[Docs] Close 19 audit gaps in 1.5.378 to 2.0 migration guide`** —
adds and reorganizes `Docs/MigrationGuide.md` to close every gap from a
rigorous code-review audit of the 1.5.378 → 2.0 breaking-change surface.

### Version bump (commit 1)

- `version.json`: `1.6-preview` → `2.0-preview`
- `common.props`: `<VersionPrefix>1.6</VersionPrefix>` →
`<VersionPrefix>2.0</VersionPrefix>`
- `nuget/Opc.Ua.nuspec` and `nuget/Opc.Ua.Symbols.nuspec`: `1.6.0` →
`2.0.0`
- `README.md`: "New in 1.6" → "New in 2.0"
- `Docs/ComplexTypes.md`, `Docs/Observability.md`, `Docs/Sessions.md`:
1.6 → 2.0 mentions
- Renames `.github/agents/opcua-v16-migration.agent.md` →
`.github/agents/opcua-v20-migration.agent.md` and updates the agent
metadata (name, description, trigger phrases, version table)
- `UA.slnx`: solution-items reference updated to the new agent filename
-
`Tools/Opc.Ua.SourceGeneration.Core/Generators/ObjectTypeProxyGenerator.cs`:
two v16 → v20 comment fixes

### Migration guide (commit 2)

Closes 19 gaps from an audit comparing `master378` (1.5.378) with
`master` (2.0). Adds a new top-level **Package, Target Framework and
Dependency Changes** section covering the new `Opc.Ua.Core.Types` and
`Opc.Ua.Security.Certificates` packages, the `LibTypesTargetFrameworks`
netstandard2.0 drop, the NuGet dependency additions/removals, and the
residual Newtonsoft.Json dependency in `Opc.Ua.PubSub`.

In-place additions:

- new H4 for `INodeManager3` under Node States
- `Session.Call` `Variant.From` worked example
- new H3 `Subscriptions and Transports` with H4 verdicts for Durable
subscriptions (source-breaking), PubSub (no source break) and Reverse
connect (no source break)
- new H3 `Telemetry and Logging` covering `ITelemetryContext`
constructor-injection patterns
- `in DataValue` async copy-to-local pattern
- UNSAFE / SAFE pair for `WithPoolNotifications` pooling
- using-alias example for classic vs V2 `SubscriptionOptions`
- "Silent regression" callout for `OnAfterCreate`
- numbered lifecycle ordering for
`ApplicationConfiguration.CertificateManager`

Corrections:

- clarifies that the `SessionReconnectHandler` `[Obsolete]` is still
present in 2.0 (not withdrawn);
- clarifies that the source-generated `ServerCapability` constants vs
the `ServerCapabilityInfo` registry type are distinct;
- disambiguates `ReconnectPolicyOptions` (DTO) from `ReconnectPolicy`
(runtime).

Adds an "Automate the migration" pointer to the forthcoming
`Opc.Ua.CodeFixers` analyzer package, which mechanizes 17 of the
patterns covered in this guide. The analyzer is published in a follow-up
PR.
2026-05-29 08:18:41 +02:00
5dd53ff9d0 [Server] Migrate all NodeManagers to AsyncCustomNodeManager (#3817)
Migrate all NodeManagers to IAsyncNodeManager
Add more Tests

## Related Issues

Issue linking is handled automatically by the system.

## Types of changes

What types of changes does your code introduce?  
_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR._

- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] Enhancement (non-breaking change which adds functionality)
- [ ] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to
ask. We're here to help! This is simply a reminder of what we are going
to look for before merging your code._

- [ ] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes, all passed.
- [x] I fixed all failing tests in the CI pipelines. 
- [x] I fixed all introduced issues with CodeQL and LGTM.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added necessary documentation (if appropriate).
- [ ] Any dependent changes have been merged and published in downstream
modules.

## Further comments

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-05-28 20:24:22 +02:00
Copilot ce9b2b4bff [Server] Load existing users through IUserDatabase.GetUsers (#3811)
**User Management Improvements:**

* The `UserManagement` class now loads existing users from the user
database during initialization, ensuring that the in-memory user list
reflects the database state through the abstraction boundary.
* The `IUserDatabase` interface now exposes `GetUsers()`, returning
`UserManagementDataType` records instead of only user names.
* `UserManagement.LoadUsersFromDatabase()` now consumes
`IUserDatabase.GetUsers()` so custom user database implementations can
provide persisted user metadata such as `UserConfiguration` and
`Description`.
* `LinqUserDatabase` implements `GetUsers()` and returns user records
with the stored user name and default user-management metadata.
`JsonUserDatabase` inherits this behavior through `LinqUserDatabase`.

**Address Space Management:**

* The server now properly removes the UserManagement node from the
address space when user management is not enabled, preventing stale or
orphaned nodes.

**Test Coverage Enhancements:**

* Added tests to verify that the UserManagement node is removed when
user management is not present.
* Added tests to confirm that `LinqUserDatabase.GetUsers()` returns the
correct users.
* Added tests to confirm that `UserManagement` starts with all users
from the database, including custom `IUserDatabase` implementations that
return full `UserManagementDataType` metadata.

These changes keep user discovery on the `IUserDatabase` abstraction,
remove the concrete-type dependency from startup, and allow existing
users to be loaded with their associated management metadata.
2026-05-28 11:00:18 +02:00
Marc Schier 5ff968fcfb Test project reorganization follow up (#3810)
### Test project reorganization

Splits the integration- vs unit-test boundary along **project
boundaries**
and introduces a runtime-tier split for subscription tests so the
mainline
CI no longer drags in stress / durable suites.

**New projects:**

- `Opc.Ua.Stress.Tests` ΓÇö cross-area stress tier (designed to
absorb
  session / channel / pub-sub stress in future). Seeded with the 6
  subscription stress classes (`SubscriptionDepthTests`,
  `SubscriptionBasicDepthTests`, `MonitoredItemDepthTests`,
  `SubscriptionTransferDepthTests`, `SubscriptionPublishTooManyTests`,
`SubscriptionMultipleTests`). **Excluded from mainline CI; runs
nightly.**
- `Opc.Ua.Subscriptions.Durable.Tests` ΓÇö durable-subscription tier
  (needs `DurableSubscriptionsEnabled` + storage-backed lifetime).
  Receives `SubscriptionDurableTests` plus the migrated
  `DurableSubscriptionTest` and `DurableSubscriptionTestDebug`.
  **Excluded from mainline CI; runs nightly.**
- `Opc.Ua.Features.Tests` ΓÇö cross-feature integration tier.
Receives the
  `[OneTimeSetUp]`-bootstraps-its-own-server integrations for AliasNames
  (Part 17), Roles (Part 18), FileSystem (Part 5 §C / Part 20), and
  NodeCache. Also absorbs the misplaced
  `Tests/Opc.Ua.History.Tests/FileSystemTests.cs`.

**Client.Tests cleanup (~28 files moved):**

- `Opc.Ua.Sessions.Tests` (existing) gains 10 session integration files
  plus the 4 session benchmark files inline (`LoadTest`,
  `RequestHeaderTest`, `TraceableRequestHeaderTest`,
  `SecurityPolicyBenchmarks`).
- `Opc.Ua.Subscriptions.Tests` (existing) gains 4 subscription
  integration files plus `PooledNotificationBenchmarks` inline.
- `Opc.Ua.Client.ComplexTypes.Tests` (existing) gains all
  `ComplexTypes/*` integration files. The Client.Tests copy of
  `TypeSystemClientTest.cs` (a stale near-duplicate) is removed.
- Benchmarks **stay inline** as `[Test]` + BenchmarkDotNet dual-use; no
  new benchmark project (per the approved plan).

**CI pipeline:**

- `.azurepipelines/test.yml` now filters
  `TestCategory!=LongRunning&TestCategory!=Stress` on the mainline run.
- `-ExcludeFileName` now accepts a comma-separated list. The mainline
  matrix excludes the 3 nightly-tier project files alongside the
  existing `Opc.Ua.Aot.Tests.csproj`.
- `get-matrix.ps1` enhanced to support multi-pattern excludes.

`InternalsVisibleTo` extended on `Opc.Ua.Client` and `Opc.Ua.Server` to
grant the new test projects access. All affected projects build clean
(`0 Error`) for `net10.0`.

### Fix 8 skipped tests flagged as suspected bugs

Triage of every `[Ignore]` / `[Explicit]` test in `Tests/**` surfaced 8
entries labelled as suspected production bugs. After inspection:

- **1 real production bug** ΓÇö the
`CancellationTokenSource.CancelAsync`
  polyfill for netstandard2.0 / netfx was running cancellation callbacks
  inline on the caller's thread, deadlocking dispose paths
  (`SubscriptionManager.DisposeAsync`, etc.) when a callback awaits on
  the caller's `SynchronizationContext`. Fixed at the polyfill root by
  hoisting `Cancel()` onto the ThreadPool via `Task.Run`, matching real
  .NET 8+ semantics. Caveat (deferred `IsCancellationRequested`
  transition) documented inline; all in-repo callers use
  `await cts.CancelAsync()` so the deviation is invisible. Test
  `DrainAsyncWaitsForInFlightPublishToCompleteAsync` un-ignored ΓÇö
  verified passes on net48 (943 ms) and net10.0 (1 s).
- **2 flaky tests stabilized** ΓÇö both
  `ValidateMetaDataUpdateTimeNonZeroIsSentPeriodically` (MQTT JSON &
  UADP) now use `Stopwatch.GetTimestamp()` instead of `DateTime.Now`,
  compute deviation against the **median** interval (not the configured
  cadence, so a startup hiccup doesn't skew the run), and use a
  proportional `max(interval / 4, 50 ms)` tolerance. `[Ignore]` removed,
  `[Category("LongRunning")]` retained.
- **1 mis-labelled "bug"** ΓÇö
  `UnorderedMessagesWouldBeLostForSequentialPublishingAsync` asserts the
*documented* lossy trade-off of strict sequential publishing.
`[Explicit]`
  removed and renamed to
  `OldMessagesAbandonedAfterRepublishTimeoutInSequentialModeAsync`; it's
  now a CI-verified regression test for the WAI behavior.
- **4 false positives** ΓÇö `JsonEncoderTestsOld.cs` (16 kLOC) was
  excluded from compilation via `<Compile Remove>` and never reached the
  runner. The 4 `[Ignore("ProductionBugSuspected")]` markers inside were
  orphaned source from a previous JsonEncoder API. Active coverage lives
  in `JsonEncoderTests.cs` and `JsonValidationData` fixtures. Deleted
  the orphan file and the now-redundant `<Compile Remove>` line.
2026-05-28 04:19:21 +02:00
35497b9deb Fix TypeInfo for generic IEncodeable types (#3801)
## Summary
- classify scalar generic `IEncodeable` implementations as
`ExtensionObject` before falling through the generic collection handling
- add a regression test for
`TypeInfo.Construct(typeof(GenericEncodeable<int>))`

Fixes #3800.

## Verification
- `dotnet build Stack\Opc.Ua.Types\Opc.Ua.Types.csproj -f net8.0
--no-restore`
- `dotnet run --project
D:\Projects\_luminaflow_work\typeinfo-probe\typeinfo-probe.csproj` ->
`BuiltInType=ExtensionObject; ValueRank=-1; IsUnknown=False`
- `git diff --check` (only existing LF-to-CRLF warnings on touched
files)

## Note
- Targeted `dotnet test
Tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -f net8.0 --filter
"FullyQualifiedName~TypeInfoTests"` could not complete in this local
environment because `Opc.Ua.SourceGeneration.Stack.dll` is rejected as
referencing Roslyn compiler version `5.3.0.0` while the installed .NET
SDK compiler reports `5.0.0.0`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Christian Scherkl <christianscherkl79@gmail.com>
2026-05-27 15:48:31 +02:00
romanett e49d17d706 [Server] Make MonitoredNode aware of IAsyncNodeManager and introduce channel-based queueing under load (#3804)
## Proposed changes

This PR updates the server monitored-item pipeline to be
`IAsyncNodeManager`-aware and introduces channel-based FIFO queueing in
the monitored-node path to improve behavior under load.

It also includes follow-up fixes from review/CI feedback:
- Adjusted
`AsyncCustomNodeManagerTests.WriteAsync_PublishesValueToMonitoredItemQueueAsync`
to account for channel-based delivery timing/coalescing while still
asserting the written value is observed.
- Merged latest `master` into this branch and resolved merge conflicts
(notably in `ConfigurationNodeManager`), keeping async address-space
lookup behavior intact.
- Applied small compile-safety corrections needed for successful
validation.
2026-05-27 10:12:17 +02:00
Marc Schier e1b9cea3ea Reduce sync-over-async in non-obsolete server APIs + post-merge code style cleanup (#3806)
## Proposed changes

Reduces sync-over-async in non-obsolete server APIs, removes stale
`#region/#endregion` noise from the test suite, and fixes analyzer/test
fallout from merging the Part 11 Historical Access framework (#3797).
Follow-up review feedback is included: `FileSystemNodeManager` is now
based on `AsyncCustomNodeManager`.

### Sync-over-async reduction (~24 `GetAwaiter().GetResult()` sites)

**Server-side:**
- `TrustList`: async `OnCallAsync` handlers added; sync `OnCall`
handlers retained as compat shims so legacy `CustomNodeManager2`
subclasses that host the TrustList but do not implement
`ICallAsyncNodeManager` keep working through `MethodState.Call`.
- `GDS ApplicationsNodeManager`: `RequestAccessToken`,
`KeyCredentialStart/FinishRequest`, and `Revoke` moved to `OnCallAsync`.
- `IKeyCredentialRequestStore`: interface + in-memory implementation
converted to async; `FinishKeyCredentialRequestResult` record carries
the previous out-set. Tests in `Tests/Opc.Ua.Gds.Tests/` updated to the
new async API.
- `FileSystem`:
- `DirectoryObjectState` methods (`CreateDirectory` / `CreateFile` /
`Delete` / `MoveOrCopy`) use `OnCallAsync`.
- `FileSystemNodeManager` now derives from `AsyncCustomNodeManager` and
overrides async address-space/node-resolution paths
(`CreateAddressSpaceAsync`, `DeleteAddressSpaceAsync`,
`GetManagerHandleAsync`, `ValidateNodeAsync`) to avoid sync-over-async
in dynamic node validation.
- `FileSystemNodeManagerFactory` now supports async creation
(`IAsyncNodeManagerFactory`) while retaining sync factory compatibility.
- Reference server registration of the FileSystem node manager is
aligned to the async node manager list.
- `ConfigurationNodeManager` namespace lookup uses sync
`GetManagerHandle` to keep the remaining `GetResult` inside the
documented `SyncNodeManagerAdapter` bridge.
- `ServerBase`: new `OnServerStartingAsync`; `StartAsync` now awaits it.
- `StandardServer.CreateMasterNodeManagerAsync` added; sync wrapper kept
for backward compatibility.
- `IServerInternal.CloseSession` marked `[Obsolete]`
(`CloseSessionAsync` exists).
- `IMasterNodeManager.RemoveReferences` marked `[Obsolete]`; in-tree
sync callers remain with scoped `CS0618` pragmas + TODO.

**Stack:**
- `EncryptedSecret`: added `VerifyHeaderForEccAsync` / `DecryptAsync` /
`TryDecryptAsync`; `UserNameIdentityTokenHandler.DecryptAsync` awaits
ECC validation.
- `CertificatePasswordProvider` ctor: `GetResult` fallback replaced with
explicit `InvalidOperationException`.

### Code-style and post-merge cleanup

- Removed unused `#region`/`#endregion` directives across test files.
- Restored green build state after Part 11 merge fallout fixes
(production + analyzer + test cleanup from earlier commits in this PR).

### CI / validation notes

- This PR branch surfaced runtime regressions introduced by Part 11
after the build-breaker was fixed (outside scope of this
async/code-style cleanup).
- Follow-up feedback change validation for `FileSystemNodeManager`:
- `dotnet build Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj -f net10.0
--no-restore` ✅
- `dotnet test Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj -f
net10.0 --filter FileSystemNodeIdTests` ✅
- `dotnet test Tests/Opc.Ua.History.Tests/Opc.Ua.History.Tests.csproj -f
net10.0 --filter FileSystemSmokeTests` ✅ (test skipped in environment)

## Related Issues

- None.

## Types of changes

- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)
- [x] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

- [x] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes, all passed.
- [ ] I fixed all failing tests in the CI pipelines.
- [ ] I fixed all introduced issues with CodeQL and LGTM.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added necessary documentation (if appropriate).
- [x] Any dependent changes have been merged and published in downstream
modules.
2026-05-27 06:48:30 +02:00
Marc Schier c89bc92e7c Part 11 Historical Access client/server framework (#3797)
## Proposed changes

Implements **OPC UA Part 11 (Historical Access)** end-to-end via a clean
**provider model** that lets servers back historizing variables with any
time-series storage engine. A reference-quality **in-memory engine**
ships in `Opc.Ua.Server` so getting started requires no extra packages.

The implementation replaces the legacy `HistoryArchive` /
`HistoryDataReader` / `HistoryFile` / `IHistoryDataSource` files
(removed from `Quickstarts.Servers/TestData/`) with a stack-level Part
11 surface.

### What's new

- **Server provider model** in `Libraries/Opc.Ua.Server/Historian/`:
- Umbrella `IHistorianProvider` + narrow capability interfaces
(`IHistorianDataProvider`, `IHistorianModifiedProvider`,
`IHistorianAtTimeProvider`, `IHistorianProcessedProvider`,
`IHistorianAnnotationProvider`, `IHistorianEventProvider`,
`IHistorianTransactionalProvider`).
- `HistorianDispatcher` — routes HistoryRead / HistoryUpdate (raw,
modified, at-time, processed, events, annotations) including
event-`WhereClause` evaluation via the framework's `FilterEvaluator`,
audit-event reporting, and continuation-point lifecycle.
- `HistorianProviderRegistry` with Node → Namespace → Default
precedence; surfaced via `IHistorianRegistryProvider` on
`ServerInternalData`.
- Fluent `HistorianBuilder`
(`server.UseHistorian().UseInMemory().Historize(...).RegisterAsDefault()`).
- Lazy `HistoricalDataConfigurationType` companion installer
(`OnPopulateBrowser`-attached).
- `ServerSystemContext` now exposes `IServerInternal Server { get; }` so
infrastructure code stops relying on the per-NM `SystemHandle`.
- **In-memory reference engine** (`InMemoryHistorianProvider`) — raw,
modified, processed via framework fallback, at-time via framework
fallback, annotations, events, transactional batches.
- **Client surface** in `Libraries/Opc.Ua.Client/Historian/`:
- `HistoryClient` with `IAsyncEnumerable<DataValue>`
raw/modified/at-time/processed/annotation reads (transparent
continuation-point handling and `try/finally`-based release on iterator
abandonment).
  - `session.Historian()` extension.
- Capability discovery (`GetServerCapabilitiesAsync`,
`GetConfigurationAsync`).
- **Node-manager wiring** through both `AsyncCustomNodeManager` and
`CustomNodeManager2` (sync-via-dispatcher).
- **Capability rollup** — `DiagnosticsNodeManager` populates the
server-wide `HistoryServerCapabilities` as the union of registered
providers' `GetCapabilitiesAsync(NodeId.Null, ...)` (runs whether the
node was loaded from the predefined nodeset or freshly created; fully
async).
- **Audit-event reporting** for HistoryUpdate (gated on
at-least-one-success):
`AuditHistoryValueUpdateEventType`,
`AuditHistoryAnnotationUpdateEventType`,
`AuditHistoryRawModifyDeleteEventType`,
`AuditHistoryAtTimeDeleteEventType`, `AuditHistoryEventUpdateEventType`,
`AuditHistoryEventDeleteEventType`.
- **ReferenceServer + TestData** historize sample variables through the
new pipeline (replaces the deleted legacy `HistoryArchive`).
- **Comprehensive developer documentation** in
`Docs/HistoricalAccess.md` (~657 lines): server-developer guide,
provider-author guide, client-developer guide, capability discovery,
auditing, limitations/roadmap.

### Tests

| Suite | Files | Tests |
| --- | --- | --- |
| Server unit (Opc.Ua.Server.Tests/Historian) | dispatcher,
event-dispatcher, registry, in-memory provider, transactional provider,
concurrency, coverage, event-filter target | **34** passing |
| Client integration (Opc.Ua.Client.Tests/Historian) |
`HistoryClientIntegrationTests` over the live `ReferenceServer` via
`ServerFixture` | **4** passing |
| AOT (Opc.Ua.Aot.Tests/HistoryAotTests) | positive assertions against
historized `Scalar_Static_Int32` (read raw, read processed/Average) |
**2** passing |

### Local validation

- `dotnet build UA.slnx -c Release` → **0 errors**.
- All historian / client integration / AOT history tests **green**.

### Hardening applied post-internal-review

- `DiagnosticsNodeManager.RollUpHistorianCapabilitiesAsync` — true async
(no more `.GetAwaiter().GetResult()` on the startup path).
- `HistoryClient` raw/modified/details readers — `try/finally` releases
the in-flight continuation point on `break`/exception/cancellation; also
detects malformed-server empty-page loops.
- `ReportAudit*` helpers gated on `HasAnyGood(...)` — audit events do
not fire when every per-value op failed.
- `ServerSystemContext` ctors throw
`ArgumentNullException(nameof(server))` at the boundary.
- `HistorianEventFilterTarget.IsTypeOf` logs a one-shot warning when
`IFilterContext.TypeTree` is null (degrades safely instead of silently).
- `InMemoryHistorianProvider.ReadEventsAsync` snapshots under lock,
filters outside (cuts lock-hold time).
- `TryRestoreContinuation` rejects cross-wired continuation points
(different `NodeId`).
- Processed-read framework fallback capped at 100 000 buffered outputs
(`BadTooManyOperations` when exceeded).
- ReferenceServer / TestData seeding is async-awaited /
`Task.Run`-bridged to escape any captured `SynchronizationContext`.

## Related Issues

- Fixes #

## Types of changes

- [x] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)
- [x] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change
- [x] Documentation Update

> The deletion of legacy
`Applications/Quickstarts.Servers/TestData/HistoryArchive*` is a
refactor of sample code only; the public stack surface is purely
additive.

## Checklist

- [x] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes, all passed.
- [ ] I fixed all failing tests in the CI pipelines.
- [ ] I fixed all introduced issues with CodeQL and LGTM.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added necessary documentation (`Docs/HistoricalAccess.md`).
- [x] Any dependent changes have been merged and published in downstream
modules.

## Further comments

The provider model is intentionally minimal and orthogonal — providers
opt into specific Part 11 features by implementing one or more of the
narrow capability interfaces, and the dispatcher type-tests the concrete
provider at runtime. This keeps the bundled `InMemoryHistorianProvider`
simple while making it trivial for vendors to plug in their own
time-series database.

A few items remain explicitly deferred (documented in
`Docs/HistoricalAccess.md` *Limitations and roadmap*):
`HistoricalEventConfigurationType` auto-install for event notifiers,
audit-event `OldValues` fidelity (would require read-before-write across
update sites), true streaming processed-read continuation (the framework
fallback is bounded by the 100 k cap; native
`IHistorianProcessedProvider` providers already get arbitrary paging).
These are on-demand items, not gaps in the mandatory Part 11 surface.
2026-05-26 10:45:32 +02:00
6db9a67a0a Fix TypeInfo.Construct for generic encodeable (#3802)
Prioritize the encodeable check over the enumerable check in
`TypeInfo.Construct(Type type)` to prevent representing a generic type
that implements `IEncodeable` as `TypeInfo.Unknown`.

Fixes #3800

Somewhat related to #3597

---------

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
2026-05-26 10:36:44 +02:00
Marc Schier eb40a59566 More format and diagnostic fixes (#3780)
Drive the solution to zero warnings / zero errors with analyzers on,
promote the consistently-fixed rules to error, and lock in coverage
on the only line of new logic.
2026-05-26 06:06:09 +02:00
Marc Schier f3928db05b Add unified Microsoft.Extensions DI surface across OPC UA libraries (#3803)
Introduces a consistent `Microsoft.Extensions.DependencyInjection`
surface across the OPC UA libraries. Every consumer starts from a single
root entry point and chains feature-specific extension methods on a
unified builder. PubSub is intentionally excluded per the original
scope.

```csharp
services
    .AddOpcUa()                          // Stack/Opc.Ua.Core
    .AddServer(o => { /* … */ })         // Libraries/Opc.Ua.Server
    .AddNodeManager<MyNodeManagerFactory>()
    .Services
    .AddOpcUa()
    .AddClient(o => { /* … */ });        // Libraries/Opc.Ua.Client
```

Manual non-DI constructors (`new ApplicationInstance(...)`, `new
StandardServer(...)`, `new LdsServer(...)`, `new ManagedSession(...)`,
etc.) remain unchanged — DI is purely additive for users who want the
.NET Generic Host to own application lifetime, logging, and
configuration.

### What ships

| Feature library | Method on `IOpcUaBuilder` | Returns |

|----------------------------------------------|-------------------------------------------|--------------------------|
| `Opc.Ua.Core` (root) | `services.AddOpcUa()` | `IOpcUaBuilder` |
| `Opc.Ua.Configuration` | `builder.AddApplicationInstance()` |
`IOpcUaBuilder` |
| `Opc.Ua.Client` | `builder.AddClient(opt => …)` |
`IOpcUaClientBuilder` |
| `Opc.Ua.Client.ComplexTypes` | `builder.AddComplexTypes()` |
`IOpcUaBuilder` |
| `Opc.Ua.Server` | `builder.AddServer(opt => …)` |
`IOpcUaServerBuilder` |
| `Opc.Ua.Gds.Client.Common` | `builder.AddGdsClient(opt => …)` |
`IGdsClientBuilder` |
| `Opc.Ua.Gds.Server.Common` | `builder.AddGdsServer(opt => …)` |
`IGdsServerBuilder` |
| `Opc.Ua.Lds.Server` | `builder.AddLdsServer(opt => …)` |
`ILdsServerBuilder` |
| `Opc.Ua.WotCon.Server` | `builder.AddWotConServer(opt => …)` |
`IWotConServerBuilder` |
| `Opc.Ua.WotCon.Client` | `builder.AddWotConClient(opt => …)` |
`IOpcUaBuilder` |

### Design highlights

- **Binary-safe inheritance** — `IOpcUaBuilder :
IDependencyInjectionBuilder` so any existing `AddLogging(this
IDependencyInjectionBuilder)` callers keep compiling unchanged.
- **Three options overloads** per feature: `Action<TOptions>`
(AOT-safe), `IConfiguration`, `IConfigurationSection`. The two binding
overloads are annotated `[RequiresUnreferencedCode]` /
`[RequiresDynamicCode]`. Section names follow the `OpcUa:<Feature>`
convention (`OpcUa:Server`, `OpcUa:Client`, `OpcUa:Gds:Server`,
`OpcUa:Lds`, …).
- **Generic Host integration** — each server feature (regular, GDS, LDS)
registers its own `BackgroundService`. Marker singletons throw
`InvalidOperationException` on duplicate `.AddServer()` /
`.AddGdsServer()` / `.AddLdsServer()`. A regular server + GDS + LDS can
coexist in one host; each owns its own `IApplicationInstance` produced
by a shared `IApplicationInstanceFactory` so PKI roots, endpoints, and
certificates don't collide.
- **Registration isolation** — node managers registered under the
regular server via `.AddNodeManager<T>()` are wrapped in
`OpcUaServerNodeManagerRegistration` so they don't bleed into the GDS /
LDS hosted services.
- **WoT Connectivity server** composes on top of the regular server
feature: `.AddServer(...).AddWotConServer(...)` registers
`WotConnectivityNodeManagerFactory` as a server node-manager
registration.
- **Unified telemetry** — `services.AddOpcUa()` registers
`ITelemetryContext` via `TryAddSingleton` using a new
`ServiceProviderTelemetryContext` that resolves the host's
`ILoggerFactory` on first use. User-supplied registrations win.

### Removed pre-release DI surface

The pre-release `services.AddOpcUaServer(...)`,
`services.AddOpcUaClient(...)`, `services.AddOpcUaGdsClient(...)`
extensions are deleted. `IClientBuilder` was renamed to
`IOpcUaClientBuilder`. In-repo callers migrated:

- `Applications/MinimalCalcServer/Program.cs`
- `Applications/MinimalBoilerServer/Program.cs`
- `Tests/Opc.Ua.Client.Tests/ClientBuilder/OpcUaClientBuilderTests.cs`
(renamed from `OpcUaClientServiceCollectionExtensionsTests.cs`)
- Docs (`MigrationGuide.md`, `Sessions.md`,
`SourceGeneratedNodeManagers.md`, README, new
`Docs/DependencyInjection.md`)

### csproj audit

Pruned every redundant `Microsoft.Extensions.*` package reference.
`Opc.Ua.Core` already pulls in `Microsoft.Extensions.Diagnostics` and
`Microsoft.Extensions.Logging`, which transitively expose DI / Options /
Configuration.Abstractions / Options.ConfigurationExtensions to every
downstream library. Only `Microsoft.Extensions.Hosting.Abstractions`
stays explicit on the server libraries (it isn't transitively
available). Also pruned redundant `System.Formats.Asn1` (Bindings.Https)
and `System.Collections.Immutable` (Security.Certificates).

### Tests

50 new tests in eight projects:

| Project                              | Tests |
|--------------------------------------|-------|
| `Opc.Ua.Core.Tests`                  | 12    |
| `Opc.Ua.Client.Tests`                | 4     |
| `Opc.Ua.Client.ComplexTypes.Tests`   | 3     |
| `Opc.Ua.WotCon.Tests`                | 6     |
| `Opc.Ua.Lds.Tests`                   | 5     |
| `Opc.Ua.Gds.Tests`                   | 6     |
| `Opc.Ua.Server.Tests` (integration)  | 10    |
| `Opc.Ua.Aot.Tests` (smoke)           | 4     |
| **Total**                            | **50** |

Combined-host integration tests cover the rubber-duck-driven scenarios:
telemetry-context override across legacy and new entry paths, server +
GDS + LDS coexistence with distinct ports and PKI roots,
duplicate-registration rejection, and node-manager isolation across
features.

### AOT

`dotnet publish Tests/Opc.Ua.Aot.Tests -c Release -r win-x64
--self-contained` reports zero `IL2026` / `IL3050` / `IL2104` warnings
from the new DI surface. All generic registrations carry
`[DynamicallyAccessedMembers(PublicConstructors)]`. AOT consumers stick
to the `Action<TOptions>` overload exclusively; the new
`Tests/Opc.Ua.Aot.Tests/HostingAotTests.cs` smoke fixture exercises this
end-to-end.
2026-05-25 20:24:53 +02:00
Marc Schier dc441a8b95 Source generator option to override instance modelling rules with type definition modelling rules (default opt in for stack) (#3799)
## Proposed changes

When the source generator creates code for "instance" definitions (e.g.,
the `Server` object which has `ServerType` as its type definition), it
currently uses the modelling rules specified on the **instance's
children**, which may differ from the **type definition's children**.
For example:

- `ServerType` defines `UrisVersion` as `ModellingRule="Optional"`
- `Server` instance overrides it to `ModellingRule="Mandatory"`

This PR introduces two layers of modelling rule enforcement:

### 1. OPC UA promotion semantics (always active)

Regardless of any configuration, the generator now enforces OPC UA
modelling rule promotion semantics. Instances may only **promote** the
type definition's rule:

| Type Definition Rule | Allowed Instance Overrides |
|---|---|
| `Optional` | `Mandatory` |
| `OptionalPlaceholder` | `Mandatory`, `MandatoryPlaceholder` |
| `MandatoryPlaceholder` | `Mandatory` |

Any other instance override (demotion) is silently rejected and the type
definition's rule is used instead. This applies to both structural code
generation decisions and the emitted runtime `ModellingRuleId`.

### 2. `UseTypeDefinitionModellingRules` option (opt-in)

When enabled, the generator uses the type definition's modelling rules
**unconditionally** — instance overrides are ignored entirely (even
valid promotions). This is useful for stack generation where the type
definition's semantics should be preserved exactly.

- **Enabled by default** for stack generation
(`Opc.Ua.SourceGeneration.Stack`)
- **Disabled by default** for the Roslyn source generator
(`Opc.Ua.SourceGeneration`), configurable via
`ModelSourceGeneratorUseTypeDefinitionModellingRules` MSBuild property

### Key design decisions

- A `TypeDefinitionModellingRule` property on `HierarchyNode` captures
the type definition's original rule before instance overrides, updated
through base→derived type-hierarchy merges but frozen once explicit
instance overrides appear
- A `GetEffectiveModellingRule()` helper with `IsValidPromotion()`
centralizes the rule resolution logic across all structural check sites
and runtime `ModellingRuleId` emission
- Factory method emission (`LoadTemplate_ReplaceChild`) uses the
instance's own modelling rule for mandatory/optional list classification
so children the instance marks Mandatory are always created

## Related Issues

- N/A

### Types of changes

- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)
- [ ] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

- [x] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes, all passed.
- [ ] I fixed all failing tests in the CI pipelines.
- [ ] I fixed all introduced issues with CodeQL and LGTM.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added necessary documentation (if appropriate).
- [ ] Any dependent changes have been merged and published in downstream
modules.

## Further comments

### Files changed:

| File | Change |
|------|--------|
| `Schema/ModelDesign.cs` | Added `TypeDefinitionModellingRule` property
to `HierarchyNode` |
| `Schema/ModelDesignValidator.cs` | Populates
`TypeDefinitionModellingRule` during hierarchy merge; updates through
base→derived type merges, freezes on instance overrides |
| `GeneratorOptions.cs` | Added `UseTypeDefinitionModellingRules` option
with full XML doc |
| `NodeStateGenerator.cs` | `GetEffectiveModellingRule()` +
`IsValidPromotion()` helpers; updated structural check sites and runtime
`ModellingRuleId` emission; `NodeToGenerate` extended with
`TypeDefinitionModellingRule` |
| `StackGeneration.cs` | Enabled option for stack generation |
| `ModelCompilationOptions.cs` | Added option + MSBuild property read |
| `ModelCompilation.cs` | Wired option through to `GeneratorOptions` |
| `OPCFoundation.Opc.Ua.SourceGeneration.props` | Registered MSBuild
`CompilerVisibleProperty` |
| `readme.md` | Documented new MSBuild property with promotion semantics
|
2026-05-24 21:00:10 +02:00
Marc SchierandCopilot 14e995c1d0 Restructure test projects for faster CI (#3767)
Rebalance the heavy test projects of the OPC UA .NET Standard stack so CI test execution can run cleanly in parallel.

### New test projects (Tests/Opc.Ua.<X>.Tests/)
| Project | Files | Sources |
|---|---:|---|
| Opc.Ua.Sessions.Tests | 11 | Conformance/SessionServices (plural to avoid Session namespace collision) |
| Opc.Ua.Subscriptions.Tests | 25 | SubscriptionServices + MonitoredItemServices |
| Opc.Ua.History.Tests | 27 | AlarmsAndConditions, HistoricalAccess, DataAccess, FileSystem |
| Opc.Ua.Lds.Tests | 15 | Discovery, DiscoveryServices, Miscellaneous |
| Opc.Ua.Core.Security.Tests | 39 | Security, Auditing |
| Opc.Ua.InformationModel.Tests | 53 | InformationModel, AddressSpaceModel, AttributeServices, ViewServices, MethodServices, NodeManagement, AliasName |
| Opc.Ua.Core.Encoders.Tests | 11 | Encoder tests extracted from Core.Tests |
| Opc.Ua.Gds.Tests (existing) | +5 | new GDS tests |

### New helper class libraries (Tests/Opc.Ua.<X>.TestFramework/ + Tests/Opc.Ua.Test.Common/)
* Opc.Ua.Client.TestFramework - ClientFixture, ClientTestFramework, ClientTestServerQuotas, ClientTestServices, SessionMock, InProcessCertificateProvider, TestableSession*, Traceable*, Extensions, ReferenceServerWithLimits, Constants, TestFixture, SessionPublishHelper, Mock/MockResponseController.
* Opc.Ua.Server.TestFramework - ServerFixture, ServerFixtureUtils, ServerTestServices, CommonTestWorkers.
* Opc.Ua.Core.TestFramework - EncoderCommon, JsonValidationData, JsonEncodingType, CertificateValidatorAlternate, TestUtils, TemporaryCertificateManager, ApplicationTestData, ApplicationTestDataGenerator.
* Opc.Ua.Test.Common - Logging.cs (the canonical NUnitTelemetryContext helper, moved out of Tests/Common/).

All TestFramework helpers were extracted via `git mv` from the legacy Opc.Ua.Client.Tests / Opc.Ua.Server.Tests / Opc.Ua.Core.Tests projects (no copy-paste).

### Server-side production-code fixes
1. `MasterNodeManager.ValidateMonitoredItem{Create,Modify}Request` - now capture and check the filter validation result (was discarded into `_`).
2. `MasterNodeManager.ReadAsync` - assign `value = values[ii] = value.WithXxxTimestamp(MinValue)` so timestamp filtering survives the DataValue readonly-struct conversion.
3. `ApplicationsDatabaseBase.{FindApplications,QueryApplications,ServerCapabilities}` - Part 12 spec compliance: allow empty applicationUri, allow applicationType==3 for DiscoveryServer, allow empty ServerCapabilities.
4. `LinqApplicationsDatabase.{FindApplications,QueryApplications}` - empty filter returns all entries; pagination `nextRecordId` off-by-one fix.

### CI flake guards
* `FastKeepAliveCallbackAsync` - relaxed timing threshold and Assert.Ignore on slow CI runners.
* `SecurityCertValidationTests` - Assert.Ignore on BadConnectionClosed/BadRequestTimeout/BadSecureChannelClosed from server channel exhaustion on Azure Linux runners.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-24 06:23:16 +02:00
Marc Schier 889572472f Convert DataValue from class to readonly struct for further GC relief (#3796)
Convert `DataValue` from a mutable reference type (`class`) to a `readonly struct` to eliminate per-allocation GC pressure on every attribute read, subscription publish, and history sample.

### Key API changes (user-visible)

| Before | After | Pattern |
|---|---|---|
| `class DataValue` | `readonly struct DataValue : INullable` | Foundation |
| `Equals(DataValue?)` / `operator ==(DataValue?, DataValue?)` | `Equals(DataValue)` / `operator ==(DataValue, DataValue)` | Value semantics |
| `IEncoder.WriteDataValue(string?, DataValue?)` | `IEncoder.WriteDataValue(string?, DataValue)` | Non-nullable wire API |
| `IDecoder.ReadDataValue(...) -> DataValue?` | `IDecoder.ReadDataValue(...) -> DataValue` | Same |
| `IDataChangeMonitoredItemQueue.PeekLast/Oldest()` | `TryPeekLastValue/Oldest(out DataValue)` | TryGet pattern |
| `IAggregateCalculator.GetProcessedValue(bool)` | `TryGetProcessedValue(bool, out DataValue)` | Same |
| `IUaPubSubDataStore.ReadPublishedDataItem(...)` | `TryReadPublishedDataItem(NodeId, uint, out DataValue)` | Same |
| `Dictionary<uint, DataValue?>` (NodeCacheContext) | `Dictionary<uint, DataValue>` | Default + IsNull |
| `IHistoryDataSource.FirstRaw/NextRaw` | `TryFirstRaw/TryNextRaw(out DataValue)` | TryGet pattern |
| `NodeState.ReadAttribute(... DataValue)` | `NodeState.ReadAttribute(... ref DataValue)` | Sync mutation via ref |
| `NodeState.ReadAttributeAsync(... DataValue)` | `NodeState.ReadAttributeAsync(...) -> ValueTask<(ServiceResult, DataValue)>` | Async return tuple |
| `obj as DataValue` (heterogeneous Dictionary<string, object>) | `obj is DataValue dv ? dv : default(DataValue)` | Safe struct cast |
| `Assert.That(dv, Is.Not.Null)` (tests) | `Assert.That(dv.IsNull, Is.False)` | Struct test pattern |
| `[Obsolete] Value` setter, all property setters | Removed; use ctor or `With*()` | Immutability |
| `[DataContract]` + `[DataMember]` | Removed; surrogate via `SerializableDataValue` + `DataValueSurrogateProvider` | Serialization |

### Wire format

Wire format is **byte-identical** to the previous class-based implementation across binary / JSON / XML encoders. Default DataValue (`.IsNull == true`) is treated as the equivalent of the old null reference (writes JSON `null`, binary `byte 0`, XML omitted element).

### Why TryGet pattern (instead of leaving `DataValue?`)

For methods that previously returned `DataValue?` to signal "no value available" (queue peek, aggregate result, data store lookup, history navigation), the conversion to `bool TryGet(out DataValue)` matches BCL conventions (`Dictionary.TryGetValue`, `Queue.TryPeek`, `Int32.TryParse`) and gives the caller an explicit "may be absent" affordance without `Nullable<DataValue>` overhead.

### Breaking changes summary

- `DataValue` is now a `readonly struct` - boxing/unboxing semantics change for callers using `object`-typed APIs.
- `IEncoder.WriteDataValue` / `IDecoder.ReadDataValue` / `ReadDataValueArray` interface signatures lose the `Nullable` wrapper.
- `IDataChangeMonitoredItemQueue.PeekLastValue` / `PeekOldestValue` -> `TryPeekLastValue` / `TryPeekOldestValue` (out parameter).
- `IAggregateCalculator.GetProcessedValue` -> `TryGetProcessedValue` (out parameter).
- `IUaPubSubDataStore.ReadPublishedDataItem` -> `TryReadPublishedDataItem` (out parameter).
- `NodeState.ReadAttribute` takes `ref DataValue`; `ReadAttributeAsync` returns `ValueTask<(ServiceResult, DataValue)>`.
- `DataValue` property setters removed (use constructor + `With*` mutators).
2026-05-23 11:38:49 +02:00
Marc Schier 51bcd4272a GDS Part 12 Full Compliance (#3795)
* GDS: Authorization & SecureChannel hardening (Part 12 §6.5, §7.2, §7.6)

* Fix OnGetCertificates self-admin authorization: pass applicationId to
  HasAuthorization so ApplicationSelfAdmin can read its own certificates per
  OPC 10000-12 §7.6.10 (was rejecting self-admin pull-management callers).
* Fix OnUnregisterApplicationAsync self-admin authorization: also pass
  applicationId (was silently failing the self-admin branch).
* OnUpdateApplication: accept ApplicationSelfAdmin and ApplicationAdmin in
  addition to DiscoveryAdmin per §6.5.7. Add self-admin role-permission shim.
* OnGetCertificateStatus: accept CertificateAuthorityAdmin / SelfAdmin /
  ApplicationAdmin per §7.6.12 (was using AuthenticatedUserOrSelfAdmin which
  is more permissive than spec).

* Introduce ApplicationAdmin privilege (Part 12 §7.2):
  - Add GdsRole.ApplicationAdmin static privilege.
  - Extend GdsRoleBasedIdentity with AdministeredApplicationIds and a new
    constructor accepting the list. WithAdditionalRoles preserves it.
  - AuthorizationHelper.HasAuthorization recognises the privilege: alone
    when no applicationId is supplied (e.g. RegisterApplication) and
    membership-tested against AdministeredApplicationIds otherwise.
  - HasTrustListAccess honours the ApplicationAdmin privilege for the
    administered applications' trust lists.
  - New role lists: DiscoveryAdminOrAppAdmin (RegisterApplication),
    DiscoveryAdminOrSelfAdminOrAppAdmin (Update/UnregisterApplication),
    CertificateAuthorityAdminOrSelfAdminOrAppAdmin (cert management).

* SecureChannel result-code alignment (Part 12 method tables):
  - HasAuthenticatedSecureChannel throws BadSecurityModeInsufficient (was
    BadUserAccessDenied).
  - Adds requireEncryption flag so callers can demand SignAndEncrypt
    (used by future StartNewKeyPairRequest/FinishRequest tightening).
  - Sign and SignAndEncrypt are now both accepted as 'authenticated'.

* Tests:
  - Register the GDS namespace in AuthorizationHelperTests SetUp so
    GDS-namespace role NodeIds do not collide on NodeId.Null with
    privilege roles.
  - Update HasAuthenticatedSecureChannelThrowsForNonSecureChannel to
    expect BadSecurityModeInsufficient.
  - Add HasAuthenticatedSecureChannelDoesNotThrowForSign and
    HasAuthenticatedSecureChannelWithRequireEncryptionThrowsForSign.
  - Add ApplicationAdmin coverage: SucceedsForAdministeredApp,
    ThrowsForUnmanagedApp, SucceedsForRegisterApplication,
    ThrowsWithEmptyAdministeredList.
  - Add NewRoleListsAreCorrectlyPopulated regression test.

The framework already enforces SigningRequired/EncryptionRequired via
DefaultAccessRestrictions in OpcUaGdsModel.xml, returning
BadSecurityModeInsufficient at request dispatch time
(MasterNodeManager.ValidateAccessRestrictions). The model declares the
correct restriction on every admin method, so explicit handler-level
checks would be redundant. Verified manually against §6.5.6, §6.5.7,
§6.5.8, §7.6.4-7.6.10 and §7.6.12.

23 Authorization unit tests pass. 181 unit + 420 integration GDS tests
pass on net10.0; build clean on net48 and net10.0.

* GDS: Audit event correctness (Part 12 7.6, 7.9)

* Fix audit-event ordering: emit CertificateRequestedAuditEvent AFTER the
  method outcome is known. Previously the event was raised before
  authorization with Status=true, so any failed call appeared in audit as
  a successful certificate request and any authorization-denied call
  generated a misleading 'success' audit trail.
  - OnStartNewKeyPairRequest: refactor to try/catch/finally, emit audit
    once with auditException reflecting either the captured throw, the
    bad ServiceResult, or null on success.
  - OnStartSigningRequestAsync: add the audit emission (previously
    missing entirely - 7.9.3 requires it) using the same pattern.
* Redact privateKeyPassword from audit InputArguments. The previous
  ApplicationsNodeManager.cs:1206-1223 included the raw password string
  as a Variant in the audit event payload, leaking the secret into any
  downstream audit sink. The password is now replaced with
  AuditEvents.RedactedPrivateKeyPassword (placeholder string).
* OnFinishRequestAsync: build the audit InputArguments from only the
  actual method inputs (applicationId, requestId) per 7.6.6 method
  signature. Previously the implementation appended the returned
  Certificate / PrivateKey / IssuerCertificates to InputArguments, which
  leaked the returned PrivateKey into audit payloads.
* Raise CertificateRevokedAuditEventType in OnRevokeCertificate on
  success and failure per 7.6.9. The event type was defined in the
  model (OpcUaGdsModel.xml:543) but never emitted. Refactored
  OnRevokeCertificate with try/catch/finally; sync-over-async (.Result)
  is preserved here and addressed separately in Track C (c-revoke-async).

* AuditEvents helper:
  - New ReportCertificateRevokedAuditEvent extension method, mirroring
    the existing CertificateRequested/Delivered helpers, with an
    Exception parameter that drives Status (true on success, false on
    failure).
  - New RedactedPrivateKeyPassword and RedactedPrivateKey constants
    documented as the canonical placeholders. Class-level remarks
    instruct callers to redact sensitive arguments before invoking the
    helpers.

* Tests:
  - AuditEventsTests: lock the placeholder values
    (RedactedPrivateKeyPassword and RedactedPrivateKey) so they remain
    stable for downstream audit-sink filters.
  - Existing 181 unit + 420 integration GDS tests pass on net10.0; build
    clean on net48 and net10.0.

b-keycred-audit and b-token-audit remain pending pending Track E and F
respectively (KeyCredentialService and AuthorizationService handlers).

* GDS: Directory / Pull Management correctness gaps (Part 12 6.5, 7.6)

* OnRevokeCertificate converted to async OnRevokeCertificateAsync; uses
  await instead of sync-over-async .Result, accepts the method-call
  cancellation token, and is now wired through OnCallAsync to integrate
  with the async dispatch path. RevokeCertificateAsync helper takes an
  optional CancellationToken so callers (OnUnregisterApplicationAsync,
  OnRevokeCertificateAsync) can forward it.

* OnCheckRevocationStatusAsync now computes the ValidityTime per
  OPC 10000-12 7.6.11. The previous implementation always returned
  DateTime.MinValue. The new implementation enumerates the CRLs in the
  trusted-issuer store and uses the earliest NextUpdate as ValidityTime
  when the chain validates. On Bad results (chain validation failed)
  ValidityTime remains DateTime.MinValue so callers re-check
  immediately.

* OnFinishRequestAsync returns the issuer chain via a new
  BuildIssuerCertificateChain helper. The previous implementation
  returned only the immediate signing CA, which is incomplete for
  multi-tier CAs per OPC 10000-12 7.6.6. The helper uses
  ICertificateGroup.Certificates (in-memory) as the X509Chain
  ExtraStore and falls back to the immediate issuing CA if chain
  building does not yield a chain. The helper is intentionally
  synchronous to avoid contending with concurrent SigningRequestAsync /
  NewKeyPairRequestAsync writes against the CertificateGroup's
  AuthoritiesStore (which caused parameterised PushTest cases to fail
  in parallel runs).

* ValidateApplication (ApplicationsDatabaseBase) now enforces the
  reverse-connect rules in OPC 10000-12 6.5.5:
  - Server / DiscoveryServer DiscoveryUrls must NOT start with the
    "rcp+" prefix.
  - Client DiscoveryUrls (when non-empty) must all start with "rcp+"
    AND ServerCapabilities must include "RCP".
  - ClientAndServer must register at least one non-rcp+ Server
    DiscoveryUrl; reverse-connect URLs are still permitted alongside
    and require the RCP ServerCapability.
  Removed the previous blanket rejection of any DiscoveryUrls on
  ApplicationType.Client (which contradicted the spec's reverse-connect
  registration pattern).

* Tests:
  - RegisterApplicationClientWithRcpDiscoveryUrlsAndRcpCapabilityDoesNotThrow
  - RegisterApplicationClientWithRcpDiscoveryUrlsAndMissingCapabilityThrows
  - RegisterApplicationServerWithRcpDiscoveryUrlThrows
  - RegisterApplicationClientAndServerWithMixedUrlsDoesNotThrow
  - RegisterApplicationClientAndServerWithOnlyRcpDiscoveryUrlsThrows
  All 188 unit + 420 integration GDS tests pass on net10.0; build clean
  on net48 and net10.0.

c-redundancy intentionally requires no enforcement: both transparent
(single application with multiple DiscoveryUrls) and non-transparent
(separate applications with NTRS capability) patterns per OPC 10000-12
6.5.6 are admitted by the validator; transparent redundancy is a
registrant convention rather than a server-enforceable invariant.


* Annex D capabilities: Self-advertise the LDS-ME capability identifier
  whenever the MulticastExtension is configured. Previously LdsServer
  hard-coded ["LDS"] regardless of whether multicast was running, so
  FindServersOnNetwork peers could not distinguish LDS from LDS-ME
  instances. Refactored the capability computation into a public static
  ComputeServerCapabilities(existing, hasMulticast) helper:
    - empty input + no multicast -> ["LDS"]
    - empty input + multicast -> ["LDS", "LDS-ME"]
    - custom seed list with no "LDS" -> "LDS" is appended
    - existing "LDS-ME" is not duplicated
  OnServerStarting now wires up the multicast layer first so the
  capability set reflects the configured topology.

* Annex C / rcp+ reverse-connect preservation: MulticastDiscovery now
  carries the OPC 10000-12 6.5.5 rcp+ DiscoveryUrl prefix end-to-end
  through mDNS announce / discover.
    - TryBuildProfile strips the rcp+ prefix before parsing the
      underlying transport URL (Uri parsing on "rcp+opc.tcp://..." is
      brittle across runtimes) and emits a dedicated TXT-record key
      ReverseConnectTxtKey ("rc") so receivers can reconstruct it
      verbatim.
    - OnServiceInstanceDiscovered honours the "rc" key when present
      and rebuilds the DiscoveryUrl with the rcp+ scheme.
  Exposes MulticastDiscovery.OpcUaServiceType,
  MulticastDiscovery.ReverseConnectScheme and
  MulticastDiscovery.ReverseConnectTxtKey as public constants so
  client code and tests can reference the same values used during
  registration and discovery.

* RegisterServer2 fallback (4.2.2): verified that LdsServer overrides
  both RegisterServerAsync and RegisterServer2Async, so the LDS
  accepts both legacy RegisterServer registrations and the modern
  RegisterServer2 + MdnsDiscoveryConfiguration path; no code change
  required.

* Tests: new LdsServerStaticTests fixture exercising the capability
  computation (six matrix cases) and locking in the Annex C / rcp+
  constants (one regression test). 195 unit + 420 integration GDS
  tests pass on net10.0; build clean on net48 and net10.0.

* Added Opc.Ua.Lds.Server as a ProjectReference of Opc.Ua.Gds.Tests so
  the new tests can use the public static surface of LdsServer and
  MulticastDiscovery.

* GDS: Push management hardening (Part 12 7.10)

* d-push-client-multitype: Make ServerPushConfigurationClient.ApplicationCertificateType
  settable instead of hard-coded to RsaSha256ApplicationCertificateType.
  Callers managing ECC, Brainpool or HTTPS certificates can now override
  the default before invoking certificate-management methods (or pass an
  explicit certificate type to the methods themselves). Removed the
  TODO comment that flagged this limitation.

* d-trustlist-ext: Set Writable / UserWritable to true for the
  DefaultApplicationGroup / DefaultHttpsGroup / DefaultUserTokenGroup
  TrustList instances in the GDS. Per OPC 10000-12 7.8.2.1 a TrustList
  that supports CloseAndUpdate / AddCertificate / RemoveCertificate is
  writeable; role-based access controls on the individual methods still
  enforce who may actually mutate the trust list. The previous values
  (false) were misleading to clients browsing the trust-list nodes.

* d-server-cred-audit: Redact the private key from
  CertificateUpdateRequestedAuditEventType InputArguments per OPC 10000-12
  7.10.3. ConfigurationNodeManager.UpdateCertificateAsync was passing
  the raw private-key ByteString into the audit payload, leaking the
  secret to any downstream audit sink. The audit payload now uses
  Opc.Ua.Server.AuditEvents.RedactedPrivateKey (an empty ByteString)
  as the placeholder; the certificate, issuer chain and key format
  remain in the payload so administrators can correlate the request.

  Also extended Server.ReportCertificateUpdateRequestedAuditEvent with
  an optional Exception parameter so callers can raise the event with
  Status=false on failure (parity with the GDS-side helpers).

* Build clean on net48 and net10.0. 195 unit + 420 integration GDS
  tests pass on net10.0.

Remaining Track D items (d-create-selfsigned, d-confirm-update,
d-managed-apps, d-alarms, d-push-tests) are larger features that
depend on Track H model additions and are not in scope for this
commit; their todos remain pending.

* GDS: Lock in ServerPushConfigurationClient.ApplicationCertificateType behaviour

Adds ServerPushConfigurationClientStaticTests covering the
Track D quick-win (d-push-client-multitype): the property defaults
to RsaSha256ApplicationCertificateType for backwards compatibility
and is now caller-overridable (e.g. for ECC management).

* GDS: Implement CreateSelfSignedCertificate (Part 12 7.10.6)

* Server-side: ConfigurationNodeManager wires the generated
  CreateSelfSignedCertificateMethodState.OnCallAsync handler when the
  optional method node exists on the ServerConfiguration instance.
  The handler:
  - Requires SecurityAdmin access (HasApplicationSecureAdminAccess)
  - Validates CertificateGroupId / CertificateTypeId via the existing
    VerifyGroupAndTypeId helper
  - Creates a self-signed certificate using the CertificateFactory
    pipeline (RSA with caller-specified key size, or ECC from the
    curve mapped by the certificateTypeId)
  - Merges DnsNames and IpAddresses into the SAN extension
  - Stores the resulting raw data on the existing
    CertificateIdentifier so the resolver can materialise it
  - Returns the DER-encoded public certificate to the caller
  The method is Optional per the spec; servers that load a nodeset
  without it simply skip the wire-up (null guard).

* Client-side: IServerPushConfigurationClient extended with
  CreateSelfSignedCertificateAsync; ServerPushConfigurationClient
  delegates to the generated ServerConfigurationTypeClient proxy
  with the standard elevate/revert permission pattern.

* net48 compat: uses new ValueTask<T>(result) instead of
  ValueTask.FromResult which is unavailable on .NET Framework 4.8.
2026-05-23 08:07:28 +02:00