Commit Graph
16 Commits
Author SHA1 Message Date
537b10c8b3 Fix History User Access Levels and add CTT coverage nodes (#4026)
# Description

Improve Reference Server coverage for OPC UA Compliance Test Tool
scenarios and document the latest CTT findings.

## Reference Server coverage

- Historize the configured AccessRights nodes and seed them with the
same deterministic mixed-quality history as the other test nodes while
preserving their access controls.
- Add a historian node that intentionally does not support
ServerTimestamp.
- Add a SelectionListType String instance whose Selections property is
String[] as required by Part 5, with localized descriptions and
RestrictToList enforcement.
- Add an EUR CurrencyUnit property and verify its structured value and
binary encoding.
- Add a node with explicit read/write base access bits plus
NonatomicRead and NonatomicWrite AccessLevelEx flags.
- Ensure the first ByteString array elements meet the CTT minimum-length
prerequisite.

## Latest CTT findings

- Confirm run-23 A&C residuals are CTT re-entrant Read and
branch-unaware Retain checks rather than server regressions.
- Confirm NumberOfTransitions includes Uncertain values as required by
Part 13.
- Document the CTT CurrencyUnit decoder dropping a correctly encoded
Exponent.

## Related Issues

- Follow-up to #3960 and #4002.

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

Focused affected tests pass on .NET 10 and .NET Framework 4.8.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
2026-07-21 13:52:12 +02:00
84fa8e8906 CTT follow-up: monitored items, alarms, codecs, history, and model validation (#4002)
# Description

Follow-up to #3978 based on the latest OPC UA CTT aggregate, matrix,
Historical Access, Attribute, and Node Management runs.

## Aggregate run 16

- Ignore synthetic `BadBoundNotFound` raw-bound markers when feeding
aggregate calculators.
- Add direct and live historian regression coverage.

## Multi-dimensional Variant matrices

- Fix `Variant.Copy()` so rank-2 Variant matrices preserve their payload
and dimensions.
- Enforce valid Variant matrix dimensions consistently across Binary,
JSON, XML stream, and XML parser codecs.
- Encoders return `BadEncodingError`; decoders return `BadDecodingError`
for malformed matrix dimensions.
- Add cross-codec valid/null/empty/malformed tests and a batched
ReferenceServer Read regression.
- Correct the previous CTT issue attribution: the invalid
`Scalar_Static_Arrays2D_Variant` server encoding poisoned the batched
CTT Read.

## Node Management AddNodes

- Return `BadNodeIdRejected` for unsupported requested-NodeId
namespaces.
- Validate hierarchical ReferenceType source/target NodeClass
constraints.
- Document run-18 confirmation that all remaining 15 AddNodes errors are
CTT project/script configuration issues.

## Historical Access and Attribute Services run 18

- Reject whitespace in `NumericRange` syntax with
`BadIndexRangeInvalid`.
- Add HistoryRead tests for operation-level invalid syntax and
per-DataValue `BadIndexRangeNoData`.
- Document CTT defects:
- `012.js` expects `BadIndexRangeNoData` at the operation level instead
of on returned DataValues;
  - `Err-012.js` uses a non-historizing node for an access-denied test;
- generic Attribute array helpers omit NodeId[] and StatusCode[]
conversion paths.

## Base Information model validation run 19

- Run the CTT main session as `sysadmin` over Basic256Sha256
SignAndEncrypt so SecurityAdmin RolePermissions and EncryptionRequired
restrictions are both satisfied without weakening the server model.
- Preserve predefined well-known NodeIds while making generated dynamic
type and optional-child factories allocate compatible per-instance
NodeIds, remap internal references, and update references from the
owning subtree.
- Materialize dynamic RoleType properties through generated helpers and
keep role-child string NodeIds disjoint from numeric RoleManager root
IDs.
- Add net48/net10 regressions for generator output, NodeId
allocation/reference remapping, DI mandatory children, dynamic roles,
and low-privilege versus SecurityAdmin Browse visibility.
- Document CTT defects for permission-unaware mandatory-child
validation, the UA 1.04 reference model used against a UA 1.05 server,
stale ServerDiagnostics cache, and the `ConformanceUnits` scalar/array
mismatch.
## Related Issues

- Follow-up to #3960 and #3978.

## Checklist

- [x] I have signed the CLA and read the CONTRIBUTING document.
- [x] Tests prove the fixes and improve coverage.
- [x] Required CTT documentation is updated.
- [x] Focused builds/tests introduce no warnings.
- [x] Affected tests pass on .NET Framework and .NET 10.
- [ ] CI and CodeQL are green.
- [ ] All PR feedback is addressed.



## Latest CTT full run and A&C run 20

- Fix Monitor Value Change V2 `036.js`: an ambiguous
`DataValue(StatusCode)` call encoded `BadIndexRangeNoData` as a Good
value.
- Prove all 19 configured matrix monitored items notify when the
selected value actually changes; document the independent `042.js`
script defects.
- Rebase dynamic `CertificateExpired` / `TrustListOutOfDate` alarm roots
and descendants, restore standard type declarations and ModellingRules,
and expose the cross-node-manager references.
- Deliver method-triggered events to every authorized Session, fixing
the two-Session Confirm scenario.
- Preserve locale-only `LocalizedText` values consistently across
Binary, JSON, and XML and align Acknowledge/Confirm comment behavior and
tests.
- Document the remaining proven CTT defects for TransactionDiagnostics,
SemanticChange, History `013`, PolicyId scope, A&C, durable
subscriptions, and Subscription Minimum events.

## Base synchronization

Merged `master` through `007cfaf90` (`#4010`, repository layout
consolidation) and retained the matrix regression tests at their new
`tests/` locations.

## Latest validation

- Focused merged-tree tests pass on net10 and net48.
- `Opc.Ua.Server` and `Quickstarts.Servers` Release builds complete with
0 warnings and 0 errors on net10 and net48.
- The existing
`DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync`
timing test took its documented inconclusive timeout path after the
layout merge; no test failure was reported.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
2026-07-18 05:01:03 +02:00
Marc Schier 8538f531e7 Update paths for the consolidated repository layout 2026-07-16 06:26:05 +02:00
0da85b9d89 CTT conformance: access rights, X509, aggregates, and matrix diagnostics (#3978)
# Description

Investigates successive OPC UA CTT runs and fixes the proven server
conformance defects while documenting proven CTT script/codec defects in
`plans/ctt-issues.md`.

## Server fixes

- Grant `WriteAttribute` on `Scalar_Static_Int32` so attributes
advertised by `WriteMask` are writable.
- Disable the client-certificate-keyed authentication lockout in the
dedicated CTT configuration so negative token tests do not mask later
results with `BadUserAccessDenied`.
- Map an X509 user-token signature algorithm mismatch to token-level
`BadIdentityTokenInvalid`.
- Persist rejected X509 user certificates to `pki/rejectedUser` to
simplify trusted-user provisioning.
- Populate `HistoricalDataConfiguration.AggregateConfiguration` with the
server's actual defaults.
- Return per-node `BadInvalidArgument` for processed reads with equal
StartTime/EndTime.
- Feed reverse processed reads in request order and preserve exact
non-Bad interpolation boundaries.
- Correct PercentGood/PercentBad uncertain classification.
- Correct WorstQuality/WorstQuality2 eligible bounds, StatusCode
preservation, and `MultipleValues`.
- Correct reverse Min/Max Raw-vs-Calculated semantics.
- Fix `HistoryClient.GetConfigurationAsync` traversal and
aggregate-configuration reads.

## Tests and CTT evidence

- Added direct calculator and live historian Part 11/13 oracle tests
covering forward/reverse intervals, equal-time validation, bounds,
status aggregates, DurationInState, and Min/Max.
- Added exact Part 6 Table 26 binary golden-vector coverage for a 2×3
Int32 Matrix Variant.
- Added in-process whole-matrix read/write and multidimensional
`NumericRange` coverage.
- Updated `plans/ctt-issues.md` with exact evidence and recommended
fixes for CTT aggregate, X509, multidimensional-array, A&C, and
AliasName script defects.

No CTT failure is suppressed or accepted without a spec-based
classification.

## Related Issues

- Relates to #3960.

## Checklist

- [x] Tests proving the server fixes are included.
- [x] CTT-side defects are documented with script locations and
specification references.
- [x] Targeted aggregate/routing/matrix suites pass on net48 and net10.
- [x] History aggregate/client suites pass on net48 and net10.
- [x] Modified lines are analyzer-clean.
- [ ] CI and CodeQL complete.
- [ ] Maintainer review complete.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Roman Ettlinger <romanett98@gmail.com>
2026-07-13 19:14:19 +02:00
Marc Schier d7cb170b86 CTT compliance (#3960) + HA/Aggregate facets (#3875) + server-side pcap capture (#3962)
## Summary

This PR bundles two independent bodies of work on the branch:

1. **CTT compliance & Historical Access** — improves OPC UA Compliance
Test Tool (CTT) conformance of the Console Reference Server (issue
#3960) and advertises the correct Historical Raw Data + Aggregate server
facets (issue #3875).
2. **Server-side in-process packet capture (pcap)** — lets a hosted
server capture its inbound client→server traffic in-process, with a
documented non-DI wiring path.

The CTT work was driven by an actual CTT run (`NewCTT2`, script
v1.05.513) whose 4757 error results were parsed and clustered by
conformance unit. Fixes are verified live via MCP against the running
`--ctt` server and covered by unit/integration tests that are green on
**net48 + net10**.

## Server-side in-process packet capture (pcap)

Adds the ability to capture a hosted server's **inbound**
(client→server) `opc.tcp` traffic in-process, symmetric to the existing
client-channel capture in `Opc.Ua.Core.Diagnostics`. Previously
`PcapBindings.Install(server.Server.TransportBindings)` only installed a
client *channel* decorator — a server accepts connections through
*listeners*, which never consult channel factories — so nothing was
captured, and `InProcessServerCaptureSource` was a placeholder.
Installing a binding also records nothing on its own until a
`CaptureSessionManager` session is started (which is now documented).

- **Core seam** (`Opc.Ua.Core`, opt-in, zero cost when unused):
`TransportListenerSettings.AcceptedTransportDecorator` +
`OnAcceptedChannel`, applied in `TcpListenerChannel.Attach(socket)` and
the reverse-connect path; `TcpTransportListener` wires them per accepted
channel. Default `null` → no behavior change for non-capturing servers.
- **`PcapTransportListenerBinding`** — an `ITransportListenerFactory`
**listener decorator** (the server-side mirror of the client
`PcapTransportChannelBinding`) that injects the capture seam via
`OpenAsync`, wraps each accepted channel's byte transport in
`CapturingByteTransport`, and forwards channel-token key material to the
active observer.
- **`PcapBindings`** — `Install` now wires **both** client and server;
new `InstallClient` / `InstallServer` helpers. `AddPcap` covers both
sides too. `InProcessServerCaptureSource` is now functional.
- **Non-DI usability** — new public
`ClientChannelManager.DefaultChannelBindings` accessor so non-DI clients
can install the binding into the registry the client actually uses, plus
a new "Enable pcap capture without dependency injection" guide in
`Docs/Diagnostics.md` (server / client / both recipes,
install-before-start timing, the two-registries nuance) and
`NugetREADME` updates.
- **Tests** — Core settings-seam unit tests (net48 + net10),
`PcapTransportListenerBinding` / `PcapBindings` unit tests, and a real
server↔client integration test asserting a non-empty pcap of
server-accepted frames.

## Fixes (CTT compliance / Historical Access)

### Historical Access — Read Raw (~1996 CTT errors)
The new historian returned `Good_MoreData` when handing back a
`ContinuationPoint`. Per OPC UA Part 11 §6.5.3.2 (and the CTT HA Read
Raw tests 008/009, which assert `Good` **and** a `ContinuationPoint`
together) the operation status must be **`Good`** — the non-empty
ContinuationPoint alone signals more data. Fixed all four continuation
sites (raw / modified / annotation / processed-event) in
`HistorianDispatcher`, and additionally report **`Good_NoData`** for a
raw/modified read of an interval that contains no data and requests no
bounds (CTT HA Read Raw 007). Adds regression tests.

### Historical Access — HA Configuration (~282 CTT errors)
Historizing reference-server nodes had no `HasHistoricalConfiguration`
reference, so History Access aggregate clients (and the CTT) could not
discover the archive/aggregate configuration. The reference server now
attaches a populated `HistoricalDataConfigurationType` companion object
("HA Configuration") to every historizing node. This also fixes a latent
bug in `HistoricalDataConfigurationInstaller`: a freshly created
configuration instance (and its mandatory `Stepped` /
`AggregateConfiguration` children) retained the type's shared NodeIds,
so the reference targeted the type node and multiple nodes collided —
the installer now assigns instance NodeIds to the whole subtree via the
node manager's `INodeIdFactory`. Adds a regression test.

### Historical Access — Read Raw status codes
Additional CTT HA Read Raw conformance fixes:
- Insufficient details (<2 of startTime/endTime/numValuesPerNode) →
`Bad_HistoryOperationInvalid` (Err-027).
- IndexRange on scalar history → `Bad_IndexRangeNoData` at the operation
level (Err-009).
- Unknown/foreign ContinuationPoint (e.g. a Browse CP) →
`Bad_ContinuationPointInvalid` (Err-014/Err-024).
- No spurious ContinuationPoint on the final page of a
one-value-at-a-time read (008/009).
- `TimestampsToReturn.Neither` → `Bad_TimestampsToReturnInvalid`
(Err-002).

All validated against the actual CTT run (`NewCTT2` Run2, an HA-only
re-run built with the base fixes) which confirms the 1999 "Good_MoreData
vs Good" failures are gone. Covered by tests.

### Earlier commits in this PR
Variant/BaseDataType array write `BadTypeMismatch`, DiagnosticInfos
gating on `DiagnosticsMask`, multi-dimensional array values;
strengthened Attribute Write conformance tests; Condition comment
retention on null Acknowledge/Confirm; historized the full scalar type
set + paired aggregate nodes; populated
`ServerCapabilities.ConformanceUnits`; advertised
`HistoricalRawData2022` + `AggregateHistorical2022` facets (#3875) with
`Docs/Profiles.md` updates.

## Diagnosed (not server bugs — CTT connection configuration)

### Auditing Connections (~1684 CTT errors)
All 1684 failures are identical ("Unable to Find Entry for
ClientAuditEntryId"). Audit events are, per spec, delivered only to
sessions holding the `SecurityAdmin` role: the server validates
`PermissionType.ReceiveEvents` per event-monitored-item, and the
integration test `AuditingOperationTests` connects as `sysadmin`/`demo`
for exactly this reason. The CTT run collected zero audit events because
it connected **anonymously**. Documented in `Docs/Diagnostics.md`:
configure the CTT to authenticate as `sysadmin`/`demo` (mapped to
`[SecurityAdmin, AuthenticatedUser]` in `ReferenceServer.cs`).

### Address Space WriteMask (~126 CTT errors)
`revertToOriginalNodeAttributes` writes back all node attributes and
gets `BadUserAccessDenied` (a permission error) rather than
Good/BadNotWritable/BadAttributeIdInvalid — the same
anonymous-connection cause; expected to clear when the CTT connects as
`sysadmin`/`demo`.

## Still open
Alarms & Conditions Retain/ConditionDisabled/InputNode (logic is
spec-aligned on inspection; needs a CTT-faithful reproduction to
separate a genuine bug from alarm-simulation timing), Aggregate function
values (now unblocked by the HA Configuration fix), GDS
self-registration, residual Attribute IndexRange/type-mismatch.
CTT-script errors (`ArrayItems` / `ItemsHistoryCountExceeds` undefined)
are not server-fixable.

## Testing
`Opc.Ua.Server.Tests` (Historian) and `Opc.Ua.History.Tests` green on
net48 + net10; historian and HA-configuration behaviour verified live
against the running `--ctt` server. Server-side pcap capture: Core
builds clean on net48 + net10, and the new unit + server↔client
integration tests pass (the only failing `Opc.Ua.Core.Diagnostics.Tests`
are 3 pre-existing, unrelated `OfflineSecureChannelSmokeTests`). The
single most decisive remaining CTT step is a re-run authenticated as
`sysadmin`/`demo`, which clears the permission-based clusters and
isolates the true remaining server bugs.
2026-07-09 09:08:27 +02:00
Marc Schier 76bceb51e2 Env-var driven Pcap auto-capture + rename to Opc.Ua.Core.Diagnostics + Docs/Diagnostics.md (#3888)
# Description

This PR also renames the project itself from `Opc.Ua.Bindings.Pcap`
(assembly / NuGet / folder / root namespace) to
`Opc.Ua.Core.Diagnostics`. `Pcap`-prefixed type names (`PcapFileWriter`,
`PcapOptions`, `IPcapAuditSink`, ...) and the `AddPcap*` extension
methods are preserved &mdash; only the project shell broadens to
`Core.Diagnostics`. The new NuGet id is
`OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics`. Folders were moved
with `git mv` so blame and log are preserved.

This PR also renames the **C# namespace** from
`Opc.Ua.Core.Diagnostics.*` to `Opc.Ua.Pcap.*` (commit `efc03691c`). The
**assembly identity** stays `Opc.Ua.Core.Diagnostics` (assembly name,
NuGet id `OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics`, folder,
csproj filename, `InternalsVisibleTo`, .slnx project paths, sign-lists
are all unchanged). Result: one `Opc.Ua.Core.Diagnostics.dll` assembly
hosting types in the `Opc.Ua.Pcap.*` namespace — standard .NET pattern
(cf. `System.Net.Http.dll` hosting `System.Web.*`). Future diagnostic
features can add other `Opc.Ua.*` namespaces to the same assembly
without another rename. 149 files changed (+411/-411 net); 422/422 tests
pass on net10.0.

This PR also merges `Docs/PacketCapture.md` and `Docs/Observability.md`
into a new `Docs/Diagnostics.md`. `ITelemetryContext` leads the doc with
expanded usage and extensibility guidance (custom contexts,
OpenTelemetry exporter wiring, ActivitySource / Meter naming
conventions, testing patterns). New sections cover OPC UA server audit
events and the built-in `Server.ServerDiagnostics` info-model nodes. The
1.4 / 1.5 evolution narrative has been dropped; the still-relevant
migration content has moved into `Docs/MigrationGuide.md`. All 8 inbound
doc / slnx / XML-doc references are retargeted.

This PR also adds a new public DI extension `AddPcapFromEnvironment` to
`OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics` that activates
packet capture and/or symmetric-key logging at host start based on two
environment variables. The variables are evaluated **once** during the
`AddXXX` call (matching the existing `OPCUA_PCAP_ENABLE_DIAGNOSTICS`
opt-in semantics):

| Variable | Effect when set |
|---|---|
| `OPCUA_PCAP_FILE` | Auto-start an in-process capture session on host
start. Frames are written verbatim to the literal path; the parent
directory becomes `PcapOptions.BaseFolder` so the existing
`SessionFolder` path-traversal validation still runs. |
| `OPCUA_KEYLOGFILE` | When set without `OPCUA_PCAP_FILE`: install a
stand-alone keylog observer (SSLKEYLOGFILE-style) directly into
`IChannelCaptureRegistry`. When set together with `OPCUA_PCAP_FILE`:
keylog records go to this path instead of the conventional
`*.uakeys.json` sibling of the pcap. |

When neither variable is set the call behaves exactly like `AddPcap`
&mdash; binding registered, no auto-start. When the auto-start path is
active the host emits a `Warning`-level log line naming the env var(s)
consumed and the resolved path(s); key material is never logged.

## Public surface

The new public API is intentionally narrow:

```csharp
namespace Opc.Ua.Core.Diagnostics.DependencyInjection
{
    public static class PcapEnvironmentVariableNames
    {
        public const string OpcuaPcapFile = "OPCUA_PCAP_FILE";
        public const string OpcuaKeyLogFile = "OPCUA_KEYLOGFILE";
    }

    public static partial class PcapServiceCollectionExtensions
    {
        public static IServiceCollection AddPcapFromEnvironment(this IServiceCollection services);
        public static IServiceCollection AddPcapFromEnvironment(this IServiceCollection services, Action<PcapOptions> configure);
    }
}
```

`StandaloneKeyLogObserver`, `PcapEnvironmentDefaults`, and
`PcapEnvironmentAutoStartHostedService` are internal &mdash; the env-var
names and DI methods are the only public surface, which keeps it
AOT-clean.

## Behaviour-impacting code change

`InProcessCaptureSourceBase` now honors
`StartCaptureRequest.PcapFilePath` and
`StartCaptureRequest.KeyLogFilePath` for in-process capture (previously
these properties were respected only by the Replay source, despite their
generic XML docs). Behaviour is **opt-in**: callers that pass `null`
continue to get the conventional `capture.pcap` / `keys.uakeys.json` /
`keys.uakeys.txt` filenames inside `SessionFolder`. Path-traversal
validation in `CaptureSessionManager.StartAsync` is unchanged.

## Operator example

```yaml
# Kubernetes manifest snippet
env:
  - name: OPCUA_PCAP_FILE
    value: /var/log/opcua/cap.pcap
  - name: OPCUA_KEYLOGFILE
    value: /var/log/opcua/keys.uakeys.json
```

```csharp
// Program.cs
HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Services.AddOpcUa().AddClient(_ => { });
builder.Services.AddPcapFromEnvironment();
await builder.Build().RunAsync();
```

For SSLKEYLOGFILE-style keylog only (no pcap):

```bash
export OPCUA_KEYLOGFILE=$HOME/opcua-keys.uakeys.json
```

## Security model

Anyone (process, container runtime, host operator, sidecar) who can set
these env vars can divert OPC UA channel keys and traffic to
attacker-controlled paths. `Docs/PacketCapture.md` gains a security note
and an operator-checklist item that ties this to the existing security
guidance: the destination path / directory must be operator-controlled
and protected with file-system permissions in addition to the binding's
own `0600`-on-Unix file mode.
2026-06-20 21:23:08 +02:00
Marc Schier f358078b1c [Client] Central channel manager: ref-counted shared channels, coalesced reconnect, IRetryBudget (#3288) (#3852)
# Description

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

## What lands

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

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

### Three-state lifecycle gate

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

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

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

### ManagedSession integration

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

### Discovery / Registration / GDS

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

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

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

### DI

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

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

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

### Tests

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

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

### Docs

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

## Carry-forward (separate PRs)

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

## Related Issues

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

## Checklist

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

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

## Public API

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

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

## Engine — batched apply

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

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

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

## State ownership

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

## Save / load (closes #3834)

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

## Error handling

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

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

## Spec consultation

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

## Tests

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

## Documentation

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

## Related Issues

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

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-12 10:38:41 +02:00
Marc Schier 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 49305d9085 V2 subscription engine close gap for test and feature parity with classic engine (#3824)
# Description

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

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

## Fixes

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

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

**Pattern.** A writer `ManagedSession` increments a `uint32` counter and
writes it into `Scalar_Static_Mass_UInt32_UInt32_00` every 250ms. A
subscriber `ManagedSession` runs a V2 subscription with a single
monitored item on the same node. `MonotonicCounterHandler` asserts each
received sample is *strictly greater than* the previous one. V2
`ISubscriptionNotificationHandler` per-subscription ordering guarantee.
Skipped values (sampling rate < write rate) are tolerated; duplicates
and reorderings are violations.
2026-06-03 04:39:08 +02:00
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 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 3e5f129c6f Enable nullable in Stack, Libraries (9), and Applications projects (#3732) 2026-05-17 12:55:24 +02:00
Marc Schier 96f8964704 Fix erroneous debug.assert in SessionManager (#3759)
* Erroneous debug.assert in SessionManager
* Improve build pipelines
2026-05-13 18:05:39 +02:00
Marc Schier d14cd10a86 Model dependency annotations using source generator + flaky test fixes (#3687)
* Fix publish/dispose race that hangs Client tests

Subscription.Dispose pre-disposed m_publishTimer and m_messageWorkerCts
before calling ResetPublishTimerAndWorkerState, causing Cancel() to throw
ObjectDisposedException and leaving the publish worker stuck. Removed the
duplicate disposal so the reset method is the single owner of those resources,
and guarded Cancel/Dispose with try/catch ObjectDisposedException for defense
in depth.

Session.OnPublishComplete could run after Session.Dispose disposed
m_reconnectLock, throwing ObjectDisposedException from SemaphoreSlim.Wait()
and re-arming via the throttle path indefinitely. Added an early Disposed
guard, wrapped Wait/Release in try/catch ObjectDisposedException, and added
a Disposed guard in QueueBeginPublish so the throttle cannot reschedule
publishes after disposal.

Fixes the macOS net10.0 hang seen in Azure pipeline build 13257
(TransferSubscriptionAsync, ReconnectWithSavedSessionSecretsAsync).

* fix lockout test and use UserName token for SuccessfulAuthenticationClearsFailedAttempts test

After c1e11b1ab made anonymous activations no-op for clearing the lockout
counter, the test must use a real (non-anonymous) credential to demonstrate
that successful authentication resets failed attempts. Use user1/password
with PolicyId=1 (UserName_1 in the reference server config).

* Fix DirectoryCertificateStore cache staleness across instances

When two DirectoryCertificateStore instances point at the same directory (e.g. a CertificateValidator's internal store and a separate store opened by a caller), the directory LastWriteTimeUtc seen by one DirectoryInfo handle does not reliably reflect modifications made through another handle on Windows, even after Refresh(). The mtime-based cache check therefore returned stale entries indefinitely, causing VerifyRejectedCertsDoNotOverflowStoreAsync to flake when the test fixture's RejectedStore failed to observe deletes performed by the validator's internal store.

Add a cheap on-disk file count cross-check in the cache-hit branch: if the count of cert files on disk does not match the cached entry count, force a reload. This avoids re-parsing every certificate when nothing has changed but reliably detects external modifications.

* test: fix keep-alive mock to return Int32 ServerState

The mock for the Server_ServerStatus_State read returned new Variant(0u)
(UInt32). The keep-alive worker expects Int32 (ServerState enum is
wire-encoded as Int32), so ClientBase.ValidateDataValue logged a
BadTypeMismatch warning on every keep-alive throughout the test run.

Real servers always emit Int32 for enum variables (BinaryEncoder
downgrades the enum encoding byte to Int32), so the warning was purely a
test-mock artifact. Switch the mock to new Variant((int)ServerState.Running)
to eliminate the spurious warnings.

* Source generator: emit and consume ModelDependencyAttribute. Emit one [assembly: ModelDependencyAttribute(...)] per model the
  generated assembly emits and per upstream model it transitively
  consumes (new ModelDependencyGenerator in
  Tools/Opc.Ua.SourceGeneration.Core).
* Replace the unused PEReader-based ReferenceMetadataScanner with a
  Roslyn IAssemblySymbol.GetAttributes() scan
  (ReferencedModelDependencyScanner in
  Tools/Opc.Ua.SourceGeneration). Hooks the per-symbol incremental
  cache, works for both PortableExecutableReference and
  CompilationReference, AOT-safe.
* Wire the scanner result through ModelSourceGenerator into
  ModelCompilation. The reduced URI-keyed map is passed to the public
  Generators.GenerateCode extension methods.
* NodesetFileCollection.GetDesignFileListForModel now consults the
  referenced-model map, so transitive nodeset dependencies that are
  satisfied by a referenced assembly no longer raise the
  missing-dependency error.
* Override resolution: when a local AdditionalFile and a referenced
  assembly both declare the same model URI under the same C# prefix
  the local generation is silently skipped (avoids CS0101); different
  prefixes still generate so downstream consumers can intentionally
  override types under a different namespace.
* New diagnostics MODELGEN010 (override skipped) and MODELGEN012
  (multiple referenced assemblies provide same model URI -> tie-break
  on highest version+publication date).
* Bump asserted GeneratedSources counts in ModelGeneratorTests for the
  new *.ModelDependencies.g.cs files (one per generated model).
* Fix unrelated CS1574 broken cref in NodeId.TryParseLongForm xmldoc
  that was blocking the Opc.Ua.Types build.
* Document the new flow in Docs/ModelDependencies.md.
* tests for ModelDependencyAttribute flow

* Refactor recently added "parsing" constructors to ParseLongForm static method. Method delegates to Parse(IServiceMessageContext)

NodeId, ExpandedNodeId and QualifiedName ParseLongForm overloads are now thin wrappers around Parse(context, options) using NodeIdParsingOptions with RequireResolvedUris=true and updateTables=false. Adds the FallbackToStringIdentifier opt-in to preserve the legacy lenient quirk for callers that need it (off by default for strict parity).

Tests for the refactored long-form parsers were added to the existing per-type test fixtures.

* Update nuget dependencies to latest
2026-04-22 10:15:12 +02:00
Marc Schier 09e44453a4 Generate type safe (method) proxies for ObjectTypes inside models (#3685)
Add interop test agent

The new stack-pass ObjectMethodProxyGenerator emits proxies that derive
from Opc.Ua.ObjectTypeClient and reference Session.MessageContext.NamespaceUris.
Neither type exists in the in-memory test compilation, which only references
Opc.Ua.Types.dll. Moving ObjectTypeClient to Opc.Ua.Types would cascade ISessionClient,
IClientBase, ITransportChannel and the generated service interfaces with it, so the
type stays in Opc.Ua.Core and we add a minimal stub to OpcUaCoreStubs instead.

Wire Microsoft.Extensions.Options 10.0.5 + DependencyInjection.Abstractions 10.0.5 into Directory.Packages.props and the Gds.Client.Common project to enable upcoming options-driven configuration work.

 Modernize Opc.Ua.Gds.Client.Common (DI, options, interfaces, async dispose)
2026-04-22 05:59:06 +02:00