102 Commits

Author SHA1 Message Date
Marc Schier ae34b02938 Fuzzing coverage expansion (Full Encoders / Certificates / Network) (#3867)
# Description

Broadens fuzz coverage across the highest-value untrusted-input surfaces
in the stack, refactors the per-area scaffolding so adding a new fuzz
area is a copy-paste-replace operation, and integrates everything into
`UA.slnx`, `Fuzzing/Fuzzing.md`, and `Docs/README.md`.

**Totals**: 143 files, +4961 / −289. Source plan:
`plans/27-fuzzing-coverage-expansion.md` (added in this PR).

### Shared scaffolding (refactor, no behaviour change)

* Extract generic NUnit replay harness from
`Encoders/Fuzz.Tests/EncoderTests.cs` into
`Fuzzing/common/Fuzz.Tests/{FuzzTargetTestsBase,FuzzTargetFunction,TestcaseAsset,TestAssetUtils}.cs`.
* Each area's `Fuzz.Tests` becomes a thin subclass binding its own
`FuzzableCode` type.
* Hardcoded testcase-suffix arrays replaced with `Testcases.*` directory
discovery at runtime.
* New `Fuzzing/scripts/fuzz-menu.ps1`: dynamic target menu enumerating
`FuzzableCode` statics via reflection — no more hardcoded target lists
in `libfuzz.bat` / `aflfuzz.sh`.

### Encoders area hardening

* **Bug fix**: `AflfuzzXmlEncoder` was re-encoding via `JsonEncoder`;
now uses `XmlEncoder` (the `LibfuzzXmlEncoder` variant was already
correct).
* Add idempotent JSON / XML targets mirroring the existing Binary
idempotent pattern.
* Add compact-JSON encoder option coverage (`JsonEncoderOptions.None`
alongside `Verbose`).
* New `FuzzableCode.BuiltInTypes.cs` (48 targets): `NodeId`,
`ExpandedNodeId`, `Variant`, `ExtensionObject`, `DataValue`,
`DiagnosticInfo`, `QualifiedName`, `LocalizedText` × Binary/JSON/XML
decoders.
* Enrich `Encoders.Testcases.cs` seed corpus with `BrowseRequest` +
`WriteRequest`.
* New `Fuzzing/dictionaries/binary.dict`.

### String parsers (folded into Encoders area)

* New `FuzzableCode.Parsers.cs` (12 targets): `NodeId.Parse`,
`ExpandedNodeId.Parse`, `RelativePathFormatter.Parse`,
`QualifiedName.Parse`, `NumericRange.Parse`, `Uuid` round-trip (both
afl-string and libFuzzer-span signatures).
* New `Fuzzing/dictionaries/nodeid.dict`.

### Certificates / CRL / ASN.1 (NEW area)

* New `Fuzzing/Certificates/{Fuzz,Fuzz.Tests,Fuzz.Tools}` referencing
`Opc.Ua.Security.Certificates` + `Opc.Ua.Core`.
* Targets (20 total): `X509CRL(byte[])` → `EnsureDecoded`,
`X509SubjectAltNameExtension` / `X509AuthorityKeyIdentifierExtension` /
`X509CrlNumberExtension` raw-ASN.1 decoders,
`PEMReader.ImportPublicKeysFromPEM` / `ImportRsaPrivateKeyFromPEM`,
`Pkcs10CertificationRequest`, `AsnUtils.ParseX509Blob` + `AsnReader`
sequence/integer helpers.
* `Certificates.Testcases.cs` builds an RSA cert / CRL / CSR with the
existing builders and emits raw DER + PEM seeds.
* New `Fuzzing/dictionaries/asn1.dict` (DER tags, context-class tags,
length-form sentinels, OID prefixes).

### Network area via `Opc.Ua.Bindings.Pcap` (NO Core changes, NEW area)

* New `Fuzzing/Network/{Fuzz,Fuzz.Tests,Fuzz.Tools}`, multi-targeted
`net8.0;net9.0;net10.0` to match the pcap binding matrix (excluded from
net48/net472).
* **Stateless targets**: `OpcUaFrameParser.Process` (TCP segment → UA-SC
chunk splitter), `TcpStreamReassembler.Process` (raw TCP reassembly),
`OfflineSecureChannel.ReadChunk` (UA-SC symmetric decrypt + verify via
`UaSCUaBinaryChannel.ReadSymmetricMessage` → every security profile
covered through the binding), `ServiceCallReassembler.Push` (chunk →
decoded service call).
* **Stateful libFuzzer-only targets**: `MockServerReplay` /
`MockClientReplay` drivers in a separate target list so cheap stateless
targets dominate throughput.
* Deterministic test-only `ChannelKeyMaterial` via
`SecurityPolicies.None`.
* `Network.Testcases.cs` emits synthetic Hello/Ack/Err/Rhe/MSG seeds via
`LoopbackFrameBuilder`. Live handshake recorder is out of scope for this
PR (degraded mode documented).
* `Network.Fuzz` host does **not** instantiate `NicCaptureSource` — the
AFL/libFuzzer process never opens raw sockets.
* New `Fuzzing/dictionaries/uasc.dict` (MSG/OPN/CLO/HEL/ACK/ERR/RHE
magic + chunk-type bytes + security-policy URI prefixes) and `tcp.dict`
(Ethernet/IPv4/TCP framing markers).

### `Opc.Ua.Core` UA-SC parse seam (complements 4a)

* New `Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` — `internal
static` helpers for the pre-crypto, pre-auth chunk surface NOT covered
by the pcap binding: `TryParseChunkHeader`, `ReadHelloMessage`,
`ReadAcknowledgeMessage`, `ReadErrorMessage`, `ReadReverseHelloMessage`,
`ReadAsymmetricMessageHeader`.
* **No production-code refactor** (Strategy B): the new internal helpers
are an independent parse seam, not a refactor of the entangled private
channel methods. Existing `ProcessHelloMessage` /
`ProcessReverseHelloMessage` / asym-header decode paths are untouched.
Behaviour is byte-identical (verified — Opc.Ua.Core.Tests Tcp subset 7/7
passing).
* Surface via `<InternalsVisibleTo Include="Network.Fuzz" />` (+
matching Tools and Tests entries because the linked
`FuzzableCode.Transport.cs` compiles from all three host projects).
* New `Fuzzing/Network/Fuzz/FuzzableCode.Transport.cs` (12 targets) +
`Fuzzing/Network/Fuzz.Tools/Transport.Testcases.cs` (seeds under
`Testcases.Tcp.*`).

### Integration

* `UA.slnx`: add the 6 new fuzz projects, 4 new shared harness `.cs`
files, 5 new dictionaries, and the new `fuzz-menu.ps1` script — all
under the right `/Fuzzing/<Area>/` solution folders.
* Rewrite `Fuzzing/Fuzzing.md`: areas table, per-area dictionaries,
dynamic target-menu, step-by-step "how to add a new fuzz area" recipe,
Network area's replay-driven seed pipeline + key-material discipline,
pcap binding dependency note.
* Cross-link from `Docs/README.md` to `Fuzzing/Fuzzing.md`.
* Fix `.Order()` → `.OrderBy()` in shared scaffolding
(`.Order(IComparer)` is net7+; the harness compiles on net48/net472 for
the Encoders + Certificates test matrices).

## Verification

| Project | TFM | Tests | Result |
|---|---|---|---|
| `Encoders.Fuzz.Tests` | net10.0 | 4131 | ✅ 0 failed |
| `Certificates.Fuzz.Tests` | net10.0 | 220 | ✅ 0 failed |
| `Network.Fuzz.Tests` | net10.0 | 330 | ✅ 0 failed |
| **Total** |  | **4681** | **✅ 0 failed** |

* Full `dotnet build UA.slnx` (all TFMs, including
net48/net472/net8/net9/net10): **0 errors**, warning count unchanged
from baseline.
* `Network.Fuzz` multi-TFM build: **net8.0 + net9.0 + net10.0** all
green.
* `Opc.Ua.Core` build: **0 errors, 0 warnings** after Phase 4b seam
addition.

## Notes for reviewers

* **Base branch**: `channelrefinements2`, not `master`. The pcap binding
(`Stack/Opc.Ua.Bindings.Pcap`) only exists on the channelrefinements
line.
* **Real keylog / key material**: never committed. All Network seed
corpora use deterministic material generated from fixture test
certificates via the binding's own replay helpers.
* **`.der` test assets**: force-added with `git add -f` (consistent with
the existing intentional exception at
`Tests/Opc.Ua.Security.Certificates.Tests/Assets/OPC Foundation Root CA
[...].der`); the repo-wide `*.der` gitignore otherwise excludes them.
* **Out of scope** (documented as fold-ins for future work):
`PcapFileReader` / `PcapNgFileWriter` / `UaKeyLogTextReader` /
`UaKeyLogJsonReader` defensive parser fuzzing; live handshake recorder
for `Network.Testcases.cs`; real loopback replay drivers (current
implementation uses synthetic replay).

## 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. _(Fuzz
suites verified on net10.0: 4681/4681 passing. Full UA.slnx build green
on all TFMs. Full repo-wide test run not yet executed on net48 —
pre-existing tests unaffected by these changes; only
`Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageParsers.cs` is new and is gated
behind `<InternalsVisibleTo>`.)_
- [ ] 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:15:28 +02:00
Marc Schier b32a6a73b3 PR feedback: reword security-bar prose, drop SAST paragraph, collapse empty doc lines, add /// <inheritdoc/> on EncryptedKeyLogStream overrides 2026-06-11 15:06:48 +02:00
Marc Schier e3e7134d56 Merge branch 'channelrefinements' into channelrefinements2
Resolved NodeSetExportTools.cs conflict by adopting base branch's ResolveExportPath / ExportRoot abstraction (configurable via OPCUA_MCP_EXPORT_ROOT env var) and removing my redundant ResolveAndValidateExportPath helper + NodeSetExportPathValidationTests.cs. Base implementation is functionally a superset of F19's hard-coded LocalAppData approach.
2026-06-11 13:24:59 +02:00
Marc Schier e8afa4237b Security hardening: 25-finding pass against SFI / SDL / OPC UA Part 2 baseline
Addresses 25 of 39 findings from a branch security review (assessment 5).

CRITICAL: F1 gate MCP key-disclosure tools, F2 0600 keylog/pcap files, F3 per-user BaseFolder, F16 ChannelKeyMaterial IDisposable + ZeroMemory, F17 PcapFileReader MaxPacketBytes (64MB) bound, F18 FlowBuffer MaxBufferBytes (256MB) cap, F19 NodeSet export path validation, F20 ReplaySession listenPort range.

HIGH: F4 IPcapAuditSink + LoggerPcapAuditSink + rate-limited frame-capture audit, F5 AES-256-GCM EncryptedKeyLogStream + SessionKeyManager (sibling .key file 0600), F14 IKeyEscrowProvider abstraction + DiskKeyEscrowProvider default (DI TryAddSingleton so consumers can route to KMS), F21 ChannelManagerOptions.MaxChannels (256) cap, F22 CaptureSessionManager.SessionFolder path-traversal guard, F23 PacketDecodeTools pcapPath/keyLogPath validation, F24 replay_pcap speed validation (NaN/Inf/<=0), F25 explicit permissions blocks on .github/workflows.

MEDIUM: F6 OnTokenActivated [EditorBrowsable(Never)] + key-disclosure doc, F7 IDiagnosticsChannelMutation accessor + stack-walk caller guard (replaces internal OfflineLoadTokens), F8 PcapOptions.AllowMockClientReplay + AllowedReplayEndpoints, F9 PcapFileWriter / UaKeyLog*Writer rotation (.NNN suffix) with MaxBytes / MaxArtifactsPerSession, F13 keylog-isolation gap acknowledgment doc, F15 HashChainedAuditFileSink (HMAC-chained JSONL ledger, VerifyChain), F11 replay URL scheme allow-list (opc.tcp/opc.tcps/opc.https).

LOW/INFO: F10 SBOM/CVE governance comments on PacketDotNet/SharpPcap + Docs/PacketCapture.md, F12 Security model section at top of PacketCapture.md.

Tests: 100+ new NUnit tests across Audit/, Capture/, Frame/, KeyLog/, McpServerTools/, Replay/, Stack/Tcp/, DependencyInjection/. All file-mode/HMAC-chain/encryption tests are [Platform(Linux,MacOSX)]-gated where Unix-specific.

Build: 0 warnings / 0 errors on Stack/Opc.Ua.Bindings.Pcap for net8.0 and net9.0. net10.0 build is environmentally blocked by a corrupted local SDK 10.0.300 workload manifest (microsoft.net.workload.mono.toolchain.net6 10.0.108 requires Emscripten.net6 >=10.0.108, only 9.0.17 installed); requires 'dotnet workload repair' from an elevated shell to validate the third TFM. No tests were run pending the SDK fix. 5 CA1508/CA2000 false positives suppressed with documented #pragma blocks (ownership transfer in escrow factory / IAsyncEnumerable iterator / unreachable catch path).

Deferred to follow-up GitHub issues: 9 MEDIUM (M-SEC-07/09/10/11/12/13/16/22/31 — exception sanitization, reconnect defaults, cert-rotation races, nonce-suppress deprecation, parser hardening) and 10 LOW/INFO (L-SEC-14/15/17/23/24/33/34/35/36/37 — handler cleanup, EventSource endpoint masking, key-log schema validation, EOL deps, SAST). Full reasoning in session plan.md Round 10.
2026-06-11 13:10:09 +02:00
Marc Schier 04f3d12fa8 [security] Bound the participant metric tag to per-kind cardinality (MEDIUM-4)
plans/security-assessment.md MEDIUM-4 (unbounded metric cardinality):

opcua.channel.participant.timeout.count and
opcua.channel.participant.recreate.count emit a "participant" tag
whose value was the full per-instance IReconnectParticipant.Id. Two
problems:
  - ClientChannelReconnectParticipant uses
    "{idPrefix}-{Guid.NewGuid():N}" — the suffix made every instance
    permanent in cardinality-retaining metric backends (Prometheus,
    OTLP, Application Insights).
  - Session.ChannelManager.cs:45 used a bare Guid with NO prefix at
    all, so the metric tag was a 32-hex-char string with no way for
    operators to bucket the data.

Fix:
  - Session.ChannelManager.cs:45 — prefix the GUID with "Session-" so
    Session participants follow the same prefix-then-suffix shape as
    ClientChannelReconnectParticipant.
  - ClientChannelManagerMetrics — add a GetParticipantKind helper
    that returns everything before the first '-' (or the full id if
    no '-'). Use it in CreateEndpointParticipantTags and
    CreateEndpointParticipantSuccessTags so the metric tag stays at
    per-kind cardinality ("Session", "Client", …).
  - Full per-instance id continues to flow through Activity tags and
    EventSource events (separate code path in
    ClientChannelManagerDiagnostics) so distributed traces remain
    correlatable.
  - Docs/Sessions.md callout updated to describe the now-bounded
    behavior (was a warning before, now describes the implemented
    semantic and the convention required of custom participants).

Build verified: Stack/Opc.Ua.Core, Libraries/Opc.Ua.Client net10.0 —
0 W, 0 E.
2026-06-11 12:26:54 +02:00
Marc Schier 2be0f5c4d5 [docs] Document the secure-by-default DI HttpClient fallback and the channel-manager diagnostics surface contract
Docs/Sessions.md:
  - New subsection "HTTPS factory + OPC UA cert validation:
    secure-by-default fallback" under § 4. Explains the two
    HttpsTransportChannel construction paths, why the factory is
    bypassed when a CertificateValidator is configured, the one-time
    LogWarning, and how a consumer can keep both Polly resilience AND
    OPC UA cert validation by registering the named HttpClient with
    ConfigurePrimaryHttpMessageHandler that wires OPC UA validation
    in themselves.
  - New subsection "Diagnostics surface contract — what tags and
    EventSource fields carry". Codifies what is allowed on Activity
    tags / EventSource events (StatusCode, SymbolicId, LocalizedText
    only) vs what stays in local ILogger.LogDebug (AdditionalInfo,
    inner exception data). Includes the bounded metric tag set with
    enumerated outcome / reason values, the correct
    transient-failure spelling, and a callout that the participant
    tag carries the full per-instance ID (cardinality is bounded by
    live participant count, but workloads with churn should rewrite
    the suffix at the OTel processor).

Docs/DependencyInjection.md:
  - "Channel manager" section adds a security trade-off callout
    pointing operators to the new Sessions.md HTTPS-secure-fallback
    section.
2026-06-11 10:48:45 +02:00
Marc Schier 297f71cfbe Merge branch 'channelrefinements' into channelrefinements2 2026-06-10 10:10:33 +02:00
Marc Schier 392ad0d3f6 Merge origin/master into channelrefinements
Conflict resolved in Libraries/Opc.Ua.Client/Session/ConnectionStateMachine.cs:
adjacent doc-comment edits. Kept channelrefinements' new
<maxTotalReconnectTime> param doc and adopted master's shorter
<see cref="TimeProvider"/> form (System namespace already imported).

Verified post-merge:
  - Libraries/Opc.Ua.Client: net10.0 build clean (0 W, 0 E)
  - Applications/Quickstarts.Servers: net10.0 build clean (inline NodeId
    fix for SecurityGroups/KeyPushTargets in
    ReferenceServerConfigurationNodeManager survived auto-merge)
  - Tests/Opc.Ua.Client.Tests: net10.0 build clean (only pre-existing
    master CA2000 warnings in StaticIssuerKeyResolverTests)
2026-06-10 09:03:00 +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 0597f069eb PR feedback: multi-target net8/9/10, wire MaxActiveSessions, restore bindings + async dispose in tests
* Pcap nuget now multi-targets net8.0;net9.0;net10.0 to match PR description claim
* Rename CaptureSessionManager.MaxActiveSessions const -> DefaultMaxActiveSessions; add instance property; new 4-arg ctor that accepts an override
* AddOpcUaBindingsPcap now passes PcapOptions.MaxActiveSessions through to CaptureSessionManager (was ignored)
* Fix stale Opc.Ua.Diagnostics.Pcap doc reference -> Opc.Ua.Bindings.Pcap
* Docs/McpServer.md: add stop_replay + list_replays rows to tool tables
* PcapBindingsTests + PcapServiceCollectionExtensionsTests: SetUp/TearDown to snapshot+restore the process-wide opc.tcp transport binding
* DefaultCaptureSourceFactoryTests: convert 3 sync-over-async tests to async with await using
* Add new test asserting PcapOptions.MaxActiveSessions actually flows into the resolved CaptureSessionManager
2026-06-09 09:22:45 +02:00
Marc Schier f7366ada6f Merge branch 'channelrefinements' into channelrefinements2 2026-06-08 19:16:06 +02:00
Marc Schier 81acd947a6 Close channel-manager production gaps and eliminate Tests/.../Gaps/
Closes the three carry-forward gaps documented in
Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ (formerly each documented via
an [Explicit] failing test). The realigned tests are relocated to
Channels/Contract/, the [Category("Gaps")] disappears, and the
Gaps/ folder is removed entirely.

G1. Faulted-entry recovery
  Already closed implicitly by Phase E SwapFaultedEntryAsync (PR #3852).
  Test realigned from "expects BadSecureChannelClosed throw" to
  "auto-resets on next ReconnectAsync and reaches Ready" and renamed
  FaultedEntryAutoResetsOnNextReconnectAsync. New file location:
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/FaultedEntryRecoveryTests.cs.
  No production change required; this was a test-vs-design alignment.

G2. Bounded participant timeout
  New ParticipantTimeout on IChannelReconnectPolicy. Default
  Timeout.InfiniteTimeSpan via DIM on net8+/netstandard2.1; legacy TFMs
  opt in via IParticipantTimeoutPolicy (mirrors the existing
  IBudgetAwareChannelReconnectPolicy pattern). ExponentialBackoffChannel-
  ReconnectPolicy sets a sensible default of 30 seconds.
  ChannelEntry.ReactivateParticipantsAsync wraps the participant's
  OnReconnectAsync in WaitAsync(timeout, ct); on timeout the participant
  is reported as TransientFailure for this cycle (the channel-level
  retry policy retries normally; eventually escalates to Faulted via
  the existing MaxAttempts path). A new participant.timeout.count
  metric is emitted per timeout.
  Test realigned from "documents indefinite hang" to "times out within
  bounded wait and surfaces as TransientFailure", renamed
  HungParticipantTimesOutAfterBoundedWaitAsync, and merged into
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/HungParticipantTests.cs.
  The sibling Contract/HungParticipantTests.HungParticipantBlocks-
  ReconnectIndefinitely is also realigned + renamed to
  HungParticipantTimesOutAndOtherParticipantsRecoverAsync. A new
  positive contract test (BoundedParticipantTimeoutHonorsTimeoutAsync)
  guards against false positives. A new live-server integration test
  exercises the timeout via a real ManagedSession in
  Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
  Tests.cs.

G3. RequiresSessionRecreate plumbing
  New RecreateAsync(ct) on IReconnectParticipant. Default no-op via DIM
  on net8+/netstandard2.1; legacy TFMs opt in via
  IRecreateAwareReconnectParticipant. ChannelEntry's switch on
  RequiresSessionRecreate now fire-and-forgets a DispatchRecreate(participant)
  task that invokes RecreateAsync; the manager does NOT block its
  transition to Ready waiting for the recreate (matches the existing
  enum doc that says "the participant is responsible for completing
  its own recreation out of band"). New participant.recreate.count
  + participant.recreate.failure.count metrics emit per dispatch.
  Session.ChannelManager.cs (the Session's IReconnectParticipant
  adapter) overrides RecreateAsync to delegate to the existing
  Session.RecreateInPlaceAsync — this is the actual "wired-through"
  deliverable flagged as PR #3852 carry-forward.
  Test realigned from "documents that nothing is invoked" to "fire-
  and-forget invokes RecreateAsync exactly once", renamed
  RequiresSessionRecreateInvokesRecreateAsync, moved to
  Tests/Opc.Ua.Stress.Tests/Channels/Contract/RecreateDispatchTests.cs.
  The 1 remaining [Ignore]d integration test from PR #3852 is replaced
  by a positive integration test in
  Tests/Opc.Ua.Sessions.Tests/ChannelManagerSessionLifecycleIntegration-
  Tests.cs that simulates BadSessionIdInvalid on reactivation.

Cleanup
  - GapTestBase.cs deleted; unique helpers folded into ContractTestBase.
  - Tests/Opc.Ua.Stress.Tests/Channels/Gaps/ folder deleted.
  - [Category("Gaps")] removed from the codebase.
  - Tests/Opc.Ua.Stress.Tests/README.md "category overview" loses the
    [Explicit] / L5 / Channels/Gaps/ row and the "Add known production
    gaps to Channels/Gaps/" guidance bullet.
  - Docs/Sessions.md "Participant model" subsection documents the new
    RecreateAsync callback and ParticipantTimeout policy property.
  - Pre-existing build-blocker in Applications/Quickstarts.Servers/
    ReferenceServer/ReferenceServerConfigurationNodeManager.cs
    (ObjectIds.SecurityGroups / KeyPushTargets are not on the curated
    Stack/Opc.Ua.Types/Internal/ObjectIds.cs surface) is replaced with
    inline NodeId literals (i=15443 / i=25440) per the docstring above
    the array. This was a separate fix required for the merged tree
    to build end-to-end.

Verified locally on net10.0:
  - Full UA.slnx Release build (CodeQL parity): 0 errors.
  - Tests/Opc.Ua.Stress.Tests Contract filter (faulted recovery,
    recreate dispatch, hung participant, participant result
    aggregation): 9/9 passed.
  - Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed.
  - Tests/Opc.Ua.Sessions.Tests ChannelManager filter: 9/9 passed
    (was 7/7+1 skipped before; the G3 RequiresSessionRecreate
    integration test that was carry-forward [Ignore] is now
    implemented and counted as a pass).
  - No remaining Gaps namespace / Category("Gaps") / GapTestBase
    references anywhere in the tree.
2026-06-08 18:12:18 +02:00
Marc Schier f6064e556e Merge branch 'channelrefinements' into channelrefinements2 2026-06-08 12:20:32 +02:00
Marc Schier c23f477bc7 Merge branch 'master' into channelrefinements 2026-06-08 12:20:15 +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 Schier a3a4dc2703 Merge origin/capture into channelrefinements2 (round 2)
Brings in three new commits on capture:
- 117f2212d Simplify token-activated subscription (channel is always ISecureChannel) (CA1508)
- f07501e0e Merge origin/master into capture
- daf3c8a8b Address PR review feedback (3 unresolved threads)

Auto-merged cleanly (no conflicts) — my "Integration with the central
channel manager" section in Docs/PacketCapture.md was preserved
unchanged alongside the capture-branch documentation tweaks (McpServer.md,
MigrationGuide.md, README.md, deletion of PacketCaptureFileFormats.md).

Folded in one nullability fix to make McpServer build:

- Applications/McpServer/Tools/PacketDecodeTools.cs:
  CreateActiveChannelInfo assigned EndpointDescription.EndpointUrl and
  .SecurityPolicyUri (both nullable strings) to ActiveChannelInfo's
  non-nullable string init properties, triggering CS8601 under the
  capture branch's strict nullability settings. Coalesce to string.Empty
  to match the default-init pattern used by ActiveChannelInfo itself.

Verified locally on net10.0:
- Stack/Opc.Ua.Bindings.Pcap build: 0 warnings, 0 errors.
- Applications/McpServer build: 0 warnings, 0 errors.
- Tests/Opc.Ua.Bindings.Pcap.Tests: 53/53 passed.
- Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed.
2026-06-08 11:26:32 +02:00
Marc Schier 5010d421c6 docs: cross-reference Pcap binding integration with IClientChannelManager
Add a dedicated 'Integration with the central channel manager' section
to Docs/PacketCapture.md spelling out the three properties of the
channel manager (sharing, transparent reconnect, faulted-entry swap)
that flow through to capture for free, plus the standard DI
registration order. Update the in-process-client quick-start to show
the channel manager registered alongside the Pcap binding.

Add a back-link from Docs/Sessions.md 'See also' so readers of the
channel-manager doc discover the capture composition.
2026-06-08 10:03:32 +02:00
Marc Schier 87dcfc10cf Merge origin/capture into channelrefinements2
Brings the packet-capture / dissection / replay engine
(Opc.Ua.Bindings.Pcap) onto the channel-refinements branch so it
composes cleanly with the central IClientChannelManager work landed in
PR #3852 (channelrefinements).

Conflicts resolved:
- Applications/McpServer/Program.cs: union of both ConfigureServices
  changes — registers AddOpcUa().AddClient() (channel manager) AND
  AddOpcUaBindingsPcap() / Formatters / Replay (capture). Channel
  manager picks up the capture binding automatically via the shared
  TransportBindings.Channels registry.
- Stack/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs: keep both
  m_sessionCount (channelrefinements2) and m_tokenActivated event
  (origin/capture). Server-side capture relies on
  TcpListenerChannel.OnTokenActivated to snapshot per-channel
  ChannelToken keys for offline decoding.
- UA.slnx: keep both Opc.Ua.Di.Tests and Opc.Ua.Bindings.Pcap.Tests
  project entries.

Pre-existing fix folded in (build-blocker introduced earlier on
channelrefinements2, not by the capture merge):
- Applications/Quickstarts.Servers/ReferenceServer/ReferenceServerConfigurationNodeManager.cs
  s_optionalPubSubFolders previously referenced ObjectIds.SecurityGroups
  and ObjectIds.KeyPushTargets, neither of which exists on the curated
  Stack/Opc.Ua.Types/Internal/ObjectIds.cs surface. Inlined the
  NodeId literals (i=15443 / i=25440) per the docstring above the
  array so the merged tree builds end-to-end across all TFMs and
  every transitive test project (Tests/Opc.Ua.Client.Tests,
  Tests/Opc.Ua.Sessions.Tests, etc.) compiles.

Architectural fit ("take advantage of all the changes you made here"):

1. Pcap integrates with the central channel manager via the global
   TransportBindings.Channels registry. ClientChannelManager.ctor
   accepts an injected ITransportChannelBindings? that defaults to the
   global registry, so the Pcap binding installed by
   AddOpcUaBindingsPcap is automatically picked up by every channel
   created through the manager. No code wiring needed — the
   composition is pure layering.

2. Channel sharing transparently includes capture. When multiple
   participants share a single IManagedTransportChannel (the F1
   refcount + key-equivalence work), the underlying CapturingMessageSocket
   captures all participant traffic on the same wire, automatically.

3. Transparent reconnect preserves the capture session. The F8
   diagnostics events on ClientChannelManager (state-change /
   close notifications) and the Phase E SwapFaultedEntryAsync swap
   path produce fresh ChannelEntry instances under the same
   ManagedChannelKey — IChannelCaptureRegistry is keyed by channel
   identity, so a swap rolls the active observer into the new entry
   and the capture session continues recording across the
   reconnect / faulted-swap cycle.

4. MCP server composes both: services.AddOpcUa().AddClient() (channel
   manager + DI), services.AddSingleton<OpcUaSessionManager>(),
   services.AddOpcUaBindingsPcap() (capture binding). OpcUaSessionManager
   resolves IClientChannelManager from IServiceProvider; the Pcap tools
   (Tools/PacketCapture/Decode/ReplayTools.cs) resolve
   CaptureSessionManager and IChannelCaptureRegistry from the same
   provider.

Verified locally on net10.0:
- Stack/Opc.Ua.Bindings.Pcap build: 0 warnings, 0 errors.
- Libraries/Opc.Ua.Client build (channel manager): 0 warnings, 0 errors.
- Tests/Opc.Ua.Bindings.Pcap.Tests: 53/53 passed.
- Tests/Opc.Ua.Client.Tests ClientChannelManager filter: 29/29 passed.
- Tests/Opc.Ua.Sessions.Tests ChannelManager filter: 7/7 passed,
  1 skipped (RequiresSessionRecreate carry-forward).
- Applications/McpServer build: 0 warnings, 0 errors.
2026-06-08 10:01:45 +02:00
Marc Schier f07501e0ec Merge origin/master into capture 2026-06-08 08:22:37 +02:00
Marc Schier daf3c8a8b2 Address PR review feedback (3 unresolved threads)
* Docs/PacketCapture.md: removed the duplicate 'MCP tool reference' section, moved into Docs/McpServer.md where the rest of the MCP tool surface lives.
* Docs/PacketCaptureFileFormats.md: inlined into Docs/PacketCapture.md (as 'File format reference' section) and deleted the standalone file. Removed the link from Docs/README.md.
* Docs/MigrationGuide.md: removed the orphaned 'Packet capture diagnostics hooks' TOC entry (the corresponding section was already removed in 67e803c, the TOC entry was missed).
2026-06-08 08:15:00 +02:00
Marc Schier 788567ad75 Merge branch 'master' of https://github.com/OPCFoundation/UA-.NETStandard into channelrefinements 2026-06-08 07:49:36 +02:00
Marc Schier c781514ad5 Refactor pcap diagnostics: decorator binding + rename to Opc.Ua.Bindings.Pcap
Three changes addressing the design feedback on PR #3855:

1. Renamed Stack/Opc.Ua.Diagnostics.Pcap -> Stack/Opc.Ua.Bindings.Pcap (and the corresponding test project). PackageId, AssemblyName and RootNamespace updated to Opc.Ua.Bindings.Pcap; references in McpServer, signlists, slnx files, docs, and the interop-tester agent updated; Opc.Ua.Core's InternalsVisibleTo entries updated.

2. Replaced UaSCUaBinaryChannel.FrameCaptureSink + per-call NotifyFrameCaptureSink null check with a transport-binding decorator. New files in Opc.Ua.Bindings.Pcap/Bindings:
   - IChannelCaptureRegistry / ChannelCaptureRegistry: singleton with a volatile IFrameCaptureSink slot (1 atomic write to switch recording on/off).
   - CapturingMessageSocket / CapturingMessageSocketFactory: IMessageSocket decorator that taps Send/Receive bytes via a single volatile read of CurrentObserver.
   - PcapTransportChannelBinding: ITransportChannelFactory decorator that injects the capturing socket factory into every TcpTransportChannel and wires ISecureChannel.OnTokenActivated through to the active observer.
   - PcapBindings.Install(): non-DI installer for direct TransportBindings.Channels.SetBinding wiring.

   IFrameCaptureSink moved to Opc.Ua.Core/Stack/Bindings and gained an OnTokenActivated method so the binding can publish key material alongside chunks. UaSCUaBinaryChannel.FrameCaptureSink property and all three NotifyFrameCaptureSink call sites removed - hot path returns to original shape with zero indirection when the binding is not installed. IMessageSocketAsyncEventArgs gains Offset + Count getters (additive).

   AddOpcUaDiagnosticsPcap/Formatters/Replay extension methods renamed to AddOpcUaBindingsPcap[Formatters|Replay]; the new AddOpcUaBindingsPcap also installs the binding into TransportBindings.Channels.

   InProcessClientCaptureSource / InProcessServerCaptureSource: dropped reflection-based AttachClientChannel/AttachServerChannel. The source itself implements IFrameCaptureSink and on Start publishes itself as the registry observer; on Stop clears it. McpServer's PacketCaptureTools simplified accordingly (no more per-channel attach loop).

3. Discarded the source-generator suggestion. Confirmed that the dissection path already 100% reuses the source-generated decoders: UaSCUaBinaryChannel.ReadSymmetricMessage for chunk decryption and BinaryDecoder.ReadEncodeable + EncodeableFactory.GlobalFactory (populated by Tools/Opc.Ua.SourceGeneration's DataTypeGenerator) for service-type decoding. Emitting additional dissection code would duplicate what's already there.

Validation:
- Full UA.slnx builds clean: 0 errors, 2 pre-existing unrelated warnings.
- All 53 tests pass on net10.0 (49 existing + 4 new CapturingMessageSocketTests covering hot-path: not-installed/installed-off/installed-on costs, byte equality, exception isolation).
- CI command line (dotnet build/test --framework net10.0 --configuration Release /p:CustomTestTarget=net10.0) reproduces clean locally.
2026-06-07 21:53:53 +02:00
Marc Schier 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
Marc Schier 67e803c6a7 Address PR review feedback
* Docs/MigrationGuide.md: drop the new 'Packet capture diagnostics hooks' section.
* Stack/Opc.Ua.Core/Stack/Tcp/IFrameCaptureSink.cs -> Stack/Opc.Ua.Core/Stack/Bindings/IFrameCaptureSink.cs.
* InProcessCaptureSource: drop sync-over-async (GetAwaiter().GetResult()) on the hot path. Frame and key-material persistence is now fully async via a bounded System.Threading.Channels queue drained by a single background writer task; the IFrameCaptureSink callbacks and OnTokenObserved enqueue non-blocking and never wait on I/O.
* Models/Models.cs: split into one file per type (FormatKind, CaptureSourceKind, CaptureSessionState, StartCaptureRequest, CaptureNowRequest, StartReplayRequest, CaptureSessionInfo, NetworkInterfaceInfo). Reformat every <summary>...</summary> block to multi-line per repo convention.
* Properties/AssemblyInfo.cs: drop duplicated InternalsVisibleTo (already declared in the csproj <ItemGroup>); keep [assembly: CLSCompliant(false)].
* Opc.Ua.Diagnostics.Pcap.csproj: trim the long marketing-style Description to a single sentence.

Validated: UA.slnx build 0 errors; 49/49 pcap library tests pass on net10.0.
2026-06-06 17:27:29 +02:00
Marc Schier 35f974996f Phase A+B: merge stress test projects + rename workflow
Merge the channel stress suite into Tests/Opc.Ua.Stress.Tests/Channels and move subscription stress tests into Subscriptions.

Drop the standalone Opc.Ua.Channels.Stress.Tests project from UA.slnx, carry over its package needs, enable nullable analysis, and fix analyzer warnings inline.

Rename the channel-manager stress workflow to the umbrella stress-test.yml workflow and point ChaosTCP at the merged project.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-06 16:51:33 +02:00
Marc Schier cbd84388d9 Add OPC UA packet capture, offline decoding and replay (Opc.Ua.Diagnostics.Pcap)
Adds a new diagnostics library (Stack/Opc.Ua.Diagnostics.Pcap) plus 13 MCP tools that capture OPC UA traffic, decode it offline by reusing the stack's own UaSCUaBinaryChannel, and replay it. Includes additive stack hooks (IFrameCaptureSink, TcpListenerChannel.OnTokenActivated), four capture sources (nic / inproc-client / inproc-server / replay), two keylog formats (.uakeys.json, .uakeys.txt), six output formats (pcap, pcapng, json, csv, text, service-timeline), and mock-server / mock-client replay engines. 49 new tests pass; no regressions in the 163 existing transport tests; full UA.slnx builds clean.
2026-06-06 16:31:41 +02:00
Marc Schier 06fbbb571f Add channel-manager stress/chaos test suite (Tests/Opc.Ua.Channels.Stress.Tests)
Layered pyramid (per rubber-duck plan) to exercise every meaningful combination
of the channel-manager + session reconnect machinery shipped in PR #3852:

L1 — Contract (deterministic, fake transport, runs in every PR):
  - CoalescingTests, ParticipantResultAggregationTests,
    RetryBudgetEnforcementTests, HungParticipantTests,
    LeaseLifecycleTests, GateAndBypassTests, KeyAndSharingTests,
    CertRotationContractTests, LeakAccuracyTests
  - 27 tests, all pass.

L2 — Integration (in-process server stop/start, runs in every PR):
  - ServerOutageRecoveryTests, CertRotationLiveTests, FailoverLeaseSwapTests
  - 6 tests, all pass.

L3 — Chaos (TCP proxy with drop/block-accept/stall, nightly category=ChaosTCP):
  - TransparentReconnectChaosTests, SubscriptionSurvivalChaosTests,
    AcceptButStallChaosTests, BlockAcceptChaosTests
  - Seed-driven via ChaosSchedule for reproducibility.

L4 — Soak (manual category=Soak):
  - LongSoakTests (60 min randomized chaos),
    CombinatorialMatrixTests (engine/transfer/subscribe/sessions matrix),
    MemoryStabilitySoakTests (30 min memory snapshots).

L5 — Known-failing gaps ([Explicit], document carry-forward):
  - FaultedEntryResetGapTests, SessionRecreatePlumbingGapTests,
    ParticipantTimeoutGapTests.

Infrastructure in Fakes/ and Helpers/:
  - FakeTransport, FakeChannelBindings, FakeParticipant (configurable fault modes).
  - ChaosBarrier (deterministic barrier eliminates timing flakiness in coalescing tests).
  - TcpChaosProxy (~200 lines; DropAllConnectionsAsync, BlockAcceptAsync, StallForwarding).
  - StressRunner (concurrent workload generator with latency percentiles).
  - ChaosSchedule + ChaosScheduleRunner (pre-generated deterministic event schedule).
  - WaitForQuiescence (ForManagerAsync, EntryRefcountReachesAsync, EntryGoneAsync).
  - MetricsCollector (subscribes to Opc.Ua.ChannelManager EventSource + IMeter).
  - LeakCounters (snapshot-based refcount/cert/entry validation).

CI wiring:
  - .github/workflows/buildandtest.yml now runs Category=Contract|Integration in PR CI.
  - .github/workflows/channel-manager-stress-test.yml runs Category=ChaosTCP nightly with random seed.

Small production-code tweaks discovered while building tests:
  - ClientChannelManager.Managed.cs / .Metrics.cs: minor refactors for testability.
  - Session.cs / Session.ChannelManager.cs / ManagedSession.cs: small adjustments to
    make IClientChannelManager-aware flows easier to assert.

Docs:
  - Docs/Sessions.md section 4 now has a "Testing the channel manager" subsection
    pointing at the test categories with run-and-reproduce commands.

Build: 0 warnings, 0 errors on net10.0.
Tests: 33/33 pass in Category=Contract|Integration.
2026-06-05 23:17:33 +02:00
Marc Schier 0a32452611 Merge branch 'master' into channelrefinements 2026-06-05 18:15:25 +02:00
Marc Schier 82347dc1da Address PR #3852 review feedback
- Move all channel-related types into `Stack/Opc.Ua.Core/Stack/Client/Channels/`
  subfolder (#13).
- Un-obsolete `SessionReconnectHandler` and the SessionExtensions
  `ReconnectAsync(connection, ct)` / `(channel, ct)` extensions; remove
  all SRH-related `#pragma warning disable CS0618` suppressions from
  applications and tests. AttachChannel/DetachChannel remain obsolete (#1, #4).
- Remove the shared-budget section from MigrationGuide.md (#3).
- Mark `ManagedSession.AttachChannel`/`DetachChannel` `[Obsolete]` so the
  warning surfaces all the way up the supported API surface (#9).
- Break the `<see cref=…>` line in `Session.ChannelManager.cs` to stay under
  140 chars (#10).
- Move `ChannelManagerSessionFactory` from `Opc.Ua.Gds.Client.Common` into
  `Opc.Ua.Client` as a public, documented session-factory option (#11).
- Audit other client classes for `IClientChannelManager`-aware overloads;
  add `ISessionFactory`-accepting overloads on `GlobalDiscoveryServerClient`,
  `LocalDiscoveryServerClient`, `ServerPushConfigurationClient` so any
  session factory (including `ChannelManagerSessionFactory`) works (#12).
- Refactor MCP server (`OpcUaSessionManager`, `Program`) to use
  `ManagedSession` + DI-resolved `IClientChannelManager`; remove
  `SessionReconnectHandler` and manual keep-alive reconnect (#2).
- ConnectionStateMachine code-style fixes: use named delegate types,
  collapse multi-line callback properties to single lines, swap
  `timeProvider`/`maxTotalReconnectTime` constructor param order,
  fix multi-line declaration (#6, #7, #8).
- HTTPS resilience: confirmed `Microsoft.Extensions.Http` and
  `Microsoft.Extensions.Http.Resilience` 10.6.0 support
  `net472`/`net48`/`netstandard2.1`; removed `#if NET8_0_OR_GREATER`
  gating from `ManagedSessionBuilder` and `OpcUaClientBuilderExtensions`
  and HTTPS csproj package refs (#5).

Verification:
- `Opc.Ua.Client` + `Opc.Ua.Core` + `Opc.Ua.Gds.Client.Common` build
  clean (0 warnings, 0 errors).
- 56/56 core channel-manager / retry-budget / HTTPS factory tests pass.
- 129/129 client (ManagedSession + legacy SRH back-compat + SessionExtensions)
  tests pass.
2026-06-05 17:22:41 +02:00
Marc Schier 36d67b3d3a [Client] Central channel manager with shared, ref-counted transport channels (#3288)
Introduces `IClientChannelManager` as the central registry for client-side
transport channels. Sessions and discovery clients now share ref-counted
managed channels per (endpoint, reverse-connect identity); reconnect is
coalesced and notified to attached participants via a new
`IReconnectParticipant` callback interface; service calls are gated by a
three-state lifecycle (`TransportReconnecting` -> `TransportConnectedSessionReactivating`
-> `Ready`) until reactivation completes.

New types in `Stack/Opc.Ua.Core/Stack/Client/`:
- `ManagedChannelKey`, `ChannelState`, `ChannelStateChange`, `ParticipantReconnectResult`
- `IReconnectParticipant`, `IManagedTransportChannel`, `IClientChannelManager`
- `IChannelReconnectPolicy` + `ExponentialBackoffChannelReconnectPolicy`
- `IRetryBudget` + `RetryBudget` (shared retry deadline across the two layers)
- `ClientChannelManager.Managed/Entry/Lease/Metrics/Diagnostics/CertRotation.cs`
  partials with refcount, coalesced reconnect, IMeter instruments, ActivitySource
  + EventSource, and automatic ReconnectAllAsync on certificate rotation.

Session integration (Libraries/Opc.Ua.Client):
- `Session.CreateAsync(IClientChannelManager, ...)` overload constructs a
  Session that shares its channel with co-located participants.
- `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; 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.
- 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 to UI / health dashboards.
- `ConnectionStateChangedEventArgs.UnderlyingChannelState` populated when
  outer reconnect was triggered by a channel fault.
- Shared `IRetryBudget` propagated to channel-mgr so the two layers
  enforce a single max-total-time instead of compounding multiplicatively.

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 (~70 new unit + 4 live-server integration fixtures):
- Core: `ClientChannelManagerManagedTests`, `ClientChannelManagerCertRotationTests`,
  `RetryBudgetTests`, `OpcUaHttpClientFactoryTests` (56/56 pass).
- Client: `ManagedSession*Tests` updated (129/129 pass) + back-compat
  suites pass with `#pragma warning disable CS0618`.
- Sessions integration: `ChannelManagerSharing/TransparentReconnect/SessionLifecycle/CertRotation`
  (6 pass, 2 skipped with documented blockers for faulted-entry reset and
  RequiresSessionRecreate end-to-end plumbing).

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 section 4 documents the channel manager,
  three-state gating, two-level retry with shared `IRetryBudget`, and
  HTTPS resilience layering.
- `Docs/MigrationGuide.md` migration recipes for the four obsoleted API
  groups.
- `Docs/DependencyInjection.md` channel-manager and HTTPS factory
  registration.
2026-06-05 15:37:23 +02: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
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
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 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
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
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 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
Marc Schier 239a40b183 GC reduction via IPooledEncodeable activator pooling (#3793)
* GC reduction via IPooledEncodeable activator pooling

Add opt-in activator-level pooling for IEncodeable instances decoded
during V2 subscription publish dispatch. The design replaces the
earlier IAllocator-on-IServiceMessageContext approach (which had a
cross-contamination hazard for non-publish decodes) with a clean
factory-based model where pooling lives on the encodeable activator.

New public API:
- IPooledEncodeable : IEncodeable - opt-in marker with Reuse() method
- PooledEncodeableType<T> : EncodeableType<T> - activator base with
  BoundedObjectPool<T> (lock-free, no new package dependency)
- ISubscriptionManager.PoolNotifications { get; set; }
- ManagedSessionOptions.PoolNotifications { get; init; }
- ManagedSessionBuilder.WithPoolNotifications(bool)

Source-gen:
- All concrete non-abstract structure types (including service
  request/response types) are generated with PooledEncodeableType<T>
  activators and IPooledEncodeable.Reuse() bodies by default
- Derived types use 'new' to hide inherited Reuse/ClearPooledSentinel
  so each concrete type routes through its own activator's pool
- No changes to external design XML files (UA Core Services.xml etc.)
  - poolable selection is hardcoded in the generator

Benchmarks (MonitoredItemNotification, 1000 ops):
- Allocations: 128 KB -> 408 B (~315x reduction)
- Gen0 collections: 29.75 -> 0.09 per 1000 ops

No interface changes to IEncodeable, IDecoder, IServiceMessageContext,
or ISubscriptionNotificationHandler. Handlers retain by copy when
pooling is enabled.
Call IPooledEncodeable.Reuse() on decoded IServiceRequest and
IServiceResponse after the server is done with them:

- TcpTransportListener.OnRequestReceivedAsync: reuse both request
  and response in a finally block after ProcessRequestAsync +
  SendResponse complete
- EndpointBase.InvokeServiceAsync (HTTPS path): same pattern after
  ProcessRequestAsync + BinaryEncoder.EncodeMessage

Walk acknowledged, overflow-evicted, and disposed NotificationMessage
objects and call IPooledEncodeable.Reuse() on their notification
payloads (MonitoredItemNotification, DataChangeNotification,
EventFieldList, EventNotificationList).

Insertion points in Subscription.cs:
- Acknowledge(): after RemoveAt from m_sentMessages
- InnerPublish() overflow: before RemoveRange/Clear on m_sentMessages
- Dispose(): before m_sentMessages.Clear()
- InnerPublish() unsent-message overflow: before RemoveRange on
  the messages list

Replace new T() with XxxActivator.Instance.CreateInstance() for all
server-side notification payload types so instances are rented from
the activator pool instead of heap-allocated:

New static helper ReuseNotificationPayloads() walks the
NotificationMessage.NotificationData, extracts DataChangeNotification
and EventNotificationList via TryGetValue, iterates their items, and
calls Reuse() on each pooled payload.
2026-05-22 12:42:33 +02:00
Marc Schier 296a904f34 Add full OPC UA Part 18 (Role management) support (#3778)
* role management compliant with OPC UA Part 18

Server-side:
- New IRoleManager / RoleManager with spec-correct status codes
  (Bad_AlreadyExists, Bad_NodeIdUnknown, Bad_RequestNotAllowed,
  Bad_NotFound) for every mutator per Part 18 4.2 / 4.4.
- Pre-populate default identities for the three reserved well-known
  roles per Part 18 4.3 (Anonymous, AuthenticatedUser,
  TrustedApplication).
- Reject mutations on reserved roles with Bad_RequestNotAllowed.
- Validate IdentityMappingRuleType per Part 18 4.4.3 (empty criteria
  for Anonymous/AuthenticatedUser/TrustedApplication; upper-case hex
  for Thumbprint; Name=Value grammar for X509Subject).
- Honour Part 18 4.4.2 default-field-as-wildcard EndpointType
  comparisons; tighten TrustedApplication/Application rules to
  require trusted certificate + signed channel.
- Introduce CustomConfiguration flag per Part 18 4.4.1.
- Add IRoleManager.RoleConfigurationChanged event raised after every
  successful mutation per Part 18 4.4.1.
- Make RoleEntry an immutable snapshot.

Address-space binding (typed proxies):
- RoleStateBinding switched from stringly-typed OnCallMethod2 plumbing
  to the source-generated typed proxies (RoleSetState, RoleState,
  AddRoleMethodState, AddIdentityMethodState, ...).
- DiagnosticsNodeManager.AddBehaviourToPredefinedNodeAsync upgrades
  passive BaseObjectState nodes of RoleSetType / RoleType to the
  typed RoleSetState / RoleState proxies at predefined-node load time.
- New RoleAuthorizationGate.CheckAdmin centralizes SecurityAdmin role
  plus SignAndEncrypt enforcement per Part 18 4.2 / 4.4 / 5.2.
- Bound writable ApplicationsExclude / EndpointsExclude /
  CustomConfiguration with the same auth gate.
- Raise RoleMappingRuleChangedAuditEventType from every successful
  mutator (Part 18 4.5) via the existing AuditEvents helper.

Client library:
- New Opc.Ua.Client.Roles.RoleManagementClient + IRoleManagementClient
  + RoleInfo + SessionRoleExtensions.OpenRoleManagementClient.
- Covers list / read / add / remove role, identity, application,
  endpoint operations plus Set*Exclude via Write Service.

Tests:
- New RoleManagerTests.cs - 37 NUnit tests for pre-populated roles,
  reserved-role immutability, spec-correct status codes, criteria
  validation, ResolveGrantedRoles, exclude semantics,
  CustomConfiguration, AddRole well-known NodeId reuse, and
  RoleConfigurationChanged event.

Documentation:
- Rewrite Docs/RoleBasedUserManagement.md as a Part 18 compliance
  reference (status-code matrix, client examples, known limitations).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add Part 18 5 UserManagement implementation

Server-side:
- New IUserManagement interface (Opc.Ua.Server.UserManagement) modelling
  Part 18 5.2.1 surface: PasswordLength, PasswordOptions, PasswordRestrictions,
  SnapshotUsers, AddUser, ModifyUser, RemoveUser, ChangePassword,
  MustChangePassword, IsUserActive, UserDeactivated event.
- Default UserManagement implementation wraps the existing IUserDatabase
  and stores per-user UserConfigurationMask + description in memory.
  Validates passwords against PasswordLength + character-class flags
  from PasswordOptions; honours Part 18 5.2 result codes
  (Bad_AlreadyExists, Bad_NotFound, Bad_OutOfRange, Bad_NotSupported,
  Bad_ConfigurationError, Bad_InvalidSelfReference,
  Bad_IdentityTokenInvalid).
- ChangePassword clears the MustChangePassword bit on success.
- New UserManagementBinding wires the typed source-generated proxies
  (UserManagementState, AddUserMethodState, ModifyUserMethodState,
  RemoveUserMethodState, ChangePasswordMethodState) - typed OnCallAsync
  delegates everywhere; admin gate via RoleAuthorizationGate.CheckAdmin;
  ChangePassword gated via RoleAuthorizationGate.CheckSelfUserName;
  closes active sessions for deactivated users via ISessionManager.
- ConfigurationNodeManager.AddBehaviourToPredefinedNodeAsync upgrades
  the passive UserManagement object to the typed UserManagementState
  proxy at predefined-node load time; CreateServerConfiguration invokes
  UserManagementBinding.Bind when an IUserManagement was injected.
- IServerInternal gains UserManagement getter + SetUserManagement
  injector.

Tests:
- New Tests/Opc.Ua.Server.Tests/UserManagementTests.cs - 18 NUnit tests
  covering AddUser happy path, duplicate / password-length / config-flag
  validation, RemoveUser non-existent / self-reference / NoDelete,
  ModifyUser self-disable / disable-other UserDeactivated event,
  ChangePassword happy path / same-as-old / wrong-old /
  MustChangePassword bit clearing / NoChangeByUser rejection,
  IsUserActive on disabled vs active users.

Documentation:
- Update Docs/RoleBasedUserManagement.md with a User Management section
  including the spec result-code matrix, example wiring code, and
  reduce known limitations - UserManagement is no longer a limitation.
2026-05-20 05:51:17 +02:00
Marc Schier 30bfa7895f Add OPC UA WoT Connectivity (OPC 10100-1) libraries and tests (#3786)
* Add WoT Connectivity (OPC 10100-1) libraries and tests

Implements the OPC UA WoT Connectivity companion specification (OPC 10100-1, `WoT Connectivity for OPC UA`) through four new projects under the unified Opc.Ua.WotCon prefix:

* Opc.Ua.WotCon - source-generated model from the official WotConnection.xml + WotConnection.csv design files (NodeStates, NodeIds, ObjectType client proxies including WoTAssetFileTypeClient : FileTypeClient).

* Opc.Ua.WotCon.Server - WotConnectivityNodeManager (AsyncCustomNodeManager subclass), factory, DI options. Provider model split into IWotAssetProvider (per-asset Read/Write/Observe/InvokeAction) and IWotAssetDiscoveryProvider (server-level Discover/Test/CreateForEndpoint per spec sec. 6.3.4-6). System.Text.Json TD parser with AOT-safe JsonSerializerContext. Property mapper per spec table 14, action mapper per sec. 6.3.9. WotAssetFileManager enforces only OpenFileMode Read (1) and Write|EraseExisting (6) per spec sec. 6.3.10 and drives CloseAndUpdate materialisation + persistence.

* Opc.Ua.WotCon.Client - WotConnectivityClient and WotAssetClient compose the generated proxies (no inheritance). Extension methods on FileTypeClient / WoTAssetFileTypeClient add chunked upload/download and the Open(Write|EraseExisting) -> Write* -> CloseAndUpdate flow, extending the existing FileSystem API without subclassing.

* Opc.Ua.WotCon.Tests - NUnit suite (28 passing tests) covering TD parsing, property and action mappers, and the SimulatedWotAssetProvider/SimulatedWotDiscoveryProvider which serve as canonical examples of the provider contract.
2026-05-19 19:02:29 +02:00
Marc Schier 535d017840 Add OPC UA Part 17 (Alias Names) support (#3779)
* Add OPC UA Part 17 (Alias Names) support

Server-side: pluggable IAliasNameStore + thread-safe InMemoryAliasNameStore, AliasNameStoreRegistry (live dispatcher, surfaces through optional IAliasNameStoreRegistryProvider on ServerInternalData), AliasNameNodeManager (standalone CustomNodeManager2; defaults to SecurityAdmin-only mutations on SignAndEncrypt channels), AliasNameMethodDispatcher bridging generated typed FindAlias/FindAliasVerbose/Add/Delete *MethodState.OnCallAsync handlers, AliasNameWildcardMatcher (Part 4 §7.40 Like operator with proper escape handling), and a DiagnosticsNodeManager partial that late-binds the standard well-known Aliases (i=23470), TagVariables (i=23479) and Topics (i=23488) FindAlias methods plus Aliases.LastChange through the registry.

Client-side: AliasNameClient (OpenStandardAliases/TagVariables/Topics + arbitrary category root) covering all four Part 17 methods, sub-category enumeration and ReadLastChangeAsync. Hardcoded method-id fast-path for standard categories; cached TranslateBrowsePaths resolution for custom ones. Service-level errors map to typed exceptions (UnauthorizedAccessException / NotSupportedException). AliasNameResolver (IAsyncDisposable) gives a name->NodeId cache with forward and reverse lookup; default refresh is Manual with opt-in AutoOnLastChange polling that compares VersionTime on inequality (wrap-safe).

Reference server: registers an InMemoryAliasNameStore for the standard TagVariables + Topics categories with sample seeds (TIC101_Setpoint, TIC101_PV, FIC202_Flow, Pump1_Status, Heater_Power, MultiRefAlias, ServerEvents, AuditEvents) so the standard well-known FindAlias methods return data through the DiagnosticsNodeManager late binder.

Tests: 45 server-side cases (wildcard matcher, in-memory store, registry, node manager) plus 11 mocked-session client cases and 4 end-to-end integration cases against a live ReferenceServer over a real OPC UA channel. All 60 tests pass; full solution builds clean across net472/net48/netstandard2.1/net8.0/net9.0/net10.0.

Docs: Docs/Part17AliasNames.md plus a link from Docs/README.md.
2026-05-19 12:55:12 +02:00
Marc Schier f3d3fd87d3 Reorganize Stack/ folder layout (#3775)
Two refactors that together reshape how the stack tier is laid out:

1. Rename `Opc.Ua` project to `Opc.Ua.Core.Types`

   * Stack/Opc.Ua/ -> Stack/Opc.Ua.Core.Types/ (git mv preserves history)
   * Opc.Ua.csproj -> Opc.Ua.Core.Types.csproj
   * AssemblyName  : `Opc.Ua` -> `Opc.Ua.Core.Types`
   * PackageId     : `OPCFoundation.NetStandard.Opc.Ua` ->
                     `OPCFoundation.NetStandard.Opc.Ua.Core.Types`
   * RootNamespace : kept as `Opc.Ua`
   * StackGeneration.cs assembly-name switch updated; the renamed
     assembly still produces `StackGenerationType.Models`.
   * The `OPCFoundation.NetStandard.Opc.Ua` name now belongs solely to
     the roll-up meta-package (nuget/Opc.Ua.nuspec); the meta-package's
     former self-dependency is replaced with the new
     `.Core.Types` package, and `.Symbols.nuspec` gains the
     matching `.Core.Types.Debug` debug-symbol dependency.

2. Move `Opc.Ua.Security.Certificates` from Libraries/ to Stack/

   It is a stack-tier dependency consumed by Opc.Ua.Core, not a
   higher-level library extension, so it now sits alongside the other
   stack projects.

Both refactors update the same set of cross-cutting infrastructure:
- UA.slnx, UA Core Library.slnx, Applications/ConsoleReferenceServer/Dockerfile
- .azurepipelines/signlist{Debug,Release}.txt
- ProjectReferences across Stack/, Libraries/, Tests/, Fuzzing/
- Tests/Opc.Ua.SourceGeneration.Stack.Tests/StackGenerationAssemblyTests.cs
- Docs/PlatformBuild.md and .github/copilot-instructions.md (new
  Core.Types package listed)

Verification
* dotnet build UA.slnx -c Debug -- succeeded, 0 errors
* StackGenerationAssemblyTests       :    5 passed
* Opc.Ua.SourceGeneration.Tests      :   40 passed
* Opc.Ua.SourceGeneration.Core.Tests : 3514 passed,  8 skipped
* Opc.Ua.Core.Tests (Stack filter)   : 1982 passed, 80 skipped
* Opc.Ua.Security.Certificates.Tests :  239 passed
2026-05-18 05:49:36 +02:00
Marc Schier 5795e50b58 Source-generated, NativeAOT-clean fluent server API with event-source publish (#3765)
* Add async read/write hooks to BaseVariableState and AsyncCustomNodeManager

Introduces an end-to-end async path for variable Value-attribute reads
and writes that does not require holding lock(this) while the user
callback executes:

* New AttributeReadResult, AttributeSimpleReadResult and AttributeWriteResult
  readonly record structs in NodeState carry the typed result of an async
  Value-attribute hook (no ref/out params across await).
* Four new async delegates (NodeValueEventHandlerAsync,
  NodeValueSimpleEventHandlerAsync, NodeValueWriteEventHandlerAsync,
  NodeValueSimpleWriteEventHandlerAsync) sit alongside the existing sync
  hooks.
* NodeState gains virtual ReadAttributeAsync / WriteAttributeAsync methods.
  The default implementation wraps the synchronous call in lock(this) so
  callers that have not opted into async hooks see bit-identical behaviour.
* BaseVariableState exposes OnReadValueAsync, OnSimpleReadValueAsync,
  OnWriteValueAsync and OnSimpleWriteValueAsync slots and overrides the new
  virtuals to dispatch to them WITHOUT holding the lock during the await.
  On simple-read the framework still applies index range, data encoding and
  copy policy; on simple-write the cached value/status/timestamp are
  updated under lock after the hook completes.
* AsyncCustomNodeManager.ReadAsync and WriteAsync now await the new
  ReadAttributeAsync / WriteAttributeAsync entry points instead of taking
  lock(source) around the synchronous call. Existing per-source locking
  semantics are preserved by the default async wrappers.
* The fluent INodeBuilder surface gains four new async OnRead/OnWrite
  overloads that wire to the new BaseVariableState slots, with the same
  ThrowIfSlotOccupied + null-check pattern as the sync overloads.

* Add typed IVariableBuilder<T> fluent surface

Adds typed sub-interfaces and impls so the existing fluent surface

can expose simple Func<T> / Action<T> / Func<CancellationToken,

ValueTask<T>> overloads for variables — without losing the lower-level

Variant-based overloads on INodeBuilder.

* IVariableBuilder<TValue> + VariableBuilder<TValue> derive from

  INodeBuilder<BaseVariableState> / NodeBuilder<BaseVariableState>

  (NodeBuilder<TState> unsealed).

* Marshalling uses Variant.AsBoxedObject(BoxingBehavior.Legacy) for

  reads and Variant(object) for writes; the AOT-unsafe write path is

  scoped-suppressed with a TODO pointing at the planned per-type

  generated walker.

* INodeManagerBuilder gains Variable<T>(string),

  Variable<T>(NodeId), VariableFromTypeId<T>(NodeId),

  VariableFromTypeId<T>(NodeId, QualifiedName).

Server library builds clean across all 6 TFMs.

* Add relative-child traversal API on INodeBuilder

Adds INodeBuilder.Child(QualifiedName), Child<TState>(QualifiedName)

and Variable<TValue>(QualifiedName) so source-generated typed

wrappers can walk one segment at a time without re-resolving from

the manager root. Resolution uses NodeState.FindChild and reuses

NodeManagerBuilder.ToVariableBuilder to keep the typed-variable

marshalling story consistent.

Adds 7 NUnit cases (Fluent category) covering the happy path,

type-mismatch, null/missing browse-name, and non-variable

rejection. All 66 fluent tests pass.

* Mix old + new fluent APIs in MinimalBoilerServer demo

BoilerNodeManager.Configure now exercises five wiring styles:

  (1) browse-path with the legacy ref-Variant callback

  (2) absolute NodeId with the legacy ref-Variant callback

  (3) typed Variable<double>(NodeId) + simple Func<double> sync getter

  (4) typed Variable<double>(NodeId) + Func<CancellationToken, ValueTask<double>>

      async getter — exercises the BaseVariableState.OnReadValueAsync

      slot end-to-end through AsyncCustomNodeManager.ReadAsync

  (5) NodeFromTypeId with OnNodeAdded lifecycle hook

All five styles compose against the same INodeManagerBuilder, with

the typed and async callbacks pulling in the new IVariableBuilder<T>

surface (committed at e557ecdd9) and the BaseVariableState async

path (committed at 2e2efb4ad).

* Add async-hook tests for BaseVariableState

15 NUnit cases covering the new On{Read,Write}ValueAsync and On
Simple{Read,Write}ValueAsync slots plus the
ReadAttributeAsync/WriteAttributeAsync overrides on
BaseVariableState:

* full async slot routes Value reads/writes
* lock(this) is released around the awaited handler
* simple async slot reapplies cached StatusCode and respects index
  ranges (BadIndexRangeInvalid for non-null ranges)
* exceptions and OperationCanceledException from the hook are
  caught and surfaced as BadUnexpectedError, mirroring the sync flow
* CancellationToken propagates to the hook
* cache (m_value, m_statusCode, m_timestamp) is updated on success
  and skipped on Bad return
* fallback to base sync ReadAttribute/WriteAttribute when no async
  slot is set (preserves today's lock(this) semantics)
* non-Value attributes never invoke the async hook
* CurrentRead/CurrentWrite access checks short-circuit before the
  hook runs

All 596 State tests pass on net10.0.

* Add round-trip tests for typed IVariableBuilder<T>

Covers the typed Func<T>/Action<T>/Func<...,ValueTask<T>> overloads
on IVariableBuilder<TValue>: each registers the appropriate hook
slot on the underlying BaseDataVariableState, and driving the
variable through ReadAttribute/WriteAttribute (sync) and
ReadAttributeAsync/WriteAttributeAsync (async) reproduces the
typed values the user supplied.

* Add typed model-traversal source generator

Adds FluentBuilderGenerator that walks the design's predefined-instance tree and emits a typed sibling for every NodeManagerBuilder. Each generated wrapper exposes IntelliSense-friendly accessors for child instances, variables (IVariableBuilder<T>) and methods (sync/async OnCall) so wiring sites become builder.Boilers.Boiler__1.LCX001.Measurement.OnRead(...) instead of a stringly typed browse path. The NodeManager template now also emits a Configure(I{Manager}NodeManagerBuilder) partial alongside the existing Configure(INodeManagerBuilder); both partials run, both are optional, and the typed surface is fully AOT-safe (no dynamic, no MakeGenericType, no reflection).

* Add tests for the typed model-traversal source generator

Adds 9 snapshot tests in Tests/Opc.Ua.SourceGeneration.Core.Tests covering the FluentBuilderGenerator's opt-in flag, typed-manager-interface and proxy emission, internal-sealed visibility, lazy child resolution via Context.NamespaceUris.GetIndexOrAppend, IVariableBuilder<TValue> accessors, sync+async OnCall overloads on method wrappers, and the dual Configure(INodeManagerBuilder)/Configure(I{Manager}NodeManagerBuilder) partial wiring in the NodeManager template. Adds 2 end-to-end AOT tests in Tests/Opc.Ua.Aot.Tests that exercise typed-traversal sync read on LCX001/Measurement and typed-traversal async OnCall on Simulation/Halt through the AOT-compiled MinimalBoilerServer fixture.

* Document the typed model-traversal Configure(I{Manager}NodeManagerBuilder) partial
2026-05-15 07:52:49 +02:00
Marc Schier 3836cbfdfe Add FileSystemClient: System.IO-style async client over OPC UA file APIs (#3760)
Add FileSystemClient: System.IO-style async client over OPC UA file APIs
2026-05-14 23:41:12 +02:00
Marc Schier 70b4498db8 Certificate manager and ref counted certificate type (#3688)
* New certificate and secrete abstraction with certificate infrastructure and certificate/secret stores. 
* leak testing for certificate resources to validate new ref counting in every test assembly
* Migration guide updated and documentation added for all
* Remove IDisposable from Nodestate and UserIdentity.
* UserCertificateTokenHandler now just points into certificate registry/stores, no dispose required
* Fix CA2000 warnings and cleanup of other diagnostics, increase warning levels in .editorconfig
2026-05-12 07:56:38 +02:00