110 Commits

Author SHA1 Message Date
Marc Schier b292e243c6 Add trusted NuGet release workflows (#4312)
## Summary

- replace the preview-only workflow with `nuget-publish.yml`, running on
every branch push, `v*` tag, and manual dispatch
- build and sign all Release packages plus genuine `.Debug` packages,
publish normal packages to GitHub Packages, and retain exact signed
package and symbol bytes
- add manual `release.yml` promotion from a successful `release/*`
publish run to nuget.org through `NuGet/login@v1` trusted publishing
- validate package identities, versions, signatures, symbols,
duplicates, and SHA-256 hashes before publication and promotion

## Signing configuration

All required repository Actions secrets are configured, including
`OPCFOUNDATION_NETSTANDARD_SNK_BASE64`, which was transferred directly
from Azure DevOps Secure Files without placing the private key in a
pipeline artifact.

## nuget.org setup

The account-side trusted publishing policy must use:

- Repository Owner: `OPCFoundation`
- Repository: `UA-.NETStandard`
- Workflow File: `release.yml`
- Environment: `release`

The existing `release` environment contains `NUGET_USER` and is
restricted to `release/*` branches.

## Validation

- parsed both workflow YAML files against the GitHub workflow schema
- parsed all embedded PowerShell and the shared package validator
- packed real Release and Debug `Opc.Ua.Types` packages and validated
their identities, matching versions, symbols, and `.Debug` family
- verified all seven signing secret names and removal of temporary key
material

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d639a224-b97c-4dbf-ba98-f99e6e3f6b9a
2026-08-26 16:16:55 +00:00
Marc Schier 06ae192084 Fix CI failures tracked by #4299 (#4308)
## Summary
- stabilize the History, authentication lockout, Robotics, redundancy,
and DI tests reported in #4301-#4305
- fix the closed-transport buffered-write completion deadlock that hung
`ConnectMultipleSessionsAsync`
- add macOS crash diagnostics for #4306 and retain evidence for
root-cause analysis
- run macOS tests and NativeAOT in GitHub Actions pull requests, using
supported Intel and ARM64 runners, while keeping every Azure macOS
definition disabled by default
- move all remaining source-level `InternalsVisibleTo` declarations into
project files so signed OpenUSD builds receive the repository public key

## Local validation
- MCP PubSub: 29 passed
- History focused tests: 3 passed
- Sessions lockout: 3 passed
- Sessions multiple-session regression: 3 passed
- deterministic binary client channel tests: 22 passed
- Robotics runtime: 34 passed
- Redundancy focused tests: 4 passed
- OpenUSD net10.0: 914 passed
- affected OpenUSD projects build on net10.0 and net48

Refs #4299, #4300, #4306

Fixes #4301
Fixes #4302
Fixes #4303
Fixes #4304
Fixes #4305

---------

Copilot-Session: 7cb486de-8b4c-43c5-a900-e7d2e0ad04a8
2026-08-22 19:30:19 +00:00
Copilot d525bc3f28 Bound reconnect cleanup and organize samples by domain (#4276)
# Description

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

## Reconnect cleanup

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

## Sample organization

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

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

## Validation

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

## Related Issues

- Fixes #4273

## Checklist

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2026-08-21 04:35:47 +00:00
Marc Schier 80db326567 Fix MCP diagnostics tool contracts and capture failures (#4241)
# Description

Fix the MCP packet-diagnostics tool contract and the capture failures
reported in #4238.

- Accept and emit canonical hyphenated capture source and format names
while preserving CLR-name and integer compatibility.
- Restore typed enum constraints in generated MCP schemas and surface
actionable diagnostics errors to MCP clients.
- Stop `list_interfaces` from reading `LinkType` on unopened SharpPcap
devices.
- Close failed SharpPcap handles before retrying NIC opens and report
permission failures immediately.
- Normalize capture roots, remove duplicated path segments, and use a
lazy secure per-user or atomic temporary fallback.
- Align MCP/diagnostics documentation and interop-agent guidance.

## Related Issues

- Fixes #4238

## Checklist

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

---------

Copilot-Session: 641a4356-aa8d-45e6-9077-eeb50b9b7e0a
2026-08-12 10:33:56 +02:00
Marc Schier 466c5cb982 Robot intent follow-ups: multi-controller executors, spec fixes, and an MCP tool library (#4195)
Follow-ups to #4165, plus the work that followed from re-checking this
branch against the specification. Everything started as a defect found
by review or by probing the running code; the specification is unchanged
except where noted.

The TLS certificate lifetime fix (#4196) and the OpenUSD work (#4198)
were both merged into this branch. #4196 has since landed on master, so
it no longer appears in this diff. #4198 was closed in favour of this PR
and its content is here.

The MCP trim/AOT work (#4228), the `DataGenerator` null `NodeId` fix
(#4227) and the NodeSet external-parent import fix (#4229) have since
been merged in as well. All three are closed in favour of this PR and
their content is here, described under *Making the MCP libraries trim
and AOT clean*, *A generator that returned the value you opted out of*
and *A NodeSet parent that lived outside the import batch* below.

## A two-arm cell could not be built

`IntentExecution` carried no controller identity, and there was
effectively one `IIntentExecutor` per server. An executor handed an
execution therefore could not tell **which arm** it was for, so a cell
with two `IntentControllerType` instances — an ordinary configuration —
was not expressible through the builder or DI.

Both halves are addressed:

- `IntentExecution` now carries `ControllerId` (`NodeId`, the OPC UA
identity) and `ControllerName`, so one shared executor can discriminate;
- DI gained per-controller registration —
`AddRobotIntentExecutor<TExecutor>(controllerBrowseName)` and an
instance overload — so two genuinely different executors can coexist.

Resolution order is `WithExecutor` → per-controller DI registration →
the single DI executor → a build-time throw. There is still **no silent
fallback** to a rejecting executor; that was a Critical defect fixed in
#4165 and it stays fixed. `IIntentExecutor` is unchanged, so this is
purely additive.

## Declarations that were quietly untrue

- **`Accepts<T>` silently discarded a redeclaration.** Declaring an
intent type twice — say to narrow `cancelSupported` on the second call —
kept the first, wider declaration and said nothing. That is the same
class of defect as the capability-honesty problems #4165 fixed
(`PauseSupported` declaring a pause the host could not perform;
`Aborting` degrading silently to `Buffered`). Identical redeclarations
remain idempotent; conflicting ones now throw `BadInvalidArgument`.
- **`RI-Interop-40010` assumed Annex B item 3 rather than checking it.**
It now requires exact set equality between the OPC 40010
`TaskProgramName` values and the Robot Intent `ProgramType.ProgramId`
values. Both sides are readable from the address space without
commanding the robot, which is what makes the requirement structural
under §12.1.

## Two things the specification requires that this did not do

Re-checking the implementation against the merged draft revision turned
up two genuine violations. Both are fixed, and the NodeSet is untouched
by either.

- **Every graphed mission reported `Failed` on success.**
`MissionRules.SelectTransition` returned `null` for two different
situations — a graph *terminus*, which is the normal success path, and a
branch point whose conditions all evaluated false — and the caller
collapsed both into `Failed`/`NoTransition`. A three-way result
(`Terminus` / `Selected` / `NoConditionMatched`) separates them, so a
mission that runs to its end now completes. The failure reason surfaces
through the inherited Part 10 result container rather than a new member.
- **Facet URIs were never published.** An upstream `may` became a
`shall` in the revision this branch adopts.
`RobotIntentConformanceUris.Facets` held 31 URIs with **zero**
production references — the set existed but nothing emitted it. The
published set is now derived from `RobotIntentFacetCalculator`, the same
authority that already computed `SupportedFacets`, rather than from a
second hand-maintained list that could drift; a reflection test pins the
two together.

## An easy client mistake

`RequestAuthorityAsync` returns a lease the caller must remember to
inspect. Forgetting makes every later submission fail with
`ControlNotOwned` for reasons that are invisible at the call site.
`RequireAuthorityAsync` throws instead, and names the current owner.
`RequestAuthorityAsync` is untouched — inspecting and branching is
legitimate.

## Letting an agent drive the robot

New `tools/Opc.Ua.Mcp.Robotics` gives a language model the intent API as
MCP tools: 36 across discovery, monitoring, direct control and missions,
behind a `Robotics` tool profile, composable into any MCP host exactly
as the existing OPC UA tool packages are. The `IntentViewerClient`
sample hosts them behind `--mcp` (stdio by default, HTTP when `--view`,
since MCP stdio and the in-process viewer would otherwise share stdout),
and the sample gained a pallet scene so an agent can pick and stack
parts and watch the result in the OpenUSD viewer. All the scene logic is
server-side — no viewer or connector changes.

Four gaps in the client API had to be filled first, since the tools are
a thin layer over it and could not have been written honestly without
them: `ReadStateAsync`, `ListOperationsAsync`/`ListMissionsAsync`,
controller-level
`CancelIntent`/`CancelAll`/`Pause`/`Resume`/`Retry`/`ReleaseControl`,
and `WaitForCompletionAsync`.

The tools refuse rather than paper over: a refusal from the server is
returned with its `IntentFailureEnum` and never retried, and command
authority is never acquired as a side effect of a tool call.

**One defect this surfaced:** `--profile robotics` shipped 36 tools that
could not do anything. Every robotics tool resolves a named OPC UA
session and only the connection tools can open one, so with no `Connect`
in the catalogue the whole profile failed. A test had pinned the defect
in place, asserting the profile did *not* contain `Connect` on the line
below the `diagnostics` assertion requiring it. Each package now
declares its own requirement in its registration extension rather than
the host special-casing it, so an application embedding
`Opc.Ua.Mcp.Robotics` directly gets a usable catalogue without
reproducing the host's pairing. `Full` still takes the connection tools
from the core package, so they are never registered twice. The
replacement test states the invariant — a profile exposing
session-scoped tools must expose the tools that open a session — rather
than the instance.

## Making the MCP libraries trim and AOT clean

*Merged from #4228.*

`Opc.Ua.Mcp.Core` is now `IsAotCompatible` on `net10.0`, and the
reflection the analysers then reported is gone. Turning the analysers on
is the point: until now they were not running on these libraries at all
— including on the tool library this branch adds.

- **`OpcUaJsonHelper.Serialize`** went through `JsonSerializer` (IL2026
/ IL3050). The conversion helpers in that class already reduce every OPC
UA value to a closed set — `null`, the primitive scalars, `string`,
`Dictionary<string, object?>` and lists thereof — so the JSON is now
written directly with a `Utf8JsonWriter`.

Output is unchanged, and that is asserted rather than assumed:
`OpcUaJsonHelperTests.SerializeMatchesTheReflectionBasedSerializer`
compares the new writer byte for byte against `JsonSerializer` for every
shape the helpers emit, including all numeric widths, the
custom-formatted scalars, nested sequences and strings that force the
encoder to escape.

One deliberate contract change: arbitrary object graphs are no longer
accepted. Serializing them needs reflection, so `Serialize` now throws
`NotSupportedException` naming the type instead of silently stringifying
it. No call site in the repository passes anything outside the closed
set.

- **`McpSchemaFilters`** used `JsonSerializer.SerializeToElement` on a
`JsonObject`; it now parses the node's own text, which needs no
reflection.

`Opc.Ua.Mcp.Diagnostics` is deliberately **not** marked. It depends on
SharpPcap and on the reflective service-call dissection in
`Opc.Ua.Core.Diagnostics`, neither of which is trim safe — which is why
that package does not opt in either. Two improvements are kept
regardless: `PacketDecodeTools` serializes `DecodedServiceCall` through
a source-generated `JsonSerializerContext`, and
`PacketCaptureTools.ListInterfaces` now carries the
`RequiresUnreferencedCode` / `RequiresDynamicCode` annotations that
`NetworkInterfaceEnumerator.ListLocalInterfaces` already declares, so
the constraint is visible to callers instead of implicit. Nothing is
suppressed.

Marking an assembly `IsAotCompatible` only proves its own IL is clean;
it says nothing about whether the library still functions once
reflection is gone. So `Opc.Ua.Aot.Tests` references `Opc.Ua.Mcp.Core`
and exercises it with JSON reflection disabled, stating what is and is
not true today:

- tool **results** serialize without reflection — this would fail
outright if the `Utf8JsonWriter` change regressed;
- tool **registration** still requires reflection, because the MCP SDK
builds each tool's input schema by asking `JsonSchemaExporter` for a
`JsonTypeInfo` of every parameter type. Without a source-generated
context covering the tool signatures — and with the injected
`OpcUaSessionManager` being described rather than treated as a service —
that throws `NotSupportedException`.

The second test asserts the throw. It documents the remaining gap and
turns red the moment someone closes it, rather than leaving it to be
found in a published binary. The package README says the same thing in
prose.

**Two pre-existing issues this surfaced, both outside this PR:** pulling
`Opc.Ua.Mcp.Diagnostics` into an AOT publish reports `IL2046` / `IL3051`
on `NicCaptureSource.StartAsync`, which is annotated
`RequiresUnreferencedCode` / `RequiresDynamicCode` while the
`ICaptureSource.StartAsync` it implements is not, and `IL2075` on
`ServiceCallReassembler.GetCollectionCount` / `GetPropertyString`, which
call `Type.GetProperty` on the result of `GetType()`. Happy to file
these separately if useful.

## A generator that returned the value you opted out of

*Merged from #4227. Fixes #4226.*

`GetRandomNodeId(useBoundaryValues: false)` could still return
`NodeId.Null` — one of the boundary values the same method offers when
boundary values *are* requested. A caller that opted out should not
receive one, and `GetRandomExpandedNodeId` builds on it and inherited
the same behaviour.

The `IdType.Opaque` branch is the practical path:
`GetRandomByteString()` draws its length with
`NextInt32(MaxStringLength)`, which is inclusive of zero, and a
zero-length opaque identifier in namespace 0 satisfies `NodeId.IsNull`.
With three namespaces and `MaxStringLength` at 10 that is roughly **one
call in 130**. A numeric identifier of `0` in namespace 0 is null too,
at around one in four billion.

The draw is now repeated while the result is null and boundary values
were not requested. Guarding the result rather than the individual
branches covers every identifier type, including the numeric case,
rather than only the one that happens to be reachable today. The
`useBoundaryValues: true` path is untouched, so boundary values remain
available when they are asked for.

Four tests in `tests/Opc.Ua.Core.Tests/Types/DataGeneratorTests.cs` loop
20,000 draws, because at one call in 130 a single draw proves nothing.
Against the **unmodified** generator they fail on draw 93 and draw 1346.
Two of the four exist to keep the fix honest rather than to test the
bug: `GetRandomNodeIdStillProducesEveryIdentifierType` catches a "fix"
that simply stopped emitting opaque identifiers, which would satisfy the
null assertions while silently narrowing the generator, and
`GetRandomNodeIdWithBoundaryValuesStillYieldsThem` confirms the leak was
closed rather than the feature removed.

**How it was found:** an intermittent downstream test failure. A test
server initialising a static `NodeId`-valued variable via
`TestDataSystem.ReadValue` occasionally stored `NodeId.Null`; the
encoder correctly writes that as JSON `null`, and a consumer asserting a
value was present failed about one run in five — never reproducibly in
isolation. It only surfaced under load because `UnsecureRandom.Shared`
is a fixed-seed, process-global generator shared between `DataGenerator`
and OPC UA transport jitter, so the number of draws preceding
address-space initialisation depends on what else is running in the
process.

## A NodeSet parent that lived outside the import batch

*Merged from #4229.*

`UANodeSetHelpers.LinkParentChildRelationships` builds its lookup table
from **only the nodes in the current import batch**, links a child when
the parent is found — and then cleared `instance.Handle`
**unconditionally**, which is exactly where `Import` had stashed the
authored `ParentNodeId`. A node whose parent lives in another NodeSet,
or is owned by another NodeManager, therefore lost that parent
**silently** at import, and nothing downstream could recover it.
`RuntimeNodeSetNodeManager.CreateAddressSpaceAsync` already has a step
that wires references to external node managers; the parent link never
reached it.

Such a parent is now recorded rather than discarded, and exposed through
`UANodeSet.TryGetUnresolvedParentNodeId`. A parent that resolved
normally inside the batch is **not** recorded, so a caller does not
re-wire references that already exist. The record lives in a
`ConditionalWeakTable`, so it does not keep an imported node alive and
does not widen `NodeState` — whose `Handle` is a general-purpose slot a
caller may legitimately use for its own purposes, and so is not a safe
place to leave the value.

`RuntimeNodeSetNodeManager` consumes it to report the one case that
cannot be repaired automatically. In NodeSet2 the `ParentNodeId`
attribute is a *hint*; the authoritative edge is the entry under
`<References>`, which the reverse-reference pass already turns into an
external reference. A node that names an out-of-manager parent but
carries **no such Reference** materializes with no path from that
parent, and no ReferenceType can be inferred to synthesise one — so it
is reported as an authoring defect rather than guessed at or silently
accepted.

This is deliberately **not** a fix for a specific address-space symptom.
It removes a silent loss of authored information and gives callers the
means to act on it. Whether any particular orphaned-node report is
caused by this path, or by namespace-index translation elsewhere, still
needs separate diagnosis.

**Changed while merging.** As authored in #4229 the accessor was
`GetUnresolvedParentNodeId`, returning `NodeId?`. `NodeId` is a readonly
struct implementing `INullable`, so that was a `System.Nullable<NodeId>`
wrapped around a type that already carries its own null representation —
which this repository excludes in favour of an `IsNull` check, and it
asks for `TryGet` over casting on struct types. The tests
correspondingly asserted `Is.Null` against a struct, which the testing
rules also exclude. It is now `bool
TryGetUnresolvedParentNodeId(NodeState, out NodeId)`, yielding
`NodeId.Null` when there is no unresolved parent. The API is unshipped —
it arrives with this branch — so nothing depended on the previous shape.
Both tests were re-verified by mutation after the rewrite: disabling the
record in `LinkParentChildRelationships` still fails
`ImportKeepsAParentThatIsNotInTheSameBatch`.

## Dependencies

- **OpenUSD to `0.7.0-alpha`.** All 11 issues we filed upstream are
fixed, so the workarounds they justified are gone and the parts of the
pump and generator samples that were blocked on them are implemented.
- **ModelContextProtocol to `2.1.0`** across all five MCP tool libraries
and the executable. No code changes were needed — the surface we use is
unchanged in 2.x — but central package management required a cascade of
pins, each commented where it sits. Both transports were verified
against a running server rather than assumed. MCP 2.x is stateless (no
`initialize`, no `Mcp-Session-Id`); an OPC UA session is application
state addressed by a caller-supplied name, which fits that model but
does assume process affinity, and that assumption is now written down
rather than implied.

## Infrastructure

The `code coverage` gate was measuring nothing on this branch and
reporting success. It is fixed and now enforced, which immediately
exposed real gaps — most usefully `RoboticsIntentJson`, which parses
agent-supplied JSON (untrusted input from a language model) and had no
tests at all. It is now at full line coverage in CI. Patch coverage is
**90.20%** against a 75% floor.

The `netstandard2.1` build leg was also failing. The MCP projects build
as empty shells under a legacy `CustomTestTarget`, and the viewer sample
referenced them on the target framework alone, so it tried to resolve a
net10.0 shell from net8.0. The sample now derives MCP support from
whether those projects are actually being built.

## From review

A review pass over the branch found two real defects, both fixed.
`RoboticsIntentJson` parses JSON written by a language model, so every
malformed shape it can be handed is reachable input; it indexed pose
arrays without checking their length and enumerated properties without
checking their kind, so a short or wrongly-typed array escaped as
`IndexOutOfRangeException` or `InvalidOperationException` rather than
the argument error the tool descriptions promise the agent. Separately,
`IntentOperationHandle` treated a zero `QueuePosition` as "not observed"
and discarded it, so a handle kept reporting the last queued position
after the server had said the operation left the queue.

The pose validation immediately caught a latent test defect: the pallet
scenario interpolated doubles into JSON with the current culture, so
under a comma-decimal culture `[0.12, 0.16, 0.28]` became a six-element
array. It passed only because that submission is expected to be refused
for want of authority, so the wrong coordinates were never used.

A third review finding — that a default-constructed
`RobotIntentControllerState` hands out a null `NodeId` — was checked and
rejected: `NodeId`, `QualifiedName` and `LocalizedText` are readonly
structs here, so `default` is the null value and `IsNull` is `true`. The
tests asserting that invariant are kept.

A second review pass over the merged branch found two more, both fixed
and both about an error surfacing as the wrong kind of failure rather
than about a wrong result:

- **Malformed agent JSON did not arrive as an argument error.**
`RoboticsIntentJson` turns every *validation* failure into an
`ArgumentException` — a wrong root kind, a missing property, a short
pose array — but three `JsonDocument.Parse` calls and the NodeId parse
were unguarded. A syntax error escaped as `JsonException` and an
unparseable NodeId as `ServiceResultException`, which are the two shapes
a language model is most likely to produce. That is the same defect
class as the pose/kind checking above: a caller separating bad input
from a genuine fault had to know to catch three unrelated exception
types. Parsing now goes through one helper that reports a syntax error
as an `ArgumentException` naming the argument, keeping the
`JsonException` as the inner exception so the position is not lost, and
the NodeId goes through `NodeId.TryParse` rather than `Parse`,
collapsing its two failure modes into one.
`MalformedIntentJsonIsRejected` had asserted
`InstanceOf<JsonException>`, pinning the behaviour rather than the
intended contract; it now asserts `ArgumentException` like every
neighbouring test, and three tests cover the newly guarded paths.
- **`ConditionalWeakTable.Add` throws on a duplicate key.** Not
reachable today, but `TryGetUnresolvedParentNodeId` documents `Handle`
as a general-purpose slot a caller may use for its own purposes — and a
caller doing exactly that before re-importing into the same collection
would take a hard throw from inside the import pipeline. Removing first
makes the most recent import win. `AddOrUpdate` would say this more
directly but does not exist on `netstandard2.0`.

## Deliberately not fixed here

`FastenIntentDataType.Joint` is refused when the Server exposes no OPC
40450/40451 model, and a client cannot discover that before calling.
That is **not fixable in this repository**: §5.4.2 makes `Joint` the
defining mechanism while §11.3 says the parameters otherwise stand
alone, and `IntentCapabilityDataType` declares capabilities at
intent-type granularity with no member-level mechanism anywhere in the
model. It is a specification gap, raised as S4 on
[marcschier/opcua-drafts#60](https://github.com/marcschier/opcua-drafts/pull/60)
rather than worked around with a bespoke mechanism the spec does not
define.

## Validation

`dotnet build UA.slnx -c Release` on the merged branch — **0 errors, 0
warnings**, with the AOT analysers now running over `Opc.Ua.Mcp.Core`.

| Suite (net10.0) | Tests |
|---|---|
| `Opc.Ua.Core.Tests` | 4247 (86 skipped) |
| `Opc.Ua.Robotics.Tests` | 540 |
| `Opc.Ua.OpenUsd.Tests` | 909 |
| `Opc.Ua.Tools.Tests` | 419 |
| `Opc.Ua.Server.Tests` — `RuntimeNodeSet` | 94 |
| `Opc.Ua.Robotics.Intent.Tests` | 22 |
| `Opc.Ua.Aot.Tests` — `McpAotTests` | 3 |

0 failures. `Opc.Ua.Tools.Tests` rose from 409 to 419 with the
serializer-equivalence tests from #4228 and the malformed-JSON tests
from the second review pass, and `Opc.Ua.Core.Tests` covers the
`DataGenerator` tests from #4227 and the import tests from #4229.
`UANodeSetHelpers` also passes on **net48** (5/5). The MCP
agent-usability grader scores Core 100 / Full 100.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Copilot-Session: a0718e8a-a158-45c5-929d-e1d6c0982431
2026-08-11 13:03:36 +02:00
Marc Schier b17007bd55 Stop the encoder tests flooding the VSTest channel with per-test output (#4219)
# Description

The `Core.Encoders` test job of run
[`31249610490`](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/31249610490)
printed nothing for 57 minutes and was killed by the 60-minute job
timeout. Issue #4213 recorded that as "hung at or before the first test"
— that reading was wrong, and the evidence to disprove it was already in
the job's own artifact.

The blame collector *had* written a hang dump and it *was* uploaded,
inside the ordinary `dotnet-results-Core.Encoders-*` artifact (11 MB of
it was the dump), where nobody looked for it. Opening it names the cause
precisely.

## What the dump shows

522 threads (408 background) in a process that finishes in 25 seconds
when healthy. Every test had already run; NUnit was in `OneTimeTearDown`
waiting on its event pump:

- the **event pump thread** is blocked in
`NUnitEventListener.TestFinished` →
`TestRunEventsHandler.HandleLogMessage` →
`LengthPrefixCommunicationChannel.Send` → `Socket.Send`, on a socket
that never drained. That send holds the channel's lock;
- `TestRunCache.SendResults` is blocked on that channel lock, and it
holds the cache lock;
- `TestRunCache`'s **one-second timer** then fired repeatedly into that
held cache lock, and the thread pool injected a fresh worker for each —
**399 threads** parked in `Monitor.Enter_Slowpath` behind a single lock;
- the NUnit run thread sits in `EventPump.Stop()` → `Thread.Join()`, so
the run can never complete.

## What put the socket under that much pressure

Volume of test output. Every encoder case dumps its input, its encoded
payload and its decoded result, the NUnit adapter forwards each captured
line to the runner as an individual message over that socket, and the
suite has 2929 cases. This is not marginal:

| | Before | After |
| --- | --- | --- |
| `Core.Encoders` TRX, green local run | **496 MB** | **4 MB** |
| `Core.Encoders` published CI artifact | **156 MB** | ~3 MB |
| Next-largest test job's artifact, for scale | 15 MB | 15 MB |
| `Client.ComplexTypes` TRX | 74 MB | 40 MB |

So the suite was shipping half a gigabyte of text through a socket, one
message per line, on every run and on every OS — and paying for it in
artifact storage even when nothing went wrong.

## The change

`EncoderCommon` gains a buffered `TestOutput` writer. The dumps are
collected per test and handed to `TestContext.Out` in `TearDown` **only
when the test did not pass** — which is the only time anyone reads them.
Nothing is lost: a failing test still reports its full
input/encoded/decoded dump under *Standard Output Messages*, verified by
injecting a deliberate failure. Explicit opt-ins
(`PrettifyAndValidateJson(..., outputFormatted: true)`) and the static
helpers keep writing immediately.

The same base class is shared by the `Client.ComplexTypes` fixtures, so
those are moved over too rather than left with half-buffered,
half-immediate output.

Two supporting changes:

- **`.github/workflows/buildandtest.yml`** — `timeout-minutes: 30` on
the `Test` step. The blame collector did its job here (dump at +50 min,
once the run went quiet), but the run still had not unwound 7 minutes
later when the *job* timeout cancelled everything, so the step needs a
bound below the job's. `Upload test results` already runs `if:
always()`, so the dump is still published.
- **`docs/DeveloperGuide.md`** — a "Test output" convention under
*Running tests*, so the next data-driven fixture does not re-introduce
this.

## Notes for reviewers

- `[Parallelizable]` on these fixtures is `ParallelScope.Self`, i.e.
fixture-level; tests inside a fixture stay sequential, so the
per-fixture buffer is as safe as the existing per-fixture
`RandomSource`/`DataGenerator` state that is already reset in `SetUp`.
- I separately confirmed that `--blame-hang-timeout` *does* fire on a
hang before the first test (minimal NUnit repro, dump at exactly the
configured timeout), which is what rules out the original "hung during
startup/discovery" theory independently of the dump.
- Local verification was scoped to the two affected suites rather than
the whole solution: `Opc.Ua.Core.Encoders.Tests` 2929/2929 on
**net10.0** *and* **net48**, `Opc.Ua.Client.ComplexTypes.Tests`
2773/2773 on net10.0, all building with 0 warnings. The full `UA.slnx`
run is left to CI.

## Related Issues

- Fixes #4213

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: da92151a-3da2-4caf-a39c-5556e56f0506
2026-08-09 21:49:05 +02:00
Marc Schier 0612a43fe4 Pluggable cryptography and hardware-held private keys (#4192)
Makes the stack's cryptographic operations replaceable - by another library, an offboard service, or hardware (TPM 2.0 / HSM / PKCS#11 token / cloud KMS) - with no performance cost in the default configuration, and extends this to certificates so a private key need never exist in process memory.

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

Fixes #4190
2026-08-09 12:40:26 +02:00
Marc Schier fb80bfdc82 [WotCon] Track WoT Connectivity/Binding 1.1 and add projections (Section 12) (#4182)
Follow-up to #4128, which merged the WoT Connectivity 1.1 work against
**draft2** of the companion specifications. Both drafts have moved
since, and this branch tracks them through to **draft3**, adds the
projection feature they introduced, and closes the gaps a bidirectional
re-audit found.

The specifications are the private
[`OPCF-Members/spec-drafts`](https://github.com/OPCF-Members/spec-drafts)
drafts of *OPC UA — WoT Connectivity 1.1* and *OPC UA — WoT Binding
1.1*. They are still drafts, which is why this is opened as a draft PR —
see [Status](#status) at the bottom.

## What changed in the specifications

Two revisions landed upstream while #4128 was in flight.

**draft2** replaced the `eventGroup` / `actionGroup` grouping constructs
with a single **projection / view** concept, added fifteen Section 6
vocabulary terms, and made two identifier rules normative. **draft3**
([spec PR #8](https://github.com/OPCF-Members/spec-drafts/pull/8))
un-deprecated the incorporated OPC 10100-1 v1.02 surface, restructured
the conformance clause into three profiles, and tightened two security
obligations.

The draft3 NodeSet is **structurally identical** to draft2 — I diffed
all 286 nodes node-by-node. Every change is metadata: 96
`ReleaseStatus="Deprecated"` attributes removed, `PublicationDate`
advanced, and `WOTC-ProjectionMaterialization` claimed by three Type
Nodes that previously implemented it in name only.

## Projections (WoT Binding Section 12)

The substantial addition. A projection document selects affordances from
other Thing Descriptions or Thing Models and resolves to a document of
its own; materialized, it becomes an OPC UA `View` that **creates no
affordance Node** and only `Organizes` what its sources already
materialized.

| Piece | Where |
|---|---|
| Document model and validation | `Opc.Ua.Types/Wot/WotProjection.cs`,
`WotProjectionModel.cs` |
| Four-stage resolution engine |
`Opc.Ua.Types/Wot/WotProjectionResolver.cs` |
| Materialization plan |
`Opc.Ua.WotCon.Server/Materialization/WotViewProjectionPlan.cs`,
`WotProjectionViewBuilder.cs` |
| Live address-space host | `WotProjectionViewNodeManager.cs`,
`LifecycleWotViewProjectionHost.cs`, `IWotViewProjectionHost.cs` |
| Node lookup seam | `IWotMaterializedNodeIndex.cs` |

Groups reached by `ua:Organizes` become organizational Objects rather
than nested Views, only the outermost materialization is a `View`, and a
member whose source is served by another server is omitted and reported
rather than recreated — the federation case the clause calls out.

### How this is validated

The specification publishes twenty worked examples, two of which are a
**golden pair**: a projection document and the resolved view it is
defined to produce. `WotSpecExampleTests` embeds all twenty and runs the
pair through the resolver, asserting against *the specification's own
expected output* rather than against my reading of the prose. That
single document exercises all three selection forms, the bulk naming
rule, the security closure naming and the provenance term.

This is the strongest evidence in the PR, and it is worth more than the
unit tests around it: it is the same check the upstream
`validate_local.py` performs.

## Other spec work

- **Section 6 vocabulary** — fifteen missing model and platform terms
with their validation rules (`WotNodeSetConverter.ModelVocabulary.cs`).
- **Portable identifiers** — the two rules release 1.1 made normative,
with `WotNodeSetConverterOptions.AllowNonPortableIdentifiers` as a
documented downgrade-to-warning switch for documents authored against
v1.00. Defaults to `false`.
- **xRegistry §6.9 identifier construction** (`XRegistryIdentifier.cs`)
— reverse-authority symbolic identifiers; all seven published worked
examples are test cases.

## Gaps found by re-auditing against draft3

I audited in both directions — every normative requirement to code, and
every `uav:` term in code back to a spec that defines it. Three
findings, all fixed:

1. **`Wot-Con 1.02` accepted anything well-formed.** draft3 makes format
validation part of the unit, because *WoT-Con Minimal* is one unit wide
and materialization projects a *valid* document. The 1.02 upload path
only deserialized into the `ThingDescription` POCO, which happily
accepts JSON that is not a WoT document at all — every member of that
type is optional, so `{}` deserialized and was materialized. It now
parses via `WotDocument.Parse` and requires the document to identify
itself by a non-empty `name` or `title`, returning `Bad_DecodingError`
and materializing nothing otherwise. The rule lives in
`ThingDescriptionFormatValidator` and is shared with
`CreateAssetForEndpoint`, whose auto-generated Thing Descriptions §11
also treats as untrusted input.
2. **Two `WoTFile` operations were ungated.** draft3 §11 names `Write`
and `CloseAndUpdate` alongside the five management methods; both reached
the materializer without an access check. `EnforceManagementAccess` is
now wired into `WotAssetFileManager`.
3. **Two invented `uav:` terms.** `uav:NodeSet2Preservation` was
declared and never used — removed. `uav:eventFields` is read by
`OpcUaBindingPlanner` and defined in *neither* spec, while a comment
claimed it came from OPC 10101. It still works for compatibility but is
now documented as a non-standard extension at both sites rather than
misattributed.

The SSRF prohibition is satisfied by construction: there is no HTTP
client anywhere in these assemblies, and all resolution goes through the
injected `IWotThingResolver`.

## Two ambiguities fixed in the specifications themselves

The audit turned up two places where a conforming implementer had to
invent an answer. Rather than pick silently, I took both upstream — they
are pushed to [spec-drafts PR
#8](https://github.com/OPCF-Members/spec-drafts/pull/8) (all checks
green) and are now normative in *Binding* §12.6 and *Connectivity*
§7.13:

- **`ViewVersion`** had one sentence saying only *that* it changes. It
now changes iff the resolved membership changes, is updated at commit
time alongside the `NodeVersion` stamping, and is a deterministic
function of the membership alone in a canonical order — so two servers
that resolved the same membership agree, which a per-server counter
would not across a redundant pair. Explicitly **not monotonic**: compare
for inequality only.
- **"Type-level view"** was used once in §12.2 and defined nowhere,
while every other statement of materialization was universal. §12.6 now
*defines* it rather than contradicting §12.2: the NodeClass is `View` in
both cases.

Both were already what this code did, so neither changed behaviour — but
the order-insensitivity the clause added was **not** covered by any
test, and `ViewVersionIsUnchangedWhenOnlyTheOrderOfTheMembershipChanges`
now holds it. It is mutation-verified: removing the ordinal sort turns
it red.

`docs/WotBindings.md` no longer lists these as points "the specification
leaves open".

## Validation

| Suite | Result |
|---|---|
| `Opc.Ua.Types.Tests` | 8525 ✅ (8518 on net48) |
| `Opc.Ua.WotCon.Tests` | 1057 ✅ (net10.0 and net48) |
| `Opc.Ua.WotCon.Bindings.Tests` | 559 ✅ |
| `Opc.Ua.XRegistry.Tests` | 196 ✅ |
| `Opc.Ua.Server.Tests` | 4027 ✅ |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3771 ✅ |
| `Opc.Ua.WotCon.Samples.Tests` | 9 ✅ |

The full `UA.slnx` builds with **0 errors and 0 warnings attributable to
this PR** on all 6 TFMs (`TreatWarningsAsErrors` is on).
`Opc.Ua.Server.Tests` and the source-generation suites are listed
because this PR now also touches `MasterNodeManager` and the WoT-Con
NodeSet.

## Compatibility

No public API is removed or broken. Everything added is new surface
using `ArrayOf<T>` / `ByteString` per the repo conventions, wired for DI
with a direct-construction fallback, and the one behavioural tightening
(`Wot-Con 1.02` format validation) rejects input that was never valid to
begin with.

One pre-existing issue is **not** addressed here because it came in with
#4128 and is out of scope: `WotAssetFileManager.OnCloseAndUpdate` uses
`.GetAwaiter().GetResult()`, which the repo rules ban. Worth a
follow-up.

## Landed since this PR was opened

- **DI upgraded to 1.05.0.** `ConnectsTo` was declared a subtype of
`HierarchicalReferences`, contradicting OPC 10000-100 §5.5 Table 48.
That is an upstream error in every official DI NodeSet up to 1.04,
corrected by the OPC Foundation in 1.05.0, so the fixture is replaced
with the verbatim 1.05.0 file rather than hand-patched. A guard test
pins the corrected form. This also closed a second gap: six DI types the
WoT model was missing are 1.05 additions.
- **The sample Pump has the type definition OPC 40223 declares for it.**
`Pump1.Identification` carried DI's `FunctionalGroupType` instead of
`PumpIdentificationType`. Every test, the sample documents and the
README agreed with each other about it, which is why it survived.
- **`WotPumpAddressSpaceComparisonTests`** compares this server against
`PumpDeviceIntegrationServer`, which builds the same OPC 40223 Pump from
generated NodeSets and hand-written wiring. An independent oracle rather
than a restatement of the same assumptions - it is what found the defect
above.
- **Error containment in `MaterializeProjectionViewsAsync`.** An
exception mid-loop discarded the applied View handles before they were
published, leaving View Nodes in the address space that nothing could
remove.
- **`HasWoTProjection`** is now created; it was documented and plumbed
but never wired.
- **The default in-memory View host** no longer deletes the View it has
just applied.
- **An undeclared NodeSet alias now says so.** It previously surfaced as
`IdentifierMissing`, naming neither the value nor the fact that an alias
was expected.
- **`ViewVersion` follows the algorithm the specification now states.**
My earlier spec text was rejected on review, correctly: it named neither
the function nor the canonical order it demanded, ignored that OPC
10000-3 §5.4 requires a value greater than zero, and its "when, and only
when" is unsatisfiable for a 32-bit value. §12.6 now specifies the
algorithm outright and this implementation follows it exactly, pinned by
an independently computed test vector. Implementing it surfaced a
further defect in the clause - joining members on U+000A is not
injective, because a NodeId string identifier may itself contain U+000A
- which is fixed in the specification by length-prefixing each member.
- **An auto-generated Thing Description is treated as untrusted input.**
§11 requires it; `CreateAssetForEndpoint` validated the endpoint but
handed whatever the discovery provider returned straight to the
materializer. The `Wot-Con 1.02` format rule now gates both paths from
one place.
- **Document bytes moved out of the registry snapshot** into a
content-addressed store, so a commit no longer rewrites the whole corpus
to edit one document. Writes stage and are promoted during the commit,
which preserves both durability-first ordering and the fail-closed rule
that a blob directory without a manifest means operator recovery.
Restoring streaming digest verification along the way closed a real gap:
content integrity had degenerated to a length comparison.
- **External references now reach a NodeManager registered later.** A
reference to a Node that does not exist yet is dropped rather than
queued, and the dynamic-registration path had no second phase to apply
them in, so registering a NodeSet before the one it points into lost the
forward edge permanently. Review of that change then found a state leak
in its rollback path, fixed with a red-to-green regression test.
- **`WOTC-Legacy` is renamed `Wot-Con 1.02`** in the specification and
here. Editorial: the NodeSet diff is `<Category>` values only.

## Status

Open for review. Two things may still move:

1. **The specifications are unpublished drafts.** Anything here can
change if the working group decides differently. The `ViewVersion`
definition has since been settled - §12.6 specifies the algorithm and
this follows it - but the same could happen to any other clause tracked
here.
2. **`uav:eventFields`** is kept as a documented non-standard extension.
That is a judgement call the WG may want to reverse, either by adopting
the term or removing it.

Two known items are recorded rather than fixed:

- `Pump1` has no hierarchical parent, so it is reachable by NodeId but
not by browsing down from `Objects`. The cross-NodeSet ordering defect
this was originally attributed to is fixed above, and cross-manager
references are now symmetric in both registration orders. The Pump
itself is still blocked on a different cause found while verifying that
fix: with the reference added, `DeviceSet` exists but the Pump has
**zero** inverse references, so the edge is dropped at *import* - before
any external-reference machinery runs. That is a separate defect in the
materialization path, not the one that was fixed, and it is left
recorded rather than guessed at.
- `WotAssetFileManager.OnCloseAndUpdate` uses
`.GetAwaiter().GetResult()`, which the repo rules ban. Pre-existing from
#4128 and out of scope here.

Review of the projection resolver and the materialization host is the
most valuable thing to look at; the vocabulary and identifier commits
are mechanical by comparison.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
2026-08-08 09:08:23 +02:00
Marc Schier 416d619c41 Split the CI matrix across both systems, split the required test gate from the advisory coverage check, and give background work an owner (#4158)
# Description

Two changes to how CI is routed and how it reports.

## 1. Split the CI matrix back across both systems

Flips both halves of the CI backend switch from `ado` to `actions`, so
the workload is shared instead of concentrated on the managed pool. The
switch is checked into source in **two places that must always be
flipped together**:

| File | Setting | Before | After |
| --- | --- | --- | --- |
| `azure-pipelines.yml` | `parameters.ciBuildBackend` | `ado` |
`actions` |
| `.github/workflows/buildandtest.yml` | `env.CI_BUILD_BACKEND` | `ado`
| `actions` |

| Work | GitHub Actions | Azure Pipelines (managed pool) |
| --- | --- | --- |
| All-TFM solution builds | `build-all-tfm-windows` + `-linux` | two-TFM
PR gate (`net10.0`, `net48`) |
| Test matrix | ubuntu, every test project | Windows `net48` + `net10.0`
fast PR legs |
| Native AoT | ubuntu | Schedule/Manual only |
| macOS | always (no managed-pool macOS image) | — |

## 2. Split the required gate from the advisory coverage check

The coverage gate job was appended to *every* test stage, so `Fast PR
test` emitted three sibling checks whose display names collided —
`Coverage Release (net10.0)` appeared twice, once for the Windows leg
and once for macOS, with nothing to tell them apart:

```text
✓ Coverage Release (net48)      55s
● Coverage Release (net10.0)
○ Coverage Release (net10.0)
```

That job also fused two unrelated concerns: whether the tests passed
(which must block a merge) and whether coverage met its thresholds
(which should not — the `net10.0` leg had `enforceCoverage: true`, so a
coverage dip blocked merges).

Both concerns now get a stage of their own, mirrored in both CI systems:

| Concern | Azure Pipelines | GitHub Actions | In the branch ruleset? |
| --- | --- | --- | --- |
| Every test passed | **`Tests passed`** stage | **`build-and-test
summary`** job | **Yes — required** |
| Coverage meets thresholds | **`Code coverage`** stage | **`code
coverage`** job | **No — advisory** |

The coverage check reports a clean failure when the thresholds are
missed, so the miss is visible, but it never blocks the merge.

Coverage is now merged **once** across every leg of the run instead of
once per leg, which also removes the `enforceCoverage` /
`publishCoverage` "exactly one invocation may set this" parameters.
Neither check re-runs any tests — the previous design re-ran the whole
suite in one job with the collector attached, which serialised a suite
that is deliberately fanned out and always exceeded the stage timeout.

### Coverage checks now exist in GitHub Actions

`build-and-test` already ran with `--collect:"XPlat Code Coverage"` and
uploaded the fragments, but nothing ever merged or evaluated them. It
now runs the same `check-coverage.ps1` against the same
`coverage-thresholds.json` as Azure. Since `CI_BUILD_BACKEND` defaults
to `actions`, that ubuntu matrix — which runs *every* test project — is
the fullest coverage signal in the repository.

### Reporting the numbers

`check-coverage.ps1` gained `-SummaryPath`, rendering a markdown report
that both systems surface:

- **GitHub Actions** — appended to the job summary and upserted as a
single **sticky pull-request comment**, updated in place on each run.
Threshold misses also appear as `::error::` annotations.
- **Azure Pipelines** — attached to the build summary via
`##vso[task.uploadsummary]`.

The report is a pass/fail table of the project floor, patch coverage and
baseline delta, with the uncovered changed lines listed by file in a
collapsible block. Rendered example:

> ## Code coverage
>
> ❌ **Coverage gate failed.** This check is advisory and does not
block the merge.
>
> | Check | Result | Threshold |
> | --- | --- | --- |
> | ✅ Project line rate | **72.10%** (40 731/56 496
lines) | >= 70.00% |
> | ❌ Patch coverage | **0.00%** (0/1 changed lines) | >= 75.00% |

The comment is fork-safe: a `pull_request` from a fork gets a read-only
token regardless of the `permissions:` block, so it degrades to the job
summary alone. The sticky comment uses `actions/github-script` rather
than a community action, keeping this workflow on first-party actions
only.

## Notable correctness details

These were found and fixed during review, and are the parts most worth a
second pair of eyes:

- **`dependencies.<stage>.result` resolves at *stage* scope only.** At
job scope `dependencies` refers to other jobs of the same stage, so
reading stage results from a jobs template would have silently returned
empty and the gate would have passed everything. `gate.yml` and
`coverage.yml` are therefore **stage** templates that derive their own
`dependsOn` from the same stage list they evaluate, so the two can never
drift apart. An unresolved result is a hard error.
- **Both rollups run on `not(canceled())` / `always()`**, not on
success. A check that reports **skipped** is treated by GitHub as
**satisfied**, so a required check that skips when its dependency fails
would wave a red build straight through.
- **`get-matrix.ps1` used to emit an empty matrix on a discovery miss.**
The downstream job is then skipped, a skipped job rolls up as a
*succeeded* stage, and the gate would have approved a run that executed
no tests. It now fails, with an `-AllowEmpty` opt-out.
- **Zero coverage reports used to be a green advisory check**, which
would hide a broken collector indefinitely. Both systems now distinguish
"the tests never ran" (warning) from "the tests ran and published
nothing" (error).
- The gate reads stage-level results rather than enumerating
matrix-generated job names, which change whenever a test project or
agent is added.

## Test-host hang and the reconnect defect behind it — both fixed

Flipping `CI_BUILD_BACKEND` to `actions` started exercising the ubuntu
matrix, which
`master` never runs, and it immediately surfaced two real defects.

**1. The test host hung for ten minutes.** `Assert.ThrowsAsync` blocks
the calling thread
with no timeout, and these tests drive a *fake clock* from that same
thread — so once the
reconnect parked on a fake-clock timer, nothing could ever advance it.
NUnit sat in
`WaitForCompletion` until `--blame-hang-timeout` killed the host,
aborting the run at 3902
of 4238 tests and taking every other test in the assembly down with it.
A hang dump named
the culprit exactly. Replaced with an awaited, bounded
`AssertThrowsAsync` helper (the
repo's own rules forbid sync over async). The run now completes in
**~1m29s** with all
tests executed.

**2. A budget-aware reconnect never returned.** With the hang gone, two
tests failed with
`Expected ServiceResultException but got TimeoutException`. `dumpasync`
on a hang dump gave
the whole chain:

```
ReconnectAsyncWithBudgetShrinksDelayToFitRemainingAsync
  → AssertThrowsAsync
  → ClientChannelManager.ReconnectAsync
  → ClientChannelManager.ReconnectLeaseAsync
  → ClientChannelManager.SwapFaultedEntryAsync   ← parked here
```

`master` (via #4128) added `IsTerminalReconnectRace` so a reconnect that
loses a race
against a concurrent close recovers on a freshly swapped entry. The
predicate only asks
whether the entry ended up `Closed`/`Faulted` with
`BadSecureChannelClosed` — which is
*also* exactly how a reconnect ends when the caller's retry budget or
the reconnect policy
is deliberately exhausted. So a budget-exhausted reconnect was misread
as a race: the
manager swapped the entry and started a **second, unbudgeted** cycle,
waiting out the swap
back-off first. `ReconnectAsync(channel, budget)` therefore could not
return within its
budget, and under a fake clock it never returned at all.

The fix requires the budget to have room left before treating a terminal
failure as a race:

```csharp
return !ct.IsCancellationRequested &&
    !budget.IsExhausted &&
    sre.StatusCode == StatusCodes.BadSecureChannelClosed &&
    entry.State is ChannelState.Closed or ChannelState.Faulted;
```

The parameterless `ReconnectAsync(channel, ct)` overload passes an
unlimited budget, so
genuine race recovery is unchanged. Verified locally: Core 4152, Client
2122 and Sessions
776 tests all pass, and the full GitHub Actions PR run is green across
56 jobs.

Two smaller fixes came along the way, neither weakening an assertion:

- `ObservableFakeTimeProvider` waiters made **relative** ("N more timers
from now") rather
than absolute ("the Nth timer of the run"). Absolute numbering let an
earlier reconnect
consume the slots, so a waiter completed before the awaited timer
existed and `Advance()`
  fired nothing.
- The raw worker thread in `NodeStateHandlerConcurrencyTests` was a
**foreground** thread
whose stop flag was read without a barrier and set only on the success
path — a failed
assertion leaked a spinning thread that pins the process at exit. Now
background,
  `Volatile`, stopped in a `finally`, joined with a bound.
## Background work now has an owner

The reconnect hang above was one symptom of a pattern the codebase had
**26 times** across seven assemblies: a bare `_ = Task.Run(...)`. Each
one handed work to the thread pool and forgot it — nothing observed the
exception if it threw, nothing bounded how many ran at once, and
disposal raced them, so a component could finish tearing itself down
while work it started was still touching its fields. Two of those sites
already carried a standing `// TODO: Await the task completion in
shutdown`.

`BackgroundTaskScope` (in `Opc.Ua.Types`) gives them an owner: schedule
with `Run`, drain with `DisposeAsync`, or signal-only with `Dispose`
where the owner's teardown is synchronous and awaiting would be sync
over async. `Run` never blocks and never throws — which matters, because
most of these call sites went to the thread pool precisely to escape a
lock they were holding. 11 unit tests cover drain, cancellation,
exception observation, the concurrency cap, post-shutdown rejection and
idempotent disposal.

Two of the fixes were more than re-wiring:

- **`SessionManager` / `SubscriptionManager` loops were structurally
unawaitable.** Both return `ValueTask`, so `Task.Factory.StartNew(() =>
...)` handed back a `Task<ValueTask>` that completes at the first yield.
Storing it would not have helped — awaiting it would only have awaited
the *scheduling*. Both now go through `AsTask().Unwrap()`, matching the
neighbouring `StartConditionRefreshWorker`.
- **`ChannelAsyncOperation` was swallowing subscriber exceptions.** Its
`doNotBlock` branch had no try/catch while the inline branch beside it
did. It is per-operation with no owner to hang a drain on, so it gained
the missing handler rather than a scope.

The Kubernetes readiness endpoint keeps a purpose-built semaphore
instead: an unauthenticated endpoint needs a hard cap on concurrent
handlers, not just an owner.

### ⚠️ Breaking: `ISessionManager.Shutdown()` is removed

Replaced by `ShutdownAsync(CancellationToken)`. There is no correct
synchronous way to wait for the session monitor loop, so leaving the
overload would only have preserved the race. This mirrors
`ISubscriptionManager`, which already had `ShutdownAsync`. Migration
notes are in `docs/MigrationGuide.md`; only one in-repo implementer
existed.

Also converted the injected `object partitionLock` shared by
`LogicalSubscription` and `CompositeMonitoredItemCollection` to
`System.Threading.Lock`, per the repo rule.

## Patch coverage now scales with the size of the patch

A flat floor over changed lines punishes small changes for arithmetic:
two uncovered lines in a four-line fix reads as 50 % and failed an 80/5
floor — which is exactly how this branch earned a red coverage check for
a four-line predicate change. A check that fires on changes nobody
considers undertested is one people learn to ignore.

| Coverable changed lines | Floor | Below it |
| --- | --- | --- |
| 1 – 10 | 50 % | warning, still passes |
| 11 – 100 | 60 % | warning, still passes |
| more than 100 | 75 % | **failure** |

Only changes past the last band can fail. Below it the author still gets
a warning naming the uncovered lines. Bands live in `patch.bands` in
`coverage-thresholds.json` and each carries its own `enforced` flag.
Verified end-to-end against synthetic repositories at every boundary.

## Codecov is back, as reporting

Dropped in #4122 when the in-pipeline gate replaced it. Keeping the gate
was right, but dropping Codecov also lost the PR comment, the
file-by-file diff view and the trend. It returns with both statuses
`informational: true` — two gates with two sets of thresholds would
eventually disagree, and `coverage-thresholds.json` stays the single
source of truth.

The upload is switchable and can never fail a build: `enableCodecov`
(Azure) and `ENABLE_CODECOV` (Actions), both defaulting on, both skipped
when `CODECOV_TOKEN` is absent as on fork PRs. Each system uploads the
single merged report it already builds, under its own flag. The old
`codecov.io/bash` uploader was sunset, so Azure uses the current CLI.

## Follow-up actions for maintainers

1. **Update the branch ruleset.** The per-leg `Coverage Release (…)`
checks are gone. Azure reports checks as `<pipeline> (<stage> <job>)`,
so the exact names are:

   | Check | Require? |
   | --- | --- |
| `OPCFoundation.UA-.NETStandard (Tests passed Verify stage results)` |
**yes** |
   | `build-and-test summary` | **yes** (already required) |
| `OPCFoundation.UA-.NETStandard (Code coverage Merge and evaluate)` |
no |
   | `code coverage` | no |
2. **Re-calibrate the thresholds.** `coverage-thresholds.json` is still
seeded from the last Codecov reading (73.61 %). The two systems now
report *different* numbers by design — Actions merges every project on
ubuntu, Azure only the Windows fast-PR legs, and scheduled runs read
higher still because the Debug / .NET 8 / .NET 9 / netstandard stages
also contribute. Re-seed from the GitHub Actions figure.
3. **Note:** a failed advisory coverage stage still turns the whole
Azure run red, which the README build badge reflects on `master`. That
follows from wanting a visible red X on a threshold miss; say the word
if you would rather it were a warning.
4. **The Azure agent pool is the slowest thing here.** Several runs on
this PR sat queued for hours, and one build failed with `Insufficient
system resources` while writing an XML doc file — an agent exhaustion
error, not a code error. It passed on re-run. Worth raising against the
`netstandard` Managed DevOps Pool capacity separately.
## Related Issues

_No tracking issue; this is CI infrastructure work requested directly.
It builds on the switch introduced in #4122._

## 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.
- No product code changed, so there are no unit tests to add. The new
logic was verified with purpose-built harnesses instead:
- `check-coverage.ps1` run for real against a merged Cobertura report on
all three paths — pass, threshold failure, and patch coverage with
uncovered changed lines (a scratch commit adding an uncovered method was
correctly resolved to `DataTypeException.cs:193`, reported 0 % patch
coverage and exited 1).
- The gate's accept/reject logic dry-run over `Succeeded` /
`SucceededWithIssues` / `Skipped` / empty / `Failed` / `Canceled` — 6/6
correct.
- `get-matrix.ps1` exit codes verified in clean processes: empty → 1,
normal → 0, `-AllowEmpty` → 0.
- All five pipeline/workflow files parse; both PowerShell scripts parse.
- Action versions checked against the GitHub API and pinned to current
majors.
- [x] I have added all necessary documentation.
- Rewrote the coverage section of `docs/DeveloperGuide.md` as *Required
checks and coverage*, covering which checks belong in the ruleset and
which must not, how coverage is measured, where the numbers appear, and
why the two systems disagree. Corrected the stale "BLOCKS" wording in
`coverage-thresholds.json`.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- CI YAML, two PowerShell scripts and documentation only; no compilation
involved.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- Not applicable — no product code changed. `Opc.Ua.Core.Schema.Tests`
was run repeatedly on net10.0 (119 passed) to generate the real
Cobertura fragments used to validate the gate.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
- Feedback on the path-relevance diff (now a merge-base diff) and the
target-framework numbering in `docs/DeveloperGuide.md` is fixed in
`15b8e38df`.
- A review pass over the gate/coverage split produced the four
correctness fixes listed above, in `863eefc4d`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26aa55a2-5731-4a4c-b61c-df2f7a785eca
2026-08-07 06:56:05 +02:00
Marc Schier 3713641479 Throttle client publish worker on undeliverable publish responses (+ CodeQL OOM fix) (#4121)
# Description

Fixes a busy-spin in the client publish worker that crashed the
**net48** CI test host (e.g. [build
16092](https://opcfoundation.visualstudio.com/opcua-netstandard/_build/results?buildId=16092&view=results),
`Test Release` reported *"The active test run was aborted. Reason: Test
host process crashed"* on
`SubscriptionManagerTests.PublishWorkerDefersDeletingUnknownSubscriptionWhileCreationIsPendingAsync`).

It also carries a small, self-contained CI fix for the repository-wide
CodeQL out-of-memory failure (see the last section).

## Root cause

In `SubscriptionManager.PublishWorkerAsync`, a publish response whose
`SubscriptionId` does not resolve to a subscription the manager owns set
`moreNotifications = true`. That flag **skips the ack-wait throttle**,
so the worker immediately re-issues `Publish`.

The condition is *persistent* — the server keeps answering with the same
undeliverable identifier — so the worker spun with **zero delay**,
logged at `Information` level on every iteration, and in the orphan
branch issued `DeleteSubscriptions` for the very same subscription on
every single response. With a transport that completes synchronously the
loop additionally never released its thread pool thread.

Measured on the unmodified code, for a single 485 ms test:

| Metric | Before |
|---|---|
| `DeleteSubscriptions` calls for one orphan | **16,500** |
| "deferred unknown subscription" log lines | **4,296** |
| TRX produced by that one test | **4.86 MB** (47% of the whole
2,124-test assembly) |
| Peak test host working set | ~528 MB |

On a slow 2-core hosted agent the polling loops stretch out, the spin
runs far longer, and the unbounded log/allocation growth takes the test
host down.

Beyond the CI symptom this is a real client defect: against a live
server the client would hammer it with `Publish`/`DeleteSubscriptions`
and flood the log whenever a subscription identifier cannot be resolved.

## Changes

`src/Opc.Ua.Client/Subscription/SubscriptionManager.cs`

- Undeliverable publish responses (retired identifier, identifier still
pending creation, or genuine orphan) now **always throttle** through a
new `DelayUnresolvedSubscriptionAsync` — an escalating, bounded backoff
(20 ms doubling to a 250 ms cap) — instead of republishing immediately.
- After deleting an orphan, its identifier is passed to
`RetireSubscriptionId`, so responses already in flight (or emitted
before the delete takes effect) are ignored rather than triggering
another delete. **One delete instead of thousands.**
- The deferral message is logged once per run of the same unresolved
identifier instead of once per response.
- The unresolved-response counters reset as soon as a response is
dispatched successfully.


`tests/Opc.Ua.Client.Tests/Subscription/Fakes/FakeSubscriptionManagerContext.cs`

- The call recordings were plain `List<T>` appended to from
publish-worker threads while the test thread read them. They are now
guarded by a `System.Threading.Lock` and exposed as snapshot
`IReadOnlyList<T>` getters.
- Added `DeleteCallsCount`, a locked count accessor, so polling wait
conditions do not allocate a snapshot per iteration.

`tests/Opc.Ua.Client.Tests/Subscription/SubscriptionManagerTests.cs`

- `PublishWorkerThrottlesWhileSubscriptionIdIsUnresolvedAsync` — bounds
the publish count over a 500 ms window; fails loudly on the old code.
- `PublishWorkerDeletesUnknownSubscriptionOnlyOnceAsync` — asserts
exactly one `DeleteSubscriptions` call for an orphan.

## Results

| Metric (single test) | Before | After |
|---|---|---|
| Deferred log lines | 4,296 | 3 |
| `DeleteSubscriptions` log lines | 16,500 | 3 |
| TRX size | 4,856,749 B | 6,466 B (**750×** smaller) |

The full-assembly TRX dropped from 10.30 MB to 4.92 MB.

No public API change; behaviour change is limited to the
undeliverable-publish-response path.

## CodeQL out-of-memory fix (`.github/workflows/codeql-analysis.yml`)

The CodeQL job has been failing repository wide — on `master` and on
every open PR — with:

```
CodeQL is out of memory. Try running CodeQL on a larger runner (hosted or self-hosted).
Error running analysis for csharp: ... codeql database interpret-results --threads=4 ...
Exit code was 99
```

The CodeQL step builds `UA.slnx` without `CustomTestTarget`, so
`targets.props` falls through to its default and multi-targets up to
**six** frameworks (`net472`, `net48`, `netstandard2.1`, `net8.0`,
`net9.0`, `net10.0`). CodeQL therefore extracts every source file once
per framework. The same multiplier applies to source-generated code,
which `EmitCompilerGeneratedFiles` materializes on disk —
`Opc.Ua.Core.Types` alone emits **52 MB** of generated C# for a *single*
framework. The database and source archive grew until
`interpret-results` exhausted the runner's memory.

- Pin the CodeQL build to `-p:CustomTestTarget=net10.0` so every source
file is extracted once instead of ~6×. Verified locally: `UA.slnx`
builds with **0 errors** under that property.
- Limit the analyze step to `threads: 2`, bounding the peak memory of
the step that failed.

This is unrelated to the publish-worker fix and is happy to be split out
if maintainers prefer.

## Review feedback addressed

- Snapshot properties are read once into a local before asserting, so
two reads cannot observe different snapshots.
- Wait conditions poll the new non-allocating `DeleteCallsCount` instead
of `DeleteCalls.Count`.
- The sync root is declared as the fully qualified
`System.Threading.Lock`. An `object`-typed sync root was **not** used:
repository coding standards forbid it, and
`Opc.Ua.Types/Polyfills/System.Threading.cs` already backports a public
`System.Threading.Lock` for every TFM below net9.0.

The branch was rebased onto current `master` (`8a10139b8`).

## Related Issues

- No tracked issue; reported via the failing `Test Release` (net48) leg
of build 16092.

## 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. _(Internal behaviour
fix, no API or documented-behaviour change.)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(0 warnings, 0 errors on net48 and net10.0.)_
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
  - `Opc.Ua.Client.Tests` **net48** (CI filter): 2,124 passed, 0 failed
  - `Opc.Ua.Client.Tests` **net10.0**: 2,121 passed, 0 failed
- `Opc.Ua.Subscriptions.Tests` **net10.0** (integration): 602 passed, 0
failed
- `SubscriptionManagerTests` fixture run 5× on net48: 36/36 every run
(no flakiness)
- [x] I have addressed **all** PR feedback received.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6615d151-e4af-43c7-8a32-4531842e7461
2026-08-03 09:50:41 +02:00
Marc Schier ea0f266b66 Move build and test to the Azure DevOps managed pool and replace Codecov with an in-pipeline coverage gate (#4122)
# Description

Moves the all-target-framework build, the cross-platform test matrix and
the Native AoT run from GitHub Actions onto the Azure DevOps
`netstandard` Managed DevOps Pool, and replaces Codecov with a coverage
gate that runs inside the pipeline.

## 1. CI backend switch

One conceptual switch decides who owns that work. It is checked into
source in **two places that must be flipped together**:

| File | Setting | Default |
| --- | --- | --- |
| `azure-pipelines.yml` | `parameters.ciBuildBackend` | `ado` |
| `.github/workflows/buildandtest.yml` | `env.CI_BUILD_BACKEND` | `ado`
|

Setting both to `actions` restores the previous behaviour exactly.

Azure Pipelines gains what it was missing to take the work over:

- A **Linux all-TFM `sln.yml` leg**, replacing `build-all-tfm-linux`.
`sln.yml` gets a `configurations` parameter so this leg builds `Release`
only instead of doubling the managed-pool job count.
- A **Linux agent in the `net10.0` `testfastpr` matrix**, replacing the
ubuntu `build-and-test` job.
- **Native AoT and Code Coverage now gate pull requests** instead of
running only on Schedule/Manual. The coverage stage drops its
`testdebug` dependency, which is Schedule/Manual-only and would
otherwise have skipped coverage on every PR.
- On a PR, Windows builds the .NET Framework TFMs and Linux the modern
ones — mirroring the split the GitHub Actions jobs used — so all seven
TFMs stay covered without fanning every TFM across both pools.

Two things are deliberately **not** covered by the switch:

- **macOS** stays on GitHub-hosted runners, because Managed DevOps Pools
provide no macOS image.
- **`master378` and `develop/*`** keep running the GitHub Actions jobs,
since Azure Pipelines only builds `master`/`main` from this file.

GitHub Actions does not expose the `env` context to job-level `if:`
conditions, so the switch is resolved once in the existing `discover`
job and re-exported as job outputs.

A new **`build-and-test summary`** job gives branch protection one
stable check to require, instead of the generated matrix job names. It
treats an intentionally skipped job as success but still fails on a real
failure or cancellation.

The `paths:` allow-list was moved off the `pull_request` trigger and
into `discover`. A workflow filtered out by `paths` never reports its
checks, and a required check that never reports leaves a PR permanently
*"Expected — waiting for status to be reported"*. That would have made
the routine "ratchet the coverage floor" PR (which touches only
`coverage-thresholds.json` and docs) unmergeable.

## 2. Coverage gate replacing Codecov

Codecov is removed entirely — `codecov.yml`, both uploads, the badge,
and the token usage. `.azurepipelines/check-coverage.ps1` runs at the
end of the `Code Coverage` stage against the Cobertura report
ReportGenerator already merges, driven by `coverage-thresholds.json`:

| Check | Behaviour |
| --- | --- |
| Project floor | **Blocking** — total line and branch rates against an
absolute floor. |
| Patch coverage | **Blocking on PRs** — changed lines must reach
`patch.target`, tolerating `patch.threshold` percentage points (the same
semantics `codecov/patch` had). |
| Baseline delta | **Advisory** — reports the change against the
recorded baseline, never fails. |

The project rate is recomputed from the per-line data rather than read
off the Cobertura root element, so the `ignore` globs carried over from
`codecov.yml` apply to the project floor as well as to the changed-lines
gate — otherwise samples and generated code would count towards one gate
but not the other.

`testcc.yml` gains an explicit `checkout: self` with `fetchDepth: 0`,
because the default shallow clone cannot diff against the base branch.

## Follow-up actions for maintainers

Two items cannot be automated from source:

1. **Enable the `/azp run` gate.** In the Azure DevOps portal: pipeline
→ **More actions** → **Triggers** → **Pull request validation** →
**Require a team member's comment before building a pull request**,
scoped to *Only on pull requests from non-team members*. This makes
Copilot-authored and outside-contributor PRs wait for a maintainer's
`/azp run`, mirroring the approval GitHub Actions already requires. The
documentation added here describes the workflow and the
private-org-membership gotcha; **the setting itself still has to be
switched on.**
2. **Update branch protection and secrets.** Drop the `codecov/project`
and `codecov/patch` required checks, require `build-and-test summary`
and the ADO coverage check instead, and remove `CODECOV_TOKEN` from the
GitHub repository secrets and the ADO variable group.

Also note the thresholds in `coverage-thresholds.json` are seeded from
the last Codecov reading (73.61 %) with headroom, because the
in-pipeline number had not been observed yet. The first full `Code
Coverage` run on master should confirm it and the floors should be
tightened then — the gate prints the exact figure it computes.

## Related Issues

_No tracking issue; this is CI infrastructure work requested directly._

## 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.
- No product code changed, so there are no unit tests to add. The new
logic was verified with purpose-built harnesses instead:
- All 8 branch/event/backend combinations of the `discover` switch,
confirming `master378` and `develop/*` are untouched and that `actions`
restores the old behaviour.
- 12 scenarios for `check-coverage.ps1`: the project floor, the patch
gate, ignore globs applying to *both* gates, three path-normalization
shapes, and the advisory delta.
- 14 path-relevance cases confirming the in-job filter matches the
retired `paths:` allow-list exactly.
- End-to-end against a **real** ReportGenerator report: correctly
resolved `src/Opc.Ua.PubSub/DependencyInjection/PubSubBuilder.cs`, found
exactly the four modified tracked lines and reported 50 % patch coverage
(2 covered, 2 uncovered).
- [x] I have added all necessary documentation.
- New **Continuous integration** section in `docs/DeveloperGuide.md`, a
CI note in `CONTRIBUTING.md`, and links from `docs/README.md`. Also
corrected the CI matrix description, which claimed the PR gate ran net48
+ **net8.0** — it runs net48 + **net10.0**.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- `dotnet build src/Opc.Ua.Server` → 0 warnings, 0 errors. No product
code changed.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- Not applicable — no product code changed. `Opc.Ua.Server.Tests` was
run on net10.0 during development (3480 passed; the single
`ServerFluentApiHostingTests` failure is pre-existing and reproduces on
master).
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
- A review pass over this branch flagged the `paths:`-filter deadlock
described above; it is fixed in `1926e240e`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 181ba7ea-c72f-42d7-a737-00a3faadaa17
Copilot-Session: 26aa55a2-5731-4a4c-b61c-df2f7a785eca
2026-07-31 21:06:25 +00:00
Marc Schier 76d33cb139 Remove SourceGenerator.Foundations, ship analyzers under Roslyn-versioned folders for VS2022 and VS2026, and validate the packages (#4120)
# Description

The two OPC UA source generators were built on
`SourceGenerator.Foundations` (SGF), whose only job was getting the
generator's dependency closure loaded inside the compiler. It does so
with a **process-wide, first-generator-wins `AppDomain.AssemblyResolve`
hook backed by a non-concurrent `Dictionary` mutated from `AssemblyLoad`
callbacks**. Concurrent compilations in a shared `VBCSCompiler` race it,
which is the root cause of the intermittent clean-build failure:

```
CSC : error CS8784: Generator 'StackSourceGeneratorHoist' failed to initialize. ...
Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly
'SourceGenerator.Foundations.Contracts, Version=2.0.14.0, ...'
```

The repository already solves that problem deterministically — #4041
made the NuGet packages ship the full closure under
`analyzers/dotnet/.../cs`, and `Directory.Build.targets` registers the
same closure as `Analyzer` items for in-repo `ProjectReference`
consumers. SGF was therefore pure liability and is removed.

## What changed

**1. SourceGenerator.Foundations removed.** `ModelSourceGenerator` and
`StackSourceGenerator` are plain `[Generator(LanguageNames.CSharp)]
IIncrementalGenerator` implementations again. SGF's implicit crash
handling is replaced by an explicit `SourceGenerator.Guard` that reports
`MODELGEN003` / `STACKGEN003` and rethrows cancellation. Its logger sink
was already dead code and is dropped.
`Opc.Ua.SourceGeneration.Stack.dll` shrank from **18 MB** (embedded
closure) to **29 KB**.

**2. Analyzers now ship under Roslyn-versioned folders.**
`analyzers/dotnet/roslyn4.14/cs` and `analyzers/dotnet/roslyn5.0/cs`
instead of `analyzers/dotnet/cs`, driven by `roslyn.props` at the
repository root, with new `*.Pack` projects owning the package identity.
The versioned folder matters: a host older than the band **skips** the
analyzer instead of loading it and failing inside generator
initialization.

**3. Dependencies flow automatically.** Both delivery paths discover the
payload by globbing the generator's output directory, so adding a
package to a generator's dependency tree requires **no** `.csproj`,
`.targets`, `.nuspec` or validation-script edit. Verified by temporarily
adding a `PackageReference` and watching it appear in the payload with
no other file touched.

## Two Roslyn bands: VS2022 and VS2026

Analyzers ship twice, once per band. The .NET SDK loads the highest band
its compiler supports and **silently skips** anything above it, so a
single-band package leaves every consumer below that band with no
diagnostics and no generated code.

| Band | Built against | Loaded by |
| --- | --- | --- |
| `roslyn4.14` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | Visual Studio
2022 17.14+ / .NET 9 SDK |
| `roslyn5.0` | `Microsoft.CodeAnalysis.CSharp 5.0.0` | Visual Studio
2026 18.0+ / .NET 10 SDK |

Getting this wrong is invisible, which is most of why it took three
attempts. The rule:

> An analyzer must reference the **lowest**
`System.Collections.Immutable` / `System.Reflection.Metadata` across all
hosts it claims to support, and must never ship them. .NET binds a
reference *upward* but never downward.

Every failure mode is a compiler **warning**:

| Diagnostic | Cause |
| --- | --- |
| `CS9057` | Built against a newer Roslyn than the host, so it is
skipped entirely |
| `CS8784` | Loaded, then `MissingMethodException` - a shipped S.C.I
bound a second `ImmutableArray<T>` |
| `CS8032` | Analyzer instance could not be created |

This PR first shipped a `roslyn4.8` band that **never loaded** -
confirmed against a real Roslyn 4.8 compiler:

```
warning CS8784: Generator 'ModelSourceGenerator' failed to initialize ...
MissingMethodException: Method not found:
'IncrementalValueProvider`1<ImmutableArray`1<!!0>>
 IncrementalValueProviderExtensions.Collect(IncrementalValuesProvider`1<!!0>)'
```

That band was removed, and **this is not a regression**: master had the
same defect, pinned to Roslyn 4.14 while referencing
`System.Collections.Immutable 10.0.0.0`, so no 4.x host ever worked
there either.

The band that landed instead is `4.14`, and it is nearly free:
`Microsoft.CodeAnalysis 4.14` depends on the **same S.C.I 9.0.0 as
5.0.0** and its `csc` runs on `net9.0`. Both bands therefore share
`$(RoslynRuntimeVersion)`, so `Opc.Ua.Types` and
`Opc.Ua.SourceGeneration.Core` are compiled **once** and serve both;
only the five Roslyn-referencing assemblies get a second build, each
from a 12-line project that glob-links the primary sources and overrides
`RoslynApiVersion`. Going lower still - 4.8 wants S.C.I 7.x - would mean
rebuilding that whole closure, `Opc.Ua.Types` included, and remains out
of scope. (`System.Collections.Immutable 7.0.0` also does not resolve in
this repository: `NU1603`, 8.0.0 substituted.)

**Nothing else in the repository exercises the down-level payload**, and
since every failure is a warning, a broken band would ship silently -
exactly as the 4.8 one did. So `validate-source-generator-packages.ps1`
now restores a real `Microsoft.Net.Compilers.Toolset 4.14.0`, runs its
`csc.dll` over the packed `roslyn4.14` payload, and asserts **both**
that none of `CS9057`/`CS8784`/`CS8032`/`CS8034` is emitted **and** that
`/reportanalyzer` lists the generator as having executed - silence alone
would also pass if the generator were never handed to the compiler. The
migration analyzer, whose payload comes from a hand-written nuspec
rather than `SourceGeneratorPack.targets`, gets the same check plus a
band/assembly manifest assertion.

To stop the trap recurring, `Microsoft.CodeAnalysis*`,
`System.Collections.Immutable` and `System.Reflection.Metadata` are
excluded from both the package payload and the in-repo analyzer closure,
and asserted against in the validation script. `roslyn.props` and
`docs/DeveloperGuide.md` document what adding a band requires.
## The shipped package set is unchanged

Evaluating `PackageId`/`IsPackable` across every project in
`preview-pack.slnx` gives the **same 47 package ids** on `master` and on
this branch, and a full Release build plus `dotnet pack --no-build`
produces exactly those 47. Only the owning project changed:

| Package | master | this PR |
| --- | --- | --- |
| `…Opc.Ua.SourceGeneration` | `Opc.Ua.SourceGeneration.csproj` |
`Opc.Ua.SourceGeneration.Pack.csproj` |
| `…Opc.Ua.SourceGeneration.Stack` |
`Opc.Ua.SourceGeneration.Stack.csproj` |
`Opc.Ua.SourceGeneration.Stack.Pack.csproj` |

`.azurepipelines/expected-packages.txt` pins the expected set and the
validation script fails when the packed output drifts.

## Two new package checks

**`build/<PackageId>.props` naming.** NuGet only auto-imports that exact
name, and the file declares every `CompilerVisibleProperty` /
`CompilerVisibleItemMetadata` the model generator relies on. It was
previously packed as
`build/OPCFoundation.Opc.Ua.SourceGeneration.props`, which does **not**
match the package id — so **no NuGet consumer ever imported it**, and
every `ModelSourceGenerator*` property and `AdditionalFiles` metadata
was silently invisible. In-repo projects were unaffected because they
import it by relative path.

**An end-to-end consumer that actually drives the generator.** The
pre-existing clean-consumer check only proved the analyzer *loads*.
`Test-SourceGeneratingConsumer` builds a standalone project that feeds a
real NodeSet2 in as an `AdditionalFile`, pins a
`ModelSourceGeneratorPrefix` that cannot be derived from the model, and
references the emitted types from hand-written code. It deliberately
does **not** import the props by path, so the package's auto-import is
what is under test. Both checks were verified to fail against a package
rebuilt with the misnamed props.

## CI: parallelism re-enabled

`buildandtest.yml` pinned three `dotnet build` invocations to
`/p:UseSharedCompilation=false` and `-maxcpucount:1`. `dotnet build`
defaults to all logical processors, so `-maxcpucount:1` actively
disabled parallelism. Both flags were introduced silently in unrelated
PRs (`f358078b1`, `208c601b0`) and suppress exactly the conditions the
SGF race needed — now structurally gone. Compilation is ~72 % of what
that workflow executes (233 of 323 job-minutes in run `29916768976`), so
the queue-independent `Build` step durations are the baseline:

| Measurement | Baseline |
| --- | --- |
| `Build` step, 26 `test-*` jobs | mean 5.53 min, sum 143.9 min |
| `build-linux-all-tfm` | 56.3 min |
| `build-windows-all-tfm` | 32.5 min |

`codeql-analysis.yml` keeps the flag — the CodeQL tracer requires it.

## Incidental fixes

- `Microsoft.CodeAnalysis.CSharp` declares an exact `[version]` range on
`Microsoft.CodeAnalysis.Common`, so with
`CentralPackageTransitivePinningEnabled` the central pins must move in
lockstep or restore fails with `NU1109`. They now derive from
`$(RoslynApiVersionVS2026)`.
- `MigrationGeneratorTests` created its generator driver without parse
options — silently fine under Roslyn 4.14, `Inconsistent language
versions` under 5.0.
- The migration analyzer props injected `<Analyzer>` items by hand from
`analyzers/dotnet/cs`; NuGet registers analyzers itself and selects the
matching folder, so the injection was obsolete and would now force the
wrong assembly to load.
- Exception diagnostics reported `ex.StackTrace` (null for an exception
that never left its throw site, and carrying neither type nor inner
chain) — now `ex.ToString()` at all three call sites.
- The pack guard tested the flattened assembly list, so it only fired
when *no* variant produced output; it now checks each declared variant
contributed assemblies.
- `GeneratePackageOnBuild` is disabled for the generators; packing
happens in CI or on an explicit `dotnet pack`.

## Note for reviewers

**Building this repository requires a Roslyn 5.x host** (the .NET 10 SDK
or Visual Studio 2026). That is already the documented prerequisite and
every CI leg installs `10.0.x` last, but it is now a hard requirement.

## Related Issues

_No tracking issue: this is a follow-up to the CS8784 clean-build
failure reported against #4041, which fixed the packaged delivery path
but left the in-repo project-reference path relying on the SGF
resolver._

- Follow-up to #4041

## Checklist

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

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

### Verification detail

| Check | Result |
| --- | --- |
| `dotnet build UA.slnx` (clean, and after merging master) | 0 errors;
18 warnings, all pre-existing `CA1873` in `Opc.Ua.Client` |
| `Opc.Ua.SourceGeneration.Tests` | 81/81 on net10.0 and net48 |
| `Opc.Ua.SourceGeneration.Stack.Tests` | 92/92 on net10.0, 89/89 on
net48 |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |
| `Opc.Ua.MigrationAnalyzer.Tests` | 121/121 on net10.0 and net48 |
| Package id set, master vs branch | identical, 47 packages |
| Release build + `dotnet pack --no-build` | 47 packages, matching the
manifest; both `roslyn4.14` and `roslyn5.0` present; no host-provided
assemblies shipped |
| `validate-source-generator-packages.ps1` | passes, including manifest,
props-naming, end-to-end generator consumer and down-level host checks |
| Down-level band under real csc 4.14.0 | all three analyzer packages
load **and run** |
| Negative tests | misnamed props → both checks fail as designed; hidden
variant output → pack guard fires; 5.0 build placed in `roslyn4.14` →
real `CS9057`; non-running generator name → execution assertion fires;
expecting a `roslyn4.8` band → manifest check fires |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
2026-07-31 15:46:17 +00:00
dependabot[bot] 7e69b4ced5 Bump actions/setup-node from 6 to 7 (#4101)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add cache-primary-key and cache-matched-key as outputs by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li>
<li>Migrate to ESM and upgrade dependencies by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Remove dummy NODE_AUTH_TOKEN export by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li>
<li>Only use <code>mirrorToken</code> in <code>getManifest</code> if
it's provided by <a
href="https://github.com/deiga"><code>@​deiga</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>Add documentation for publishing to npm with Trusted Publisher
(OIDC) by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li>docs: Update restore-only cache documentation by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li>
<li>docs: Update caching recommendations to mitigate cache poisoning
risks by <a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write
denied by <a
href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li>
<li><a href="https://github.com/deiga"><code>@​deiga</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li>
<li><a href="https://github.com/jasongin"><code>@​jasongin</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p>
<h2>v6.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update <code>@​actions/cache</code> to 5.1.0 and add security
overrides for undici and fast-xml-parser by <a
href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p>
<h2>v6.4.0</h2>
<h2>What's Changed</h2>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade <a
href="https://github.com/actions"><code>@​actions</code></a>
dependencies by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
<li>Update Node.js versions in versions.yml and bump package to v6.4.0
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p>
<h2>v6.3.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support parsing <code>devEngines</code> field by <a
href="https://github.com/susnux"><code>@​susnux</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-node/commit/820762786026740c76f36085b0efc47a31fe5020"><code>8207627</code></a>
Migrate to ESM and upgrade dependencies (<a
href="https://redirect.github.com/actions/setup-node/issues/1574">#1574</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/04be95cf3511ea51ebf9f224ddfb99cc7ab87cd4"><code>04be95c</code></a>
Add cache-primary-key and cache-matched-key as outputs (<a
href="https://redirect.github.com/actions/setup-node/issues/1577">#1577</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/7c2c68d20d402ed6a201ada70a81341941093140"><code>7c2c68d</code></a>
docs: Update caching recommendations to mitigate cache poisoning risks
(<a
href="https://redirect.github.com/actions/setup-node/issues/1567">#1567</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/6a61c0375d66246de94630495909f12cf8dac84d"><code>6a61c03</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/setup-node/issues/1569">#1569</a>
from jasongin/update-actions-cache-5.1.0</li>
<li><a
href="https://github.com/actions/setup-node/commit/30eb73b41ded577900c1ebf968ef95cdf8f7434f"><code>30eb73b</code></a>
Resolve high-severity audit issues</li>
<li><a
href="https://github.com/actions/setup-node/commit/4e1a87a501d0302f99e30e2748568adcb388d09f"><code>4e1a87a</code></a>
Update dist</li>
<li><a
href="https://github.com/actions/setup-node/commit/360237f0c01778d0c17291f75c56d6feae4f7574"><code>360237f</code></a>
Strict equality</li>
<li><a
href="https://github.com/actions/setup-node/commit/4f8aac5beb2f0854bc79651567a18c67eb0b9de3"><code>4f8aac5</code></a>
Bump <code>@​actions/cache</code> to 5.1.0, log cache write denied</li>
<li><a
href="https://github.com/actions/setup-node/commit/f4a67bbeca970f103397d3d2b9462cf787cd2980"><code>f4a67bb</code></a>
Only use <code>mirrorToken</code> in <code>getManifest</code> if it's
provided (<a
href="https://redirect.github.com/actions/setup-node/issues/1548">#1548</a>)</li>
<li><a
href="https://github.com/actions/setup-node/commit/0355742c943ddb13ca8a6b700f824231caa91e75"><code>0355742</code></a>
Remove dummy NODE_AUTH_TOKEN export (<a
href="https://redirect.github.com/actions/setup-node/issues/1558">#1558</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-node/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 14:26:45 +02:00
Copilot 16dd14f420 Improve MCP server coverage and agent usability (#4080)
## Summary

- Expand deterministic MCP coverage across host startup, sessions,
configuration, NodeSet export, PKI, packet diagnostics, and PubSub.
- Fix Streamable HTTP startup by supplying the OPC UA client application
identity.
- Add client-visible required-argument errors and normalize generated
tool schemas with explicit `required` arrays.
- Introduce bounded `core`, `services`, `administration`, `pubsub`, and
`diagnostics` tool profiles while retaining `full` as the current-major
default.
- Split configuration updates into focused transport, client, and
security tools while retaining `SetConfiguration` in the `full`
compatibility profile.
- Improve tool descriptions/examples and add pinned, deterministic
`mcpgrade` static/probe CI.

## Compatibility and behavior

- Existing stdio clients continue to receive the `full` catalog by
default.
- Streamable HTTP is exposed at `/mcp`; the previous root route is
intentionally removed.
- `--transport http` is preferred; `--transport sse` remains a
deprecated current-major alias.
- Diagnostics decode/replay/key tools remain behind the existing
explicit security gate.
- N002 is disabled only for documented OPC UA standard and
compatibility-sensitive name pairs.

## Validation

- Release analyzer build: 0 warnings and 0 errors.
- MCP tests: 276/276 passed with focused Coverlet collection.
- MCP coverage: 82.73% lines, 78.96% branches.
- MCP patch coverage: 94.79% lines (291/307 changed coverable lines).
- `core`: 15 tools, mcpgrade A/100, zero findings.
- `full`: 78 tools, mcpgrade A/100; only documented T001, T004, and S007
findings remain.
- Full stdio probe: zero C003/C004 findings.

Fixes #4079

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: bb615463-338e-4bbb-9779-5bf6780b8b47
2026-07-24 20:23:04 +02:00
Marc Schier ec02804157 Do not fail Docker builds on cache export errors (#4082)
## Summary
- keep successful Docker builds from failing when GitHub Actions cache
export is unavailable
- apply the non-fatal cache export setting to both Docker image
workflows

## Validation
- targeted rerun of the failed MCP server Docker job succeeded
- changed workflow YAML parsed successfully
2026-07-23 12:23:24 +02:00
Marc Schier 83f8db28a2 Bump setup-dotnet to v6 and fix UDP fake-time test hang (#4067)
# Description

Updates every GitHub Actions workflow from `actions/setup-dotnet@v5` to
`actions/setup-dotnet@v6` and fixes the net48 `UdpMessageRepeaterTests`
hang observed in Azure build 15773.

The hang dump showed NUnit blocked in `AsyncToSyncAdapter` waiting for
an unfinished async test after virtual time advanced before the repeater
registered its next delay timer. The tests now wait for each timer
registration before advancing `FakeTimeProvider`, apply the same
synchronization to cancellation coverage, and use real-time deadlines so
regressions fail instead of hanging the test host.

## Related Issues

- Replaces #4064
- Fixes the PubSub UDP net48 hang in [Azure build
15773](https://opcfoundation.visualstudio.com/865bd736-d0a6-4260-a336-d09d3eb39375/_build/results?buildId=15773)

## Testing

- Exact PubSub UDP net48 pipeline selection: 175 passed, 4 skipped.
- `UdpMessageRepeaterTests`: 30 consecutive net48 runs passed.
- `UdpMessageRepeaterTests`: 10 passed on net10.0.

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 13:28:18 +02:00
Marc Schier e73eb3a0e7 Fix source-generator package delivery and preview restore (Roslyn packaging) (#4041)
## Failure

The source-generator projects disabled normal build-output packing
without adding
their analyzer assemblies and private runtime closure back to the NuGet
payload.
Debug and Release packages also shared IDs, and both preview workflows
passed the
unsupported `--configuration` option to `dotnet restore`.

## Fix

- Package each generator and its private runtime closure under
`analyzers/dotnet/cs`, excluding Roslyn host assemblies and package
dependencies.
- Preserve Roslyn compile references for project-reference test
consumers.
- Give Debug generator packages `.Debug` IDs.
- Validate package contents and clean net10 consumers in both preview
pipelines.
- Remove invalid restore configuration arguments from both preview
workflows.

## Reference

- Roslyn analyzer packaging and MSBuild restore semantics; no normative
OPC UA change.
- Split from #4021.

## Tests

- `Opc.Ua.SourceGeneration.Tests`: 68 passed on net10.0 and 68 on net48.
- `Opc.Ua.SourceGeneration.Stack.Tests`: 90 passed on net10.0 and 87 on
net48.
- Generated `preview-pack.slnx` and restored it with
`--disable-parallel`.
- Packed both Release generators with `--no-build`; package-content
checks and two clean net10 consumer builds passed with zero
warnings/errors.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-22 12:40:27 +02:00
Marc Schier 8538f531e7 Update paths for the consolidated repository layout 2026-07-16 06:26:05 +02:00
Marc Schier 7d0e5870cb Migrate to source-generated logging across Stack, Libraries and Applications (#3908) (#4003)
# Description

Migrates the entire code base (Stack, Libraries and Applications) from
direct

`ILogger.LogInformation/LogError/LogWarning/LogDebug/LogTrace/LogCritical(...)`
calls to **source-generated `[LoggerMessage]` logging**, as tracked in
#3908.
Source-generated logging avoids boxing of value-type arguments, caches
the
message formatter, and emits an efficient `IsEnabled` check so a
disabled log
level costs nothing.

This is a behavior-preserving refactor: message text, levels and
structured
placeholders are unchanged; only the call shape changes.

## What changed

- **36 projects migrated.** Every static log call now goes through a
per-file
  `internal static partial class <Class>Log` holding `[LoggerMessage]`
  extension methods on `ILogger`, so call sites keep the natural
`logger.SomeEvent(...)` shape. Closely-related files that emit the same
messages share one log class (e.g. the encoders/decoders in
`Opc.Ua.Types`
  share `EncodingLog`) to avoid duplication and CS0121 ambiguity.
- **Per-assembly event-id classes.** Each project has one
  `internal static class <AssemblyToken>EventIds` (e.g. `CoreEventIds`,
  `ClientEventIds`, `PubSubEventIds`) with a reserved `public const int`
  offset block per log class. The assembly-token prefix avoids CS0436
  collisions across `InternalsVisibleTo` boundaries.
- **`CA1873` enabled solution-wide** in `.editorconfig` now that the
whole
code base (including test, sample and tooling projects) is migrated, so
any
regression back to a direct `ILogger.Log*` call with an expensive
argument is
flagged. Every occurrence has been guarded with an `IsEnabled` check (or
the
check merged into an existing condition where the log was the sole body
of an
`if`/`else`, preserving side effects); the only suppressions are two Moq
  `Verify` expression-tree sites, which are not executed log calls.
- **Dynamic log levels stay hand-written.** A handful of calls whose
level is
only known at runtime keep the structured `logger.Log(logLevel, ...)`
form
under an `if (logger.IsEnabled(logLevel))` guard — `[LoggerMessage]`
requires
  a compile-time level.
- **netstandard duplicate-generator fix.** Projects that also pull in an
R9
package (`Microsoft.Extensions.Http.Resilience`, `.Compliance`,
`.Telemetry`)
get the `Microsoft.Gen.Logging` generator in addition to the in-box one;
on
  `netstandard` both implement every partial method (CS0757). A single
`Directory.Build.targets` target removes the R9 analyzer on
`netstandard`
  only (no-op elsewhere and where it is not referenced).
- **Shared/linked source files** that are compiled into more than one
project
(e.g. sample files linked into test projects) use self-contained literal
event ids instead of a per-assembly class the consumer does not compile.
- **Docs.** The authoring recipe (event-id convention, log-class
convention,
  parameter rules, the `IsEnabled` guard, shared/linked files, and the
  netstandard generator interaction) lives in the new

[`Docs/DeveloperGuide.md`](Docs/DeveloperGuide.md#add-a-log-message-source-generated);
`Docs/Diagnostics.md` keeps the runtime/observability side and links to
it.
- **Redaction preserved.** Log calls that previously wrapped sensitive
values
with `Redact.Create(...)` keep passing the `RedactionWrapper<T>`
(instead of
an eager `.ToString()`), so format-time redaction still applies — this
also
  clears the CodeQL clear-text alerts introduced during the migration.

## Validation

- Full `UA.slnx` build passes with **0 errors / 0 warnings**; the
multi-target
  test projects force every referenced library to build on
  net472/net48/netstandard2.1/net8.0/net9.0/net10.0.
- No unguarded static `ILogger.Log*` calls remain anywhere (only the
guarded
dynamic-level calls); `CA1873` is clean across the full solution on
net10.0.

## Related Issues

- Fixes #3908

## Checklist

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

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

Completes the OPC UA Part 12 ServerConfiguration and transactional
PushManagement implementation, including opt-in distributed/HA
coordination, while retaining the original review-feedback fixes from
#3952.

## Transactional certificate and TrustList management

- Adds a Session-owned transaction coordinator shared by certificate
groups and TrustLists.
- Stages certificate and TrustList changes until `ApplyChanges`, with
ordered prepare/commit, rollback, intra-operation compensation,
cancellation, Session cleanup, and diagnostics.
- Implements `CreateSelfSignedCertificate`, `DeleteCertificate`,
`GetCertificates`, `CancelChanges`, `SupportsTransactions`, and
`TransactionDiagnostics`.
- Enforces method-specific SecureChannel requirements and standard Part
12 status codes.
- Persists regenerated signing-request keys and incorporates caller
nonce entropy where supported.
- Protects certificates currently presented by live endpoint security
policies.

## Distributed and high-availability PushManagement

- Adds opt-in `UseDistributedPushConfiguration` for redundant server
deployments.
- Uses the existing `ISharedKeyValueStore` CAS primitives for a
renewable cluster-wide transaction lease.
- Reuses the configured `ILeaderElection`; Kubernetes deployments use
the existing native Kubernetes Lease implementation, while
Raft/shared-store deployments use their existing providers.
- Persists pending regenerated keys in the shared store with
`IRecordProtector` authenticated encryption and atomic cross-replica
consumption.
- Fails closed when an external shared store lacks record protection.
- Keeps non-distributed server behavior unchanged.

## ApplyChanges effects

- Forces affected classic and Kestrel opc.tcp peer channels to
renegotiate after TrustList changes.
- Routes trust effects by listener trust-list scope.
- Revalidates X.509 user identities and closes invalid Sessions and
Subscriptions.
- Preserves response-before-channel-close sequencing and drains deferred
effects during shutdown.

## Diagnostics, alarms, and optional configuration

- Implements Part 12 TransactionDiagnostics DataValue status semantics
with client-safe error messages.
- Activates CertificateExpiration and TrustListOutOfDate alarms.
- Exposes identity properties and provider-backed optional
ServerConfiguration functionality.
- Adds secure, versioned configuration-file updates with rollback and
confirm/revert lifecycle.
- Aligns advertised and enforced MaxTrustListSize.

## Security and reliability feedback

- Clears certificate-store password buffers after use.
- Prevents internal exception details and paths from leaking through
TransactionDiagnostics.
- Hardens shared-fixture transaction cleanup.
- Preserves `IRecordProtector` compatibility through an additive
owned-buffer extension.

## Related Issues

- Follow-up to #3952

## Testing

- Full server and GDS suites pass on net10.0.
- Focused server and distributed PushManagement suites pass on net10.0
and net48.
- Distributed HA tests cover cross-replica contention, lease
expiry/renewal, leadership gating, protected pending-key transfer,
tamper rejection, and fail-closed configuration.
- Analyzer-enabled affected-project builds pass without new warnings or
errors.
- CTT/UACTT was not run because the licensed GUI tooling is unavailable
in this environment.

## Checklist

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 14:40:47 +02:00
Marc Schier c8bc1bbee6 Fix nightly Stress Tests (Chaos TCP): transparent reconnect + robust chaos assertions (#3986)
# Description

Fixes the nightly **Stress Tests / Chaos TCP** job (e.g. run
[29068846476](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/29068846476)),
which had failed on **every run since it was introduced** — the workflow
runs on a schedule and never gated PRs, so the failures went unnoticed.

## Root cause

The Chaos TCP tests point a `ConfiguredEndpoint` at a local
`TcpChaosProxy` and set `UpdateBeforeConnect = false` so the session
connects *through* the proxy. Two problems prevented this from working:

1. `ManagedSession.HandleConnectAsync` **ignored**
`ConfiguredEndpoint.UpdateBeforeConnect` and hard-coded a pre-connect
re-discovery for all non-OpenAPI endpoints. Re-discovery adopted the
server's advertised `EndpointUrl`, so the session connected **directly
to the server, bypassing the proxy** — no chaos ever reached the
connection. Two tests "passed" vacuously.
2. Once the proxy was genuinely engaged, the transparent-reconnect
feature had a gap: a request that hit the just-dropped channel (before
the manager detected the drop) failed with `BadConnectionClosed` instead
of transparently recovering.

## Changes

**Product**

- **`ManagedSession`** now honours
`ConfiguredEndpoint.UpdateBeforeConnect` (default `true`). Setting it
`false` opens the channel against exactly the supplied URL (proxy /
gateway / NAT / pinned endpoint) without re-discovering.
- **`ManagedTransportChannelLease.SendRequestAsync`** transparently
resends **idempotent** requests (`Read` / `Browse` /
`TranslateBrowsePaths` / `GetEndpoints` / `FindServers`) across a
channel-manager reconnect. On a transient transport-drop error it forces
a *coalesced* reconnect and resends once the shared channel recovers.
- A new **`ChannelEntry.ReconnectGeneration`** counter guards this so a
reconnect is only forced for a genuinely undetected drop (same
generation, still `Ready`), never for a stale in-flight failure that
arrives after recovery — which previously spawned spurious extra
reconnect cycles that tore down healthy channels.
- **Non-idempotent** requests (`Create*`, `Write`, `Call`, `Publish`, …)
surface the error so higher-level recovery (e.g. the subscription
engine's own re-create loop) stays in control; requests are never
double-applied.

**Tests / harness**

- **`TcpChaosProxy`** no longer rethrows the expected `OperationAborted`
raised by an in-flight upstream `ConnectAsync` during
`DropAllConnections` (it was faulting the awaited connection task and
failing the test).
- Chaos reconnect assertions (L3-A1/A2/A5) are made robust to the
inherent non-determinism of reconnect **counts** under concurrent chaos
(coalescing merges drops landing in one recovery window; a stale
keep-alive `Bad` can add one cycle). Survival is asserted via
`FailureRate`, `ReconnectFailed == 0`, and the coalescing fan-out
relationship rather than exact `ReconnectStarted/Completed ==
dropCount`.
- Subscription post-drop recovery window widened (`2s → 8s`) to allow
re-creating all sessions' subscriptions and monitored items on a loaded
CI agent.
- **New unit tests** in `ClientChannelManagerManagedTests` cover the new
product paths (fast-PR / codecov, since the nightly ChaosTCP job does
not run in the fast pipeline): idempotent transient-drop retry,
non-idempotent no-retry, non-transient no-retry, and
`ChannelEntry.ReconnectGeneration` increment.

## Validation (local, net10.0)

- **Chaos TCP**: 6/6 pass across many random seeds (incl. post-merge
re-runs of `338475986` and `1999999999`).
- `Opc.Ua.Client.Tests` ChannelManager/ManagedSession: **256/256**
(incl. the 4 new tests).
- `Opc.Ua.Sessions.Tests` ChannelManager/Reconnect: **139/139**.
- Core / Client / Stress build with **0 warnings**.

## Notes

- This branch has been merged up to date with `master` (redundancy PR
#3918).
- The two failing `Opc.Ua.PubSub.Tests` in CI —
`UdpLoopbackActionResponderAnswersRequesterAsync` and
`UdpLoopbackDiscoveryPublisherAnswersSubscriberRequests` — are
**unrelated** to this PR (UDP-loopback request/response tests in the
PubSub library, associated with #3918) and are timing/environment-flaky.
They are out of scope for this change.

## Related Issues

- Fixes the recurring nightly Chaos TCP failure (workflow:
`.github/workflows/stress-test.yml`).

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-07-12 10:20:02 +02:00
Alexander Köpke 26771349af Add Docker support for Pump Device Integration Server (#3987)
- Introduced a GitHub Actions workflow for building and pushing the
Docker image.
- Created a Dockerfile for multi-stage build of the
PumpDeviceIntegrationServer.
- Updated Program.cs to allow configurable host and port for the OPC UA
endpoint.
- Enhanced README with Docker usage instructions.
- Added fluent builder extensions for property access levels.
- Implemented unit tests for new fluent builder functionalities.

# Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

## Related Issues

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

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

- Fixes #github-issue-number, ...

## Checklist

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

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] 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: Marc Schier <marcschier@hotmail.com>
2026-07-11 21:25:45 +02:00
Marc Schier 7f409657ad OPC UA Part 4 6.6 Redundancy (server + client) with opt-in distributed high-availability (#3918)
## Summary

This branch implements **OPC 10000-4 §6.6 Redundancy** end to end —
server and client, **transparent and non-transparent**, across all
failover modes (`None`, `Cold`, `Warm`, `Hot`, `HotAndMirrored`,
`Transparent`) — and layers an **opt-in distributed high-availability
(HA)** capability on top so replicas can share address-space, session,
and subscription state and expose redundancy to clients through the
documented OPC UA mechanisms (for example, running a server replica set
across nodes in Kubernetes).

The single-instance, in-memory path stays **zero-overhead**: every
distributed feature is opt-in through dependency injection, with a
direct-construction fallback, and replaced APIs are kept and marked
`[Obsolete]`.

## New packages

| Package | Purpose |
| --- | --- |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy` | §6.6 redundancy
nodes plus opt-in distributed state: shared address space, secure
session sharing, subscription/continuation-point mirroring, and leader
election. |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.Crdt` |
Active/active, leaderless multi-writer replication via CRDTs (an
extension beyond §6.6). |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.K8s` | Kubernetes
integration: Lease leader election, EndpointSlice peer discovery, and
ServiceLevel-driven readiness. |

Also adds the worked samples `Applications/RedundantServer` and
`Applications/RedundantClient`, and the guides
`Docs/HighAvailability.md` and `Docs/HighAvailabilityKubernetes.md`.

## Server side (§6.6)

- `AddServerRedundancy(...)` populates the live
`Server.ServerRedundancy` nodes (`RedundancySupport` plus
`RedundantServerArray` / `ServerUriArray` / `CurrentServerId`) and
drives `Server.ServiceLevel` from an `IServiceLevelProvider` (leader
high, standby low). `Server.ServerRedundancy` is typed to the
NonTransparent or Transparent subtype according to the configured mode.
- `AddRequestServerStateChange(...)` implements the §6.6.5
manual-failover method, with `EstimatedReturnTime` and the Maintenance /
NoData `ServiceLevel` sub-ranges.
- Non-transparent peers resolve through `FindServers`
(`ConfiguredRedundantServerSetProvider`) and advertise the `NTRS`
discovery capability for GDS/NTRS registration.
- Deterministic, replica-stable `EventId` synchronization for
Transparent and HotAndMirrored sets (§6.6.2.2), excluding per-replica
fields so de-duplicating clients do not double-process events.

## Client side (§6.6)

- `DefaultServerRedundancyHandler` reads `Server.ServerRedundancy` and
`Server.ServiceLevel` and fails over to the highest-ServiceLevel peer.
It honors Maintenance (Part 4 Table 105 — disconnect to an available
peer), keeps Warm backups disabled until failover (Table 107), and
treats peers known only through `ServerUriArray` as failover candidates.
- `RedundantManagedClient` implements the Cold, Warm, Hot (a), Hot (b),
and HotAndMirrored client patterns: one active session, optional
lightweight ServiceLevel status-check sessions to backups, and — for Hot
(b) — merging of concurrent reporting streams with exact value-identity
de-duplication. HotAndMirrored failover re-activates the mirrored
session with the existing `AuthenticationToken` instead of recreating
it.
- Network redundancy: alternate communication paths via endpoint
selection.

## Distributed state (opt-in; an extension beyond §6.6)

- `ISharedKeyValueStore` (in-memory default) and `INodeStateStore`, with
an `AddressSpaceSynchronizer` that bridges a `CustomNodeManager2`'s
predefined nodes to the shared store, so node and reference
additions/removals and value changes propagate to other replicas.
`IDistributedValueCache` lets read/write callbacks cache the last value
with a freshness bound; monitored items use the normal read pipeline and
therefore participate only when the read path does.
- **Secure session sharing (active/passive fast reconnect), server and
client.** `DistributedSessionManager` (wired through a new additive
`ISessionManagerFactory` seam on `StandardServer`) mirrors **encrypted**
session state across replicas. On a failover reconnect the standby
restores the session and performs the **full `ActivateSession`
client-certificate signature validation** against a **single-use**
`serverNonce` consumed via compare-and-swap across the replica set,
enforcing the same SecurityPolicy and SecurityMode. The
`AuthenticationToken` is a **lookup key only**, never an authenticator:
entries are keyed by the SHA-256 digest of the token, and a
cross-replica restore emits a distinct audit event.
- Subscription, retransmission-queue, and continuation-point mirroring
for subscription transfer and failover.
- Leader election: a static single-leader provider and a shared-store
**lease via compare-and-swap**, supporting active/passive and
active/active (shared-read / master-write).
- Fail-closed security defaults: shared records are protected at rest
through `IRecordProtector`; secret-bearing mirrors require a registered
protector or an explicit opt-out.

## Active/active (CRDT)

`Opc.Ua.Server.Redundancy.Crdt` adds leaderless multi-writer
address-space replication with CRDTs and gossip
(`UseReplicatedAddressSpace`) plus CRDT-backed session metadata
(`UseReplicatedSessions`). Networked gossip is **fail-closed without
authenticated transport** (mutual TLS for TCP; an explicit opt-out for
isolated dev/test). CRDT state is eventually consistent, so exactly-once
decisions (such as the single-use nonce registry) stay on a strongly
consistent store. The package is available on all stack target
frameworks.

## Kubernetes

`Opc.Ua.Server.Redundancy.K8s` provides Kubernetes Lease leader
election, EndpointSlice-based peer discovery, and ServiceLevel-driven
readiness for running an OPC UA server replica set.
`Docs/HighAvailabilityKubernetes.md` covers StatefulSet/Deployment and
Service manifests, RBAC, probes, time synchronization, secrets,
gossip-port NetworkPolicy, and GDS/NTRS registration.

## Compatibility and validation

- **Zero-overhead** single-instance default; additive,
backward-compatible seams (for example `ISessionManagerFactory` and
`IServerStartupTask`); replaced APIs marked `[Obsolete]` for migration.
- Builds across the full stack TFM set (`net472`, `net48`,
`netstandard2.0`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`);
NativeAOT-compatible where the runtime supports it.
- Unit and integration tests for server, client, subscriptions, session
mirroring, CRDT, and Kubernetes; the conformance suite stays green.

See `Docs/HighAvailability.md` for the full OPC 10000-4 §6.6 mapping,
the `Add*` (standard nodes) versus `Use*` (extension) builder
convention, and the security/rotation guidance.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-11 04:47:25 +02:00
Marc Schier 8899af69b5 GH Actions pipelines update, nightly + mac gating, diagnostic dumps (#3881)
# Description

CI-only PR (no shipped-code change). Modernises the AzDO + GitHub
Actions pipelines, adds a preview-package workflow that publishes to
GitHub Packages, adds a nightly full-scope run, tightens macOS gating,
and improves testhost diagnostics.

## Summary

### Azure Pipelines

1. **`managedPool` parameter** — Optional routing of Windows/Linux jobs
to the `netstandard` Managed DevOps Pool (`ImageOverride` demands).
Default `false`; operators can flip per queue.
2. **Per-TFM build matrix** — `sln.yml` fans out into one job per (TFM ×
configuration) for the full-scope build so each matrix leaf fits on
small-disk pool agents.
3. **Cancel-aware `condition:` forwarding in `test.yml`** — parameters
wrapped in `and(succeeded(), <forwarded>)` so user-cancel propagates to
jobs.
4. **Decoupled test / result-publish steps** — `DotNetCoreCLI@2 command:
test` runs with `publishTestResults: false` + explicit `--logger trx`; a
separate `PublishTestResults@2` runs `succeededOrFailed() +
continueOnError: true`. Prevents transient DNS blips against
`opcfoundation.visualstudio.com` from flipping green runs to failed.
5. **Blame + dump collection** — `dotnet test` uses
`--blame-hang-timeout 10m --blame-hang-dump-type mini` so silent hangs
become named test failures with a mini-dump attachment. Fuzz-artifact
download/extract/upload steps gated to `Fuzz.Tests` jobs only.
6. **Diagnostic dumps published as build artifacts** — new
`Stage/Publish diagnostic dumps` steps collect `*.dmp`, `*.mdmp`,
`*.dump`, Linux `core`/`core.*` from `$(Agent.TempDirectory)` on every
finished job (`succeededOrFailed()`), keyed by `System.JobId` so
parallel matrix entries never collide. Root-caused from a run where the
previous `failed()` gate silently skipped because the test task itself
was reported succeeded despite testhost issues.
7. **Weekly full-scope run** — new `schedules:` block firing Sundays
02:00 UTC (`always: true`, master). All stages already gated on
`ScheduledBuild` (`Debug`, `.NET 9/8/472/netstandard`, `Coverage`,
`AOT`, macOS matrix) participate automatically.
8. **macOS gating** — mac net10 job added back to `testfastpr` gated on
`ne(ScheduledBuild, 'False')` so mac runs nightly/manual only (not on
PR/master push). Stripped 4 stale `IsDraft` per-job conditions in
already-`ScheduledBuild`-gated stages (`testdebug`, `testnet90`,
`testnet80`, `testnetstandard21`).
9. **Preview solution generation** — deleted the hand-maintained `UA
Core Library.slnx` (and dead `MonoReferenceServer.csproj`); added
`.azurepipelines/generate-slnx.ps1` that derives `preview-pack.slnx`
from `UA.slnx` by stripping `/Tests/`, `/Fuzzing/`, `/Solution Items/`.
`preview.yml` uses the generated solution.

### GitHub Actions

10. **New `.github/workflows/preview-publish.yml`** — mirrors
`.azurepipelines/preview.yml` end-to-end (versioning via existing
`set-version.ps1`, strong-name + Authenticode + nupkg signing, modern +
legacy nuspec packaging) and publishes to **GitHub Packages**
(`https://nuget.pkg.github.com/OPCFoundation/index.json`) with
`--skip-duplicate`. Triggers: `workflow_dispatch`, push to `master`
(Preview version), push to `v*` tags (Public version). Every signing
step gracefully skips when the corresponding secret is missing
(fork-friendly). Requires `permissions: packages: write`.
11. **Reshuffled `buildandtest.yml` matrix** — GH Actions covers Linux +
macOS (net10 only); Windows is fully covered by AzDO. AoT job expanded
to `ubuntu-latest` (`linux-x64`), `macos-13` (`osx-x64`), `macos-latest`
(`osx-arm64`) with a single RID-aware run step. Added `schedule` (Sun
02:00 UTC) + `workflow_dispatch(include_macos)` triggers. The `discover`
job computes `test_os` + `aot_matrix` dynamically based on event type:
macOS is **off** by default on `pull_request` and **on** by default for
push/schedule/workflow_dispatch (respecting the input).

### Tests

12. **`ReferenceNodeManager.DoSimulation`** — added `catch
(ObjectDisposedException) when (m_simulationTimer is null)` to swallow
the expected teardown race between the simulation timer callback and
`Dispose`.
13. **`AlarmsAndConditions*` test fixture** — added
`DefaultEventWaitTimeout = TimeSpan.FromSeconds(30)` and migrated 22
call sites off the previous 5 s poll; `AcknowledgeBranchAsync` +
`ConfirmBranchAsync` marked `[Retry(2)]` to absorb a rare macOS
`BadSecureChannelClosed` race.

## Related issues

Follow-up to #3872.
2026-07-08 08:31:43 +02:00
Marc Schier f9a8343dea Full 1.05.06 compliant OPC UA Part 14 (PubSub) (#3892)
# Full OPC UA Part 14 (PubSub) — v1.05.06

This PR replaces the legacy PubSub stack with a modern, **OPC UA Part 14
v1.05.06**-compliant reimplementation built on current .NET: fully
`async`, dependency-injection-first with a fluent builder
(`services.AddOpcUa().AddPubSub(...)`), NativeAOT/trim-safe, and
designed for high-throughput, highly-available, distributed deployments.
The libraries — `Opc.Ua.PubSub`, `Opc.Ua.PubSub.Udp`,
`Opc.Ua.PubSub.Mqtt`, `Opc.Ua.PubSub.Server`, and
`Opc.Ua.PubSub.Adapter` — keep source compatibility with 1.5.378 where
practical (obsolete shims) and ship a complete migration path. See the
**[PubSub
guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md)**
and the **[2.0.x migration
guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/migrate/2.0.x/pubsub.md)**.

**Encodings & transports.** Both wire encodings are implemented in full
— **UADP** (key/delta/event/keepalive, RawData, chunked,
signed+encrypted) and **JSON** (verbose/compact, single-message,
metadata; migrated from `Newtonsoft.Json` to `System.Text.Json`) —
across **UDP** (uni/multi/broadcast + discovery), **MQTT** (3.1.1/5.0,
retained metadata, TLS), and a from-scratch **DTLS 1.3** secured
datagram transport (`opc.dtls://`) implemented on the .NET BCL crypto
primitives (ECDHE, HelloRetryRequest cookies, certificate auth, AEAD
record protection, HKDF key schedule), wired through
`AddUdpTransport().WithDtls(...)`. Details in
[Transports](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#transports)
and
[Encodings](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#encodings).
A raw **Ethernet (Layer 2)** transport (`opc.eth://`, EtherType
`0xB62C`, optional 802.1Q VLAN) carries UADP NetworkMessages directly in
Ethernet II frames with no IP/UDP, via `AddEthTransport()`
(`.WithPcap()` for cross-platform capture). See [Ethernet
transport](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#ethernet--uadp-opceth).

**Runtime schema generation.** A new schema subsystem
(`Opc.Ua.Core.Schema`, `Opc.Ua.PubSub.Schema`) generates **XSD**, **OPC
Binary (BSD)**, and **JSON Schema** for OPC UA data types at runtime —
including PubSub NetworkMessage/DataSet message schemas — driven from
the type system and source-generated metadata (no reflection;
NativeAOT/trim-safe). It is consumable from the client, server, and
PubSub stacks. See the **[Schema generation
guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/SchemaGeneration.md)**.

**Security, fail-closed.** Message-level security is wired end-to-end
through `IPubSubSecurityWrapperResolver`: publishers are fail-closed (a
secured group with no key source throws rather than emitting plaintext)
and subscribers enforce per-reader inbound security. It includes AES-CTR
(128/256) policies with HMAC-SHA-256, a deterministic 96-bit nonce with
a messages-per-key cap, a monotonic sliding replay window, and a
Security Key Service (SKS) pull client + in-memory server with
SecurityGroups and key rotation. Two independent security assessments
(IEC 62443-4-2 × OPC UA Part 2/4/14, plus a Microsoft SDL/SFI delta
pass) drove remediation of all actionable findings — including orphaned
message security, chunked-frame security bypass, unauthenticated Action
invocation, pre-auth reassembly DoS, and DTLS hardening. See
[Security](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#security)
and
[SKS](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#security-key-service-sks).

**Server-side & high availability.** The runtime can optionally surface
through a standard in-process OPC UA address space
(`Opc.Ua.PubSub.Server`) with configuration methods, per-component
diagnostics, the full PubSubState model, KeepAlive and
MessageReceiveTimeout. Pluggable configuration/runtime-state stores, an
id allocator, and HA runtime-state providers with failover support
running publishers/subscribers in a distributed system. See [Server-side
address
space](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#server-side-address-space)
and [High
availability](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#high-availability-state-providers).

**External-server adapter (bridge) with hot-reload.** A new
`Opc.Ua.PubSub.Adapter` binds PubSub publishers, subscribers, and Action
responders to an **external** OPC UA server over `ManagedSession` —
cyclic Read or subscription-backed sources, browse-path node mapping,
and metadata that resolves/retries and re-emits on model-change events.
Adapter and PubSub configuration **hot-reload incrementally** (only
changed writers/readers/responders are rewired; unchanged sessions are
reused), driven by `IPubSubConfigurationStore.Changed` and
`IOptionsMonitor`, with a pluggable change-feed source (e.g. etcd). See
[Binding PubSub to an external
server](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters).

**Samples, packaging & quality.** One combined sample,
`ConsoleReferencePubSubClient`, demonstrates publisher, subscriber, and
external-server modes on `Host.CreateApplicationBuilder` with full
fluent/DI wiring; all libraries publish **NativeAOT with zero
IL2026/IL3050 warnings** ([Native
AOT](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/PubSub.md#native-aot),
[DI](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/DependencyInjection.md)).
The change ships comprehensive NUnit/integration suites and a libFuzzer
harness, maintains ≥80% coverage on the new libraries, and is documented
end-to-end ([What's new in
2.0](https://github.com/OPCFoundation/UA-.NETStandard/blob/part14pubsub/Docs/WhatsNewIn2.0.md)).
2026-07-01 06:49:55 +02:00
Marc Schier bab073952c Adding HTTPS/WSS transports + Web API binding (Part 6 §G.3 OpenAPI Mapping), shared Kestrel host, Kestrel-TCP (#3880)
# HTTPS / WSS transports + OPC UA Web API binding (Part 6 §G.3) + shared
Kestrel host

Combined follow-up that completes the HTTPS / WSS / Web API transport
story started in #3876 and #3877. Three coordinated bodies of work land
together: a **shared Kestrel host** with full WSS reverse-connect and an
opt-in Kestrel-hosted `opc.tcp://` path (no separate NuGet), the **OPC
UA Web API binding** (Part 6 §G.3 "OpenAPI Mapping") with a
spec-compliant ASP.NET Core Minimal-API server and symmetric C# client,
and a targeted **edge-security hardening** pass across every new HTTPS /
WSS / JSON / Web API surface. UASC crypto / signing / sequence / nonce
logic is unchanged.

## Shared Kestrel host, WSS reverse-connect, Kestrel-TCP

Multiple `HttpsTransportListener` instances now share one `IHost`, so
`opc.https://` and `opc.wss://` can be colocated on a single port. WSS
gains full reverse-connect on **both** server and client sides via
`HttpsTransportListener.CreateReverseConnection` /
`TransferListenerChannelAsync`, plumbed through the same
`TcpServerChannel.BeginReverseConnect(IUaSCByteTransport)` path as TCP.
An opt-in `KestrelTcpTransportListener` (`AddKestrelOpcTcpTransport()`)
hosts `opc.tcp://` on Kestrel via `ConnectionHandler` so consumers can
run every OPC UA transport under one host; it ships **inside**
`OPCFoundation.NetStandard.Opc.Ua.Bindings.Https` gated `#if
NET8_0_OR_GREATER` with no new dependencies, while the raw-socket
`TcpTransportListener` stays in `Opc.Ua.Core` for trimmed/AOT
deployments. The key technical fix was overriding `StartReceiveLoop` in
`TcpReverseConnectChannel` to do a one-shot `ReverseHello` read —
`WebSocket.ReceiveAsync(CancellationToken)` aborts the socket on
cancellation, which previously broke the WSS handoff. See
[Docs/Transports.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Transports.md)
and
[Docs/ReverseConnect.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/ReverseConnect.md).

## OPC UA Web API binding (Part 6 §G.3)

All **28 services** from `opc.ua.openapi.allservices.json` are mapped at
the spec routes (`/findservers`, `/getendpoints`, `/createsession`,
`/read`, `/write`, `/browse`, `/call`, `/publish`, …) on ASP.NET Core
Minimal-API endpoints. Both `application/json; encoding=compact`
(mandatory per Part 6 §5.4.9) and `encoding=verbose` are supported via
media-type negotiation. The implementation is **NativeAOT-compatible** —
each route binds to a static `RequestDelegate` thunk that calls a
generic instantiation of `WebApiEndpointDispatcher.HandleAsync<TRequest,
TResponse>` with zero MVC reflection and no
`[UnconditionalSuppressMessage]`
(`<IsAotCompatible>true</IsAotCompatible>` is clean of `IL2xxx` /
`IL3xxx` warnings on net10). NodeManagement (§5.8) and Query (§5.10) are
deliberately omitted per the spec doc. New profile URIs
`Profiles.HttpsOpenApiTransport` (profile/2338) and
`Profiles.WssOpenApiTransport` (profile/2339) are emitted as discovery
twins on every SM=None HTTPS / WSS endpoint. Naming follows the [OPC
Foundation
UA-WebApi-StarterKit](https://github.com/OPCFoundation/UA-WebApi-StarterKit).
Full developer guide in
[Docs/WebApi.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/WebApi.md).

## Authentication, WSS sub-protocol, symmetric client

Four pluggable auth modes wire through
`OpcUaWebApiAuthenticationBuilderExtensions`: **Anonymous**, **Bearer
(OIDC/JWT)** with a built-in `JwtClaimSessionlessIdentityProvider` that
projects `sub` / `scope` / `roles` onto `IUserIdentity`, **Basic**, and
**Mutual TLS**. A policy scheme (`WebApiAuthSchemes.Default`) becomes
the default authenticate/challenge/forbid scheme so
`UseAuthentication()` populates `HttpContext.User` even with multiple
handlers registered; every route carries `RequireAuthorization()` except
`/findservers` and `/getendpoints` which keep `AllowAnonymous` per the
spec. The **WSS `opcua+openapi` sub-protocol** (profile/2339) is
accepted in two forms — bare and `opcua+openapi+<accesstoken>` (bearer
rides in the sub-protocol name because browser WebSocket APIs forbid
custom headers, per Part 6 §7.5.2) — and the token is validated against
the registered JwtBearer scheme **before** the upgrade completes;
cleartext `ws://` bearer upgrades are rejected on both server and
client. A symmetric **C# client** (`Libraries/Opc.Ua.Client/WebApi`)
provides `IWebApiClient` / `WebApiTransportChannel` /
`WebApiWssTransportChannelFactory` so `Session`, `ManagedSession`, and
the V2 `SubscriptionManager` all dispatch unchanged; fluent shortcuts
`ManagedSessionBuilder.UseWebApiEndpoint(url, encoding)` and
`UseWssOpenApiEndpoint(url, encoding)` are wired in.

## Edge security hardening

A targeted audit against OPC UA Part 2/4/6 + Microsoft SDL drove
**fail-closed** defenses across every new surface. TLS server-cert
validation in both `WebApiTransportChannel` and
`WebApiWssTransportChannel` delegates to the OPC UA
`CertificateValidator` (with the default .NET TLS chain check as
fall-back), mirroring `HttpsTransportChannel`. WSS bearer tokens are
validated before upgrade; empty-password identity-token fallbacks were
removed from `DefaultSessionlessIdentityProvider` and
`JwtClaimSessionlessIdentityProvider`, with the upstream principal now
flowing via the new `SecureChannelContext.UpstreamIdentity`.
`WebApiBodyCodec.DecodeBodyAsync` enforces `MaxMessageSize` during the
read (BadRequestTooLarge before allocation), and both server and client
WSS receive loops reject zero-byte continuation frames. On the
base-branch side, `JsonRequestMapper.ReadAllBoundedAsync` bounds
JSON/binary reads, HTTPS / WSS handlers fail-closed on missing SM=None
JSON endpoints (BadSecurityPolicyRejected, restrict-to-discovery), and
`WebSocketByteTransport` carries the same zero-progress guard for
`opcua+uacp`.

## Testing, docs, scope

Approximately **300 new passing net10 tests**: 107 in
`Opc.Ua.Core.Tests` (codec, media type, route table, profile constants),
153 in `Opc.Ua.Bindings.WebApi.Tests` (endpoint dispatcher, auth
handlers, TLS regression, mTLS source-pin, body-size, zero-progress,
identity plumbing, bearer validation), 29 reference-server integration
tests in `Opc.Ua.Sessions.Tests` (HTTPS WebApi, WSS OpenAPI, WSS
reverse-connect, Kestrel-TCP, shared host), and 7 `Opc.Ua.Aot.Tests`
round-trips on the natively-compiled binary. Full multi-TFM `dotnet
build UA.slnx` is clean. Docs updated:
[Transports](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Transports.md),
[WebApi](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/WebApi.md),
[ReverseConnect](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/ReverseConnect.md),
[Profiles](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/Profiles.md),
[MigrationGuide](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/MigrationGuide.md),
[NativeAoT](https://github.com/OPCFoundation/UA-.NETStandard/blob/httpwssbindingfollowups/Docs/NativeAoT.md).
Out of scope (tracked as follow-ups): own-listener hosting mode for Web
API, source generation of extended complex types not exercised by the 28
services, load-testing harness, `MatchEndpoints` honouring user
`TransportProfileUri` on `UpdateFromServer`, rate limiting on the
long-poll `/publish` path, and a richer first-class `UpstreamPrincipal`
model.

## Related issues

- Closes the follow-up scope of #3876 (WSS transport)
- Closes the follow-up scope of #3877 (HTTPS-JSON transport)
- Supersedes #3882 (folded in via fast-forward push of `openapi` →
`httpwssbindingfollowups`)
2026-06-29 17:29:27 +02:00
Marc Schier 208c601b09 Fix weekly all-TFM UA.slnx build (net8/net9 + netstandard) and add Windows/Linux all-TFM CI build jobs (#3913)
The OPC UA stack builds for seven target frameworks (net472, net48,
netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0), but our
regular pull-request builds only compile two of them (net10.0 and
net48). The complete per-framework build runs only in a weekly scheduled
Azure job, so when a change breaks one of the other frameworks nobody
notices for days. That is exactly what happened in build 14931, which
was red on net8.0, net9.0, netstandard2.0 and netstandard2.1.

This PR fixes all of those broken frameworks. The fixes are small and
framework-specific: a net10.0-only test project reference is made
conditional so net8.0/net9.0 can build, a set of "legacy-stripped"
project shells that failed to compile as empty netstandard assemblies
now build as net10.0, and a few polyfill and NuGet-package collisions in
the netstandard2.1 build are resolved by building its test/app projects
as net8.0 and pinning the matching MQTTnet version. After these changes,
`dotnet build UA.slnx` succeeds cleanly for every framework.

To stop this from regressing, the PR adds two CI jobs that run on every
pull request, in parallel with the existing unit tests: one on Windows
that builds the .NET Framework targets (net472, net48, netstandard2.0)
and one on Linux that builds the modern .NET and netstandard2.1 targets
(net8.0, net9.0, net10.0, netstandard2.1). Together they cover all seven
frameworks and fail the PR if any single one won't build. The workflow's
trigger list was also widened so these jobs run when only build or
configuration files change (for example `*.props` or `*.targets`), not
just C# source. This PR supersedes #3912 and folds in its fixes.
2026-06-25 14:47:57 +02:00
Marc Schier 44355a4c38 Fix client user-cert selection: curve-aware match + client-enabled-policy filter (#3891)
# Description

X509ClientIdentityProvider previously accepted any `UserTokenPolicy`
whose `TokenType` was `Certificate`, then narrowed by
`HasSameEncryptionAlgorithm` which only checked `IsEccPolicy` parity. An
ECC user cert could therefore satisfy a policy of any ECC curve (e.g. a
NistP256 cert silently authenticating against a NistP384 policy), and
policies the client had disabled in
`SecurityConfiguration.SupportedSecurityPolicies` were silently used as
long as the cert "looked compatible".

This PR makes user-cert selection curve-aware and filters offered
policies against the client's enabled set, with rich diagnostics on
failure.

## What changed

**API (v2.0-only, not yet shipped — no `[Obsolete]` shim):**

- `IClientIdentityProvider.CanSatisfy(...)` → `CanSatisfyAsync(...)`
returning a new `CanSatisfyResult` (bool + rejection reason).
Sync-over-async would have been required for the X.509 provider to load
the cert during selection — repo rules forbid this, so the contract is
async end-to-end.
- `IdentitySelectionContext` gained `EnabledSecurityPolicyUris`
(init-only). The 3-arg ctor and the 2-arg `AcquireIdentityAsync`
extension default this to `Array.Empty<string>()` (no filter) so
existing callers keep their pre-PR behaviour. The 4-arg ctor is the
explicit opt-in.
- `SelectUserTokenPolicy` → `SelectUserTokenPolicyAsync`.

**Curve-aware X.509 selection (`X509ClientIdentityProvider`):**

- Loads the user cert once via
`ICertificateProvider.GetPrivateKeyCertificateAsync` (cached lock-free
via `Interlocked.CompareExchange`; load failures are NOT cached so
transient errors / cert provisioning / rotation are retried).
- Maps the public key to `CertificateKeyAlgorithm` (RSA / NistP256 /
NistP384 / BrainpoolP256r1 / BrainpoolP384r1) by inspecting
`Certificate.GetKeyAlgorithm()` and `PublicKey.EncodedParameters`.
- Accepts a policy only when
`SecurityPolicies.GetInfo(uri).CertificateKeyAlgorithm` matches the
cert. Empty `SecurityPolicyUri` inherits the channel policy.
`SecurityPolicies.None` imposes no constraint. Unknown /
platform-unsupported policy URIs are rejected with
`UnknownOrUnsupportedSecurityPolicy`.

**Client-enabled-policy filter (`SelectUserTokenPolicyAsync`):**

- Before delegating to `CanSatisfyAsync`, drop any offered
`UserTokenPolicy` whose non-empty `SecurityPolicyUri` is not in
`EnabledSecurityPolicyUris` (`NotEnabledByClient`). Applies to RSA /
UserName / IssuedToken too, so deprecated policies (e.g.
`Basic128Rsa15`) cannot be picked accidentally.

**Aggregated diagnostics:**

- When no policy is selectable, the `BadIdentityTokenRejected` exception
lists every offered policy (PolicyId / TokenType / SecurityPolicyUri /
IssuedTokenType) with its rejection reason — `NotEnabledByClient`,
`TokenTypeNotSupported`, `IssuedTokenProfileNotSupported`,
`CertificateAlgorithmMismatch` (with cert and policy algorithm names),
`CertificateLoadFailed` (with cert subject/thumbprint),
`UnknownOrUnsupportedSecurityPolicy`, `ProviderRejected` — plus the
client's enabled-policy set. `CompositeClientIdentityProvider`
aggregates child reasons.

**Plumbing:**

- `Session.cs` passes
`Configuration.SecurityConfiguration.SupportedSecurityPolicies` into the
context (falling back to `[channel]` only when the configuration is
unset).
- `AnonymousIdentityProvider`, `UserNamePasswordIdentityProvider`,
`IssuedTokenIdentityProvider`, `CompositeClientIdentityProvider` updated
to the async API; trivial providers wrap the existing logic in
`ValueTask.FromResult` plus a reason string when rejecting.

## Tests

- 4 existing test files updated for the async API.
- New `X509ClientIdentityProviderSelectionTests.cs` (7 tests):
  - RSA cert prefers RSA policy over offered ECC policy.
  - NistP256 cert prefers `ECC_nistP256` policy over `ECC_nistP384`.
- NistP384 cert with only `ECC_nistP256` offered →
`BadIdentityTokenRejected` with `CertificateAlgorithmMismatch` (mentions
both algorithms).
- Disabled policy in offered set → `NotEnabledByClient` in diagnostic
message.
- With multiple offered policies, the enabled filter prefers the enabled
one.
- Cert load failure → `CertificateLoadFailed` with cert subject in the
diagnostic.
  - Unsupported token type → `TokenTypeNotSupported` in diagnostic.

`dotnet test` against `net10.0` for the Identity + ClientBuilder
categories: 43/43 pass. `Opc.Ua.Core`, `Opc.Ua.Client`, `Opc.Ua.Server`,
`Applications/ConsoleReferenceClient` all build clean.

## Related Issues

_No tracking issue; this addresses a class of OPC UA client misbehaviour
(curve confusion / silent acceptance of disabled policies) discovered
during user-cert authentication review._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-18 16:37:39 +02:00
Marc Schier 1c312e5559 Fix #3546: re-enable FuzzCrashAssets and harden BinaryDecoder (#3884)
# Description

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

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

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

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

## Regression coverage

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

## Verification

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

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

## Related Issues

- Fixes #3546

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran the affected tests locally (`Opc.Ua.Encoders.Fuzz.Tests` on
net10.0 + net48, targeted `Opc.Ua.Types.Tests` subset on net10.0); full
UA.slnx still pending CI.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-15 15:55:04 +02:00
Marc Schier f358078b1c [Client] Central channel manager: ref-counted shared channels, coalesced reconnect, IRetryBudget (#3288) (#3852)
# Description

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

## What lands

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

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

### Three-state lifecycle gate

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

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

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

### ManagedSession integration

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

### Discovery / Registration / GDS

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

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

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

### DI

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

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

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

### Tests

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

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

### Docs

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

## Carry-forward (separate PRs)

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

## Related Issues

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

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
*(Channel-manager-specific tests + ManagedSession/client tests verified
across all 6 TFMs; full UA.slnx run is pending CI.)*
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-13 16:34:48 +02:00
Marc Schier 7728b65e8d CI: Fix pipeline cannot be cancelled regression and flaky tests (#3872) 2026-06-13 08:33:29 +02:00
Marc Schier 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 a81d981755 dotnet format sweep (whitespace, IDE, RCS) + code-style enforcer agent fix (#3865)
# Description

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

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

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

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

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

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

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

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

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

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

## Related Issues

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

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: agent <agent@local>
2026-06-10 08:35:44 +02:00
Marc Schier 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 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 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 eb40a59566 More format and diagnostic fixes (#3780)
Drive the solution to zero warnings / zero errors with analyzers on,
promote the consistently-fixed rules to error, and lock in coverage
on the only line of new logic.
2026-05-26 06:06:09 +02:00
Marc Schier 14e995c1d0 Restructure test projects for faster CI (#3767)
Rebalance the heavy test projects of the OPC UA .NET Standard stack so CI test execution can run cleanly in parallel.

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

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

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

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

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-24 06:23:16 +02:00
Marc Schier 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 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 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
Marc Schier 94653f1aee Add a MCP server that enables LLM/Copilot to be a OPC UA client (#3701)
* Opc ua client Mcp Server
* fix for #3699
2026-04-24 19:03:45 +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
Marc Schier c71f2e7733 Fix source generator generated structures with optional fields (#3674)
Split validator load/validate; split DesignFileCollection Targets/Dependencies

ModelDesignValidator.LoadNodes now skips ValidateDictionary when a model is loaded purely as a dependency. The flag is plumbed through LoadModelDesign and LoadDesignFile; the dep loop in ValidateModel passes false, the primary target passes true. This lets callers supply unrelated or reverse dependencies (deps that import the target) without triggering premature validation failures.

DesignFileCollection exposes Targets + Dependencies; generators and tests updated. MergeNamespace preserves FilePath across PublicationDate-based replacement so later loads can still find the source file. Tester simplified to treat every other xml/csv pair as a dependency of each target.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 22:36:53 +02:00
Marc Schier 6c8c1a6d29 Enable dispose analyzers CA2000 and CA2213 and fix all issues (#3670) 2026-04-17 13:39:34 +02:00
Marc Schier fd41681f4b Add support to publish OPC UA applications as Native AOT (#3633)
* make most ptojects aot compatible
* Remove newtonsoft
* Migrate all projects from Mono.Options to System.CommandLine 2.0.5
* Fix array clone
* Replace serializers for storage using new attribute driven  type geberator
* Add new AOT integration tests covering all functional areas
* Documentation updates
2026-04-13 20:52:29 +02:00
dependabot[bot] 189a6c7c83 Bump codecov/codecov-action from 5 to 6 (#3645)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:13:37 +02:00
romanett 66f44396ff [Server] Improve Request Queue, fix Event handling, add Tests (#3611)
- implement RequestQueue using Channels
- add event load test
- add event integration test verifying event from server object is received
- fix Client Reconnect Test
- fix DiagnosticsNodeManager not creating RootNotifier for Server object
- add Benchmarks for MonitoredItem.QueueEvent
- add serveral UnitTest for DiagnosticsNodeManager
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-03-20 08:09:51 +01:00