Skip to content

[WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on - #4128

Merged
marcschier merged 357 commits into
masterfrom
marcschier/wot-01-core-types
Aug 5, 2026
Merged

[WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on#4128
marcschier merged 357 commits into
masterfrom
marcschier/wot-01-core-types

Conversation

@marcschier

@marcschier marcschier commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds OPC UA WoT Connectivity 1.1 to the stack, together with the Types, source generation, Server and xRegistry work it is built on. This branch is the merge point for the fourteen-PR stack that replaced integration PR #4093; every constituent PR was reviewed and approved separately and is listed below.

414 files changed, +108,936 / -3,199. Roughly 46k added lines of product code, 57k of tests, plus samples, tools and documentation.

Types

Source generation

Server

  • Add live NodeManager shadow and immediate reload. A reload materializes a new generation beside the active one and switches atomically; the superseded generation is retired gracefully so existing MonitoredItems keep being served until they drain, or immediately when the caller asks for it. Includes the request admission and drain machinery that lets an orderly shutdown wait for admitted requests instead of tearing down underneath them ([Server] Add live NodeManager shadow and immediate reload #4147).
  • Make subscription transfer between sessions transactional, so a failed transfer leaves neither session holding a partially moved subscription ([Server] Make subscription transfer between sessions transactional #4150).

xRegistry

WoT Connectivity

New projects

src/Opc.Ua.WotCon.Bindings, src/Opc.Ua.WotCon.Bindings.Mqtt, tests/Opc.Ua.WotCon.Bindings.Tests, tests/Opc.Ua.WotCon.Samples.Tests, and the samples/WotCon AggregationServer, AggregationClient and FlatTagServer samples.

Constituent pull requests

Merged into this branch top-down, each independently approved:

PR Title
#4128 [Types] Complete ExtensionObject decoding and add an atomic file replace capability
#4131 [Types] Add lossless conversion between WoT documents and NodeSet2
#4132 [SourceGeneration] Improve NodeSet to ModelDesign conversion and generated node state
#4134 [SourceGeneration] Generate OPC UA models from WoT Thing Description files
#4147 [Server] Add live NodeManager shadow and immediate reload
#4150 [Server] Make subscription transfer between sessions transactional
#4130 [XRegistry] Allow clients to use explicit registry roots
#4139 [WotCon] Add the WoT Connectivity 1.1 information model
#4140 [WotCon] Add the WoT protocol binding abstractions and planners
#4144 [WotCon] Add the HTTP, Modbus, OPC UA and MQTT binding executors
#4146 [WotCon] Add the WoT Connectivity registry and its stores
#4142 [WotCon] Add the WoT Connectivity registry client
#4154 [WotCon] Materialize WoT Thing Descriptions into the server address space
#4156 [WotCon] Add the WoT Connectivity samples and complete the documentation
#4171 Restore WoT asset registry bridge
#4172 Restore WoT coverage and docs

Supersedes #4093.

Follow-up

The WoT Connectivity and WoT Binding drafts have moved to 1.1-draft2 since this work was authored. Aligning with that revision - the removed group vocabulary, the new projection/View construct, the remaining model vocabulary terms and the two new portable-identity validation rules - is tracked separately and will follow in its own pull request.

Validation

Built for every target framework with zero warnings and zero errors, and validated at the tip of the stack with Opc.Ua.WotCon.Tests at 999 passed / 0 failed and Opc.Ua.Server.Tests at 4024 passed / 0 failed.

Copilot AI review requested due to automatic review settings July 30, 2026 17:22
Comment thread src/Opc.Ua.Types/Utils/FileSystem/IAtomicFileReplace.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds full ExtensionObject raw-body decoding (binary/XML/JSON) and introduces an optional atomic file replace capability for file systems, along with targeted tests.

Changes:

  • Implement raw-body decoding in ExtensionObject and update Variant helpers to delegate structure decoding to it.
  • Introduce IAtomicFileReplace and implement atomic publish for LocalFileSystem and VirtualFileSystem.
  • Add/extend tests covering atomic replace behaviors and updated type/variant/extensionobject semantics.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Opc.Ua.Types.Tests/Utils/FileSystem/AtomicFileReplaceTests.cs New tests validating atomic replace semantics for local and virtual file systems.
tests/Opc.Ua.Types.Tests/Nodes/TypeTableTests.cs Adds coverage for IsEncodingFor and IsTypeOf equivalence scenarios.
tests/Opc.Ua.Types.Tests/BuiltIn/VariantCoverageTests.cs Adds coverage for Variant.TryGetStructure(..., context, ...) decoding behavior.
tests/Opc.Ua.Types.Tests/BuiltIn/ExtensionObjectTests.cs Adds coverage for ExtensionObject decoding of binary/XML/JSON raw bodies.
src/Opc.Ua.Types/Utils/FileSystem/VirtualFileSystem.cs Implements IAtomicFileReplace.Replace via in-memory entry “re-keying”.
src/Opc.Ua.Types/Utils/FileSystem/LocalFileSystem.cs Implements IAtomicFileReplace.Replace using File.Replace / File.Move.
src/Opc.Ua.Types/Utils/FileSystem/IAtomicFileReplace.cs New capability interface documenting atomic publish semantics.
src/Opc.Ua.Types/Nodes/TypeTable.cs Makes IsTypeOf reflexive and relaxes IsEncodingFor to accept direct DataTypeIds.
src/Opc.Ua.Types/BuiltIn/Variant.cs Adds context-aware structure extraction overloads + documentation improvements.
src/Opc.Ua.Types/BuiltIn/TypeInfo.cs Improves ExtensionObject datatype detection when TypeId is already a known datatype.
src/Opc.Ua.Types/BuiltIn/ExtensionObject.cs Adds raw-body decoding using the message context encodeable factory.

Comment thread src/Opc.Ua.Types/Utils/FileSystem/VirtualFileSystem.cs
Comment thread src/Opc.Ua.Types/Utils/FileSystem/LocalFileSystem.cs
Comment thread src/Opc.Ua.Types/BuiltIn/ExtensionObject.cs
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.54545% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.92%. Comparing base (cfd15f0) to head (d7caecc).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
src/Opc.Ua.Types/BuiltIn/ExtensionObject.cs 72.72% 13 Missing and 8 partials ⚠️
src/Opc.Ua.Types/BuiltIn/Variant.cs 91.66% 1 Missing and 1 partial ⚠️
...c/Opc.Ua.Types/Utils/FileSystem/LocalFileSystem.cs 60.00% 1 Missing and 1 partial ⚠️
src/Opc.Ua.Types/BuiltIn/TypeInfo.cs 80.00% 0 Missing and 1 partial ⚠️
...c.Ua.SourceGeneration/SourceGeneratorFileSystem.cs 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4128      +/-   ##
==========================================
- Coverage   80.18%   79.92%   -0.27%     
==========================================
  Files        1514     1515       +1     
  Lines      209245   210114     +869     
  Branches    36008    36244     +236     
==========================================
+ Hits       167789   167932     +143     
- Misses      28878    29573     +695     
- Partials    12578    12609      +31     
Files with missing lines Coverage Δ
src/Opc.Ua.Types/Nodes/TypeTable.cs 98.57% <100.00%> (+0.02%) ⬆️
...pc.Ua.Types/Utils/FileSystem/CombinedFileSystem.cs 55.55% <100.00%> (+4.04%) ⬆️
...rc/Opc.Ua.Types/Utils/FileSystem/NullFileSystem.cs 100.00% <100.00%> (ø)
...pc.Ua.Types/Utils/FileSystem/ResourceFileSystem.cs 40.00% <100.00%> (+1.76%) ⬆️
...Opc.Ua.Types/Utils/FileSystem/VirtualFileSystem.cs 87.58% <100.00%> (+0.86%) ⬆️
src/Opc.Ua.Types/BuiltIn/TypeInfo.cs 83.35% <80.00%> (+0.86%) ⬆️
...c.Ua.SourceGeneration/SourceGeneratorFileSystem.cs 44.82% <0.00%> (-1.61%) ⬇️
src/Opc.Ua.Types/BuiltIn/Variant.cs 80.78% <91.66%> (+0.24%) ⬆️
...c/Opc.Ua.Types/Utils/FileSystem/LocalFileSystem.cs 20.00% <60.00%> (+13.33%) ⬆️
src/Opc.Ua.Types/BuiltIn/ExtensionObject.cs 78.54% <72.72%> (-2.41%) ⬇️

... and 91 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

marcschier and others added 23 commits August 1, 2026 10:30
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…-10-wotcon-model

# Conflicts:
#	tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs
#	tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs
Linearising the stack brought the registry together with the core-types change that
moved atomic replacement onto IFileSystem itself. The registry still carried the
superseded IAtomicFileReplace capability interface, so both designs were present at
once.

Deletes IAtomicFileReplace and migrates its users to IFileSystem.Replace. The blob
store no longer probes for the capability, because every file system now provides it,
and the test that asserted the failure path for a file system without the capability
is removed with it - that state is no longer representable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…alization

# Conflicts:
#	src/Opc.Ua.Types/Utils/FileSystem/VirtualFileSystem.cs
marcschier and others added 23 commits August 5, 2026 06:15
…ransfer' into marcschier/wot-09-xregistry-client
…ion (#4156)

Adds the runnable WoT Connectivity samples and completes the
documentation. This is **PR 16 of a
stack** splitting #4093, and the last one.

## Samples

Three projects under `samples/WotCon/`:

* **FlatTagServer** — a plain OPC UA server exposing flat tags, used as
an aggregation source.
* **AggregationServer** — registers Thing Descriptions with the WoT
registry and materializes two
flat sources into a runtime-loaded DI/Machinery/Pumps Pump model. It
also ships a small in-memory
protocol binding, which is the worked example the binding contributor
guide walks through.
* **AggregationClient** — drives the aggregated model: commands,
`Refresh`, monitoring, and
  generation replacement.

## Documentation

* New `docs/WotBindings.md` — the bindings that ship today
(planner/executor architecture, bundled
vs separate packages, operation coverage, target mapping, lazy channels,
generation lifetime) plus
the contributor guide for adding your own, with diagnostics, tests,
packaging, TFM, trimming and
  NativeAOT guidance.
* Expanded `docs/WoTConnectivity.md` covering the registry,
dependency-closure materialization and
  runtime NodeSet projection.
* `docs/RuntimeNodeSets.md` gains the shadow-reload section and
`docs/XRegistry.md` the shared
byte-store section. Both document APIs introduced earlier in the stack;
they are collected here
  because this is the documentation PR.
* `docs/README.md` links the new pages.

## Solution registration

Registers the three samples and `Opc.Ua.WotCon.Samples.Tests` in
`UA.slnx` so CI actually builds
and runs them.

## Stack position

Sits on top of **#4154** (materialization) and additionally merges
**#4142** (registry client) and
**#4144** (protocol executors), because the samples exercise the whole
stack — `AggregationServer`
needs the OPC UA executor and the client sample needs the registry
client. Its diff therefore shows
their content until they land.

It also carries `WotRegistryProjectionLiveTests.cs`, which #4154
deliberately left out: those tests
need both the materialization runtime *and* the registry client, so this
is the first branch where
they compile.

## Validation

* All three samples build clean on every target framework.
* `Opc.Ua.WotCon.Tests`: **830 passed**, 0 failed (769 from #4154 plus
61 from the live projection
  tests).
* `Opc.Ua.WotCon.Samples.Tests`: 6 of 7 pass locally; the remaining one
fails with
`CryptographicException: The system cannot find the path specified`, a
known certificate-store
problem on this machine that was previously reproduced against a clean
baseline worktree and is
  not related to this change. CI will confirm.
* 0 warnings, 0 errors.

One fix was needed while bringing the sample across: its memory binding
called `ResolveCodec` with
the pre-`out WotPayloadDescriptor` signature. The API gained the payload
out-parameter and a
`bool` result during review of the binding PRs, so the sample now
follows the same
`if (!ResolveCodec(...)) return Unsupported(...)` pattern as the shipped
planners.

Integration PR: #4093.
…pace (#4154)

PR 14 of the stacked series splitting the large WoT Connectivity
integration PR #4093.

This PR adds the server-side materialization runtime that turns WoT
Thing Description resources from the xRegistry snapshot into OPC UA
address-space projections. The coordinator builds the dependency closure
for each resource, plans binding metadata, activates binding channels,
and publishes the resulting projection through the registry NodeManager
so refresh, retire, and reload operations remain observable and
deterministic.

Projection intentionally goes through the NodeManager lifecycle reload
API instead of mutating live nodes directly. That keeps complex type
reload, runtime NodeSet publication, shadow reload, immediate reload,
and retirement on the same lifecycle path as other dynamic server
address-space changes.

This branch has a triple dependency. It is based on #4146 (registry),
and additionally needs #4147 (lifecycle reload API) and #4128
(2-argument TryGetStructure). Both #4147 and #4128 are merged into this
branch, so this PR's diff will show their content until those
dependencies land.

Validation performed:
- dotnet build src\Opc.Ua.WotCon.Server\Opc.Ua.WotCon.Server.csproj -c
Release -f net10.0 -v:m
- dotnet build tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 -v:m
- dotnet test tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 --no-build
- dotnet build tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net48 -v:m
- dotnet test tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net48 --no-build
- dotnet build tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 -v:m
- dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 --no-build (one known inherited
failure: LiveNodeManagerAddRefreshesTheClientNamespaceTable)
PR 15 of a stacked split of integration PR #4093 into independently
reviewable pieces.

This branch is stacked on #4139 (`marcschier/wot-10-wotcon-model`) and
additionally has #4128 (`marcschier/wot-01-core-types`) and #4130
(`marcschier/wot-09-xregistry-client`) merged in so this client slice
can build.

Those extra dependencies are intentional:
- Without #4130: `WotRegistryClient.cs:83` — `error CS1729:
'XRegistryClient' does not contain a constructor that takes 4
arguments`.
- Without #4128: `WotRegistryClient.cs:346,375` and
`WotRegistryResourceClient.cs:144` — `error CS1501: No overload for
method 'TryGetStructure' takes 2 arguments`.

The extra diff from #4128 and #4130 disappears once those PRs land.

This PR adds the WoT Connectivity registry client surface for browsing
and reading registry groups, resources and versions, uploading documents
through FileTransfer, invoking registry methods, and registering the
client through DI/builder helpers.
Adds the WoT Connectivity registry service and stores for Thing
Description and Thing Model documents.

This is PR 13 of the stacked split of integration PR #4093 and is
stacked on #4140 (`marcschier/wot-11-bindings-core`). The
materialization runtime that consumes this registry follows separately.

The registry has no dependency on the binding runtime or the NodeManager
lifecycle; the stacking is only because this change shares the WoT
Connectivity server project file with the preceding PRs.

Validation:
- `dotnet build src\Opc.Ua.WotCon.Server\Opc.Ua.WotCon.Server.csproj -c
Release -f net10.0 -v:m`
- `dotnet build tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test tests\Opc.Ua.WotCon.Tests\Opc.Ua.WotCon.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 --no-build -v:q`
- `dotnet build src\Opc.Ua.WotCon.Server\Opc.Ua.WotCon.Server.csproj -c
Release -v:m`
## Summary
- Adds the concrete WoT binding transport executors for HTTP, Modbus
TCP, OPC UA and MQTT.
- Includes executor/channel tests and in-process HTTP, Modbus, MQTT and
OPC UA harness coverage.
- Leaves CoAP, BACnet, PROFINET and LoRaWAN planner-only because this
stack split intentionally lands validation/planning before executable
transports.

## Stack context
This is PR 12 of the stacked split of integration PR #4093. It is
stacked on #4140, where the protocol-agnostic planner layer landed
separately, so this PR is limited to the concrete transport
implementations.

## Validation
- `dotnet build
tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test
tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 --no-build` (512 passed)
- `dotnet build src\Opc.Ua.WotCon.Bindings\Opc.Ua.WotCon.Bindings.csproj
-c Release -v:m`
- `dotnet build
src\Opc.Ua.WotCon.Bindings.Mqtt\Opc.Ua.WotCon.Bindings.Mqtt.csproj -c
Release -v:m`
## Summary

PR 11 of the stacked split for integration PR #4093. This adds the
protocol-agnostic core of `Opc.Ua.WotCon.Bindings`: binder, planner,
executor and channel-factory contracts; payload codec and credential
provider seams; the binding plan model; polling subscriptions for forms
without native observation; and DI/builder registration helpers.

This PR is stacked on #4139, which itself depends on #4132 and #4134.

## Scope

The included planners validate forms and produce binding plans without
transport I/O:

- HTTP planner and validation only; the concrete HTTP executor follows
in the next PR.
- MQTT planner and validation only; the separate MQTT executor package
follows in the next PR.
- Modbus TCP planner and shared Modbus type/limit definitions only; the
Modbus executor and TCP client follow in the next PR.
- OPC UA planner and validation only; the concrete OPC UA executor
follows in the next PR.
- CoAP, BACnet, PROFINET and LoRaWAN are planner/validation-only in this
split.

Keeping concrete transport executors out of this PR makes the
abstractions and planning layer reviewable without a network stack.

## Validation

- `dotnet build
tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test
tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 --no-build`
- `dotnet build src\Opc.Ua.WotCon.Bindings\Opc.Ua.WotCon.Bindings.csproj
-c Release -v:m`
This is PR 10 of the stacked split of integration PR #4093 into
independently reviewable pieces.

Stack/dependency shape:

- This branch is stacked on #4134 (`marcschier/wot-04-generator-wot`).
- It also requires #4132 (`marcschier/wot-02-sourcegen`), which is
merged into this branch so the model can build.

The #4132 dependency is functional, not cosmetic. Without #4132,
building the WoT Connectivity model on #4134 alone fails with duplicate
generated declarations, for example:

```text
CS0102: The type 'Methods' already contains a definition for 'CreateAssetMethodType'
```

That occurs because the combined WoT-Con NodeSet ships the 1.02
`CreateAssetMethodType` node explicitly; without #4132's
NodeSet-to-ModelDesign fix, the generator synthesizes a second
declaration with the same name.

Once #4132 and #4134 land, the extra diff from those dependencies
disappears and this PR reduces to the three WoT Connectivity model
files:

- `src\Opc.Ua.WotCon\Design\Opc.Ua.WotCon.NodeSet2.xml`
- `src\Opc.Ua.WotCon\Design\Opc.Ua.WotCon.NodeSet2.csv`
- `src\Opc.Ua.WotCon\Opc.Ua.WotCon.csproj`

The generated C# is produced from the pinned NodeSet2 at build time. I
verified it builds from clean after deleting the WotCon `bin`/`obj`, the
source-generation tool `bin`/`obj` directories, and shutting down dotnet
build servers.
This extracts the xRegistry client generalisation from #4093 as PR 9 of
the stacked split into independently reviewable pieces.

The change lets the xRegistry client base accept an explicit registry
root NodeId while preserving the existing well-known-root default.
GenericXRegistryClient exposes the same explicit-root path so callers
can drive registries whose root Object is discovered separately, and the
tests cover the supplied-root and fallback behavior across the lifecycle
helpers.

This PR is independent of the other stack PRs and contains no WoT
registry implementation or Opc.Ua.WotCon.* references. A later PR adds
the WoT registry client on top of this general xRegistry client surface.
…4150)

This is PR 7 of the stacked series splitting integration PR #4093,
stacked on #4147 (`marcschier/wot-05-lifecycle`).

This PR makes subscription transfer between sessions transactional:
subscriptions are prepared before any ownership move, publish-queue
transfer claims block stale source-session publishing, and rollback
restores monitored-item resend-data trigger state.

`ServerInternalData` also gains asynchronous disposal so the
asynchronously disposable subscription manager is shut down without
blocking, with sync and async dispose paths guarded so repeated disposal
is a no-op.

It also brings the transfer-focused coverage for subscription transfer,
publish-queue transfer claims, and `ServerInternalData`
async/double-disposal behavior.

The immediate monitored-item retirement mechanism present in #4093 is
deliberately excluded per maintainer decision. `IRetirableMonitoredItem`
and `INodeManagerMonitoredItemRetirementTracker` do not appear in this
PR.
## Summary

Adds PR 5 of the stack splitting #4093 into independently reviewable
pieces, targeting `master` directly.

This PR extends the NodeManager lifecycle with live shadow reload
support and RuntimeNodeSet lifecycle helpers. `ShadowReloadAsync` stages
a replacement generation and routes new service requests to it while
monitored items already owned by the retired generation continue there
until subscriptions drain.

`ImmediateReloadAsync` keeps the immediate policy but implements it on
the existing detach machinery at the maintainer's request: affected
non-durable monitored items are detached/marked deleted with
`IDetachableMonitoredItem`, queue `BadNodeIdUnknown`, and are not
migrated to the replacement generation.

The prior NodeManager generation is disposed before
`ImmediateReloadAsync` returns in the covered lifecycle path. The
bespoke permanent retirement-error path is intentionally removed, so
follow-up operations use the existing detached-item semantics rather
than a separate retirement state.

## Validation

- `dotnet build src\Opc.Ua.Server\Opc.Ua.Server.csproj -c Release -f
net10.0 -v:m`
- `dotnet build tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net10.0 --no-build` — 3914 passed, 9
skipped, 0 failed
- `dotnet build tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net48 -v:m`
- `dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c
Release -p:CustomTestTarget=net48 --no-build` — full run hit local
durable queue file-lock flake; the failed test passed in isolation. Full
run otherwise reported 3913 passed, 9 skipped, 1 failed.
…files (#4134)

## Summary
- Adds WoT Thing Description and Thing Model AdditionalFiles support to
Opc.Ua.SourceGeneration for .tm.json, .td.json, .tm.jsonld, .td.jsonld,
and explicitly opted-in .jsonld inputs.
- Converts WoT inputs to an in-memory UANodeSet and reuses the existing
NodeSet generation pipeline for constants, node states, and proxies.
- Reports parsing, conversion, and virtual path collision failures as
MODELGEN030 through MODELGEN034 diagnostics instead of generator
exceptions.

## Stack context
This is PR 4 of the stacked split of integration PR #4093. It is stacked
on #4131 (`marcschier/wot-03-converter`) and should be reviewed and
merged after that PR.

The source-generation entry-point project is included whole in this PR
because its final state is irreducibly WoT-coupled. The genuinely
generic generator work lives in the separate PR 2 branch.

## Notes
Roslyn source generators must run synchronously. This PR drives WoT
conversion through the converter's synchronous entry point and does not
use the asynchronous resolver contracts from generator code.

## Validation
- `dotnet build
tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj -c Release
-v:m`
- `dotnet build
tests\Opc.Ua.SourceGeneration.Tests\Opc.Ua.SourceGeneration.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test
tests\Opc.Ua.SourceGeneration.Tests\Opc.Ua.SourceGeneration.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 --no-build`
- `dotnet build
tests\Opc.Ua.SourceGeneration.Tests\Opc.Ua.SourceGeneration.Tests.csproj
-c Release -p:CustomTestTarget=net48 -v:m`
- `dotnet test
tests\Opc.Ua.SourceGeneration.Tests\Opc.Ua.SourceGeneration.Tests.csproj
-c Release -p:CustomTestTarget=net48 --no-build`
…rated node state (#4132)

## Summary
- Improves NodeSet to ModelDesign conversion for combined and
multi-namespace NodeSets.
- Fixes generated method declarations, argument resolution, object type
proxies, fluent builders, generated node state, shared generation
helpers, and validation.
- Regenerates ISA95 NodeIds as generator output from these changes, not
as a hand edit.

## Stack context
This is PR 2 of a stacked split of integration PR #4093 into
independently reviewable pieces. This PR is independent of the other
stack PRs and contains no WoT content.

## Validation
- `dotnet build
tests\Opc.Ua.SourceGeneration.Core.Tests\Opc.Ua.SourceGeneration.Core.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 -v:m`
- `dotnet test
tests\Opc.Ua.SourceGeneration.Core.Tests\Opc.Ua.SourceGeneration.Core.Tests.csproj
-c Release -p:CustomTestTarget=net10.0 --no-build`
- `dotnet build
tests\Opc.Ua.SourceGeneration.Core.Tests\Opc.Ua.SourceGeneration.Core.Tests.csproj
-c Release -p:CustomTestTarget=net48 -v:m`
- `dotnet test
tests\Opc.Ua.SourceGeneration.Core.Tests\Opc.Ua.SourceGeneration.Core.Tests.csproj
-c Release -p:CustomTestTarget=net48 --no-build`
- `dotnet build src\Opc.Ua.ISA95\Opc.Ua.ISA95.csproj -c Release -f
net10.0 -v:m`
- Confirmed ISA95 build leaves
`src\Opc.Ua.ISA95\Design\Common\Opc.ISA95.NodeIds.csv` with no unstaged
regeneration diff.
…4131)

Adds a dependency-light, AOT-safe lexical model for Thing Description
and Thing Model documents to Opc.Ua.Types, together with lossless
converters between WoT documents and NodeSet2 and canonical NodeSet
comparison for round-trip checks.

This is PR 3 of a stacked split of #4093 into independently reviewable
pieces. It is self-contained, targets master directly, and depends on no
other PR in the stack.

The change is a pure library in Opc.Ua.Types with no server/client
involvement and no network I/O of its own. Unknown JSON-LD members are
preserved as digest-protected RFC 6901 residue entries, native NodeSet
projection is emitted according to the preservation mode, and resolver
contracts expose asynchronous bounded resolution with structured
diagnostics.

This PR is still large (~8,000 lines). Suggested review passes:

1. Lexical model and residue preservation.
2. Converter and native projection.
3. Resolver contracts and NodeSet comparison.
@marcschier marcschier changed the title [Types] Complete ExtensionObject decoding and add an atomic file replace capability [WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on Aug 5, 2026
@marcschier
marcschier merged commit e73e711 into master Aug 5, 2026
205 checks passed
@marcschier
marcschier deleted the marcschier/wot-01-core-types branch August 5, 2026 08:14
marcschier added a commit that referenced this pull request Aug 7, 2026
… 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
>
> :x: **Coverage gate failed.** This check is advisory and does not
block the merge.
>
> | Check | Result | Threshold |
> | --- | --- | --- |
> | :white_check_mark: Project line rate | **72.10%** (40 731/56 496
lines) | >= 70.00% |
> | :x: 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
marcschier added a commit that referenced this pull request Aug 7, 2026
…oot override

Two changes after merging master.

Revert the generic Enum.Parse<T> in the Avro configuration model tests. That
overload does not exist on net48, which the test project also targets, so
e10326c broke the net48 leg while chasing a CA2263 warning - undoing the fix
683b192 had already made for exactly this. Building only the src project for
net48 hides it; the net48 test build is what surfaces it. The non-generic form
is back with a scoped CA2263 suppression so the warning cannot invite the same
regression again.

Mirror the registry-root constructor overload XRegistryClient gained in #4128:
a server need not publish the Schema Registry root at the provisional well-known
identifier, so a client that discovered it by Browse can now pass it to
SchemaRegistryClient instead of falling back to the generic client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready Ready to merge once CI Passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants