9 Commits

Author SHA1 Message Date
romanett 333de8305b Add kitchen-sink coverage NodeSet2 with three load pipelines (#4251) (#4272)
# Description

Regression fence for the NodeSet2 import + code-generation surface. This
adds a single, purpose-authored "kitchen-sink" NodeSet2
(`Opc.Ua.CoverageTest.NodeSet2.xml`) plus a small dependent secondary
model that deliberately flip a broad set of the knobs defined by
`UANodeSet.xsd` (every node class, every `Definition` shape, custom
reference types, all five modelling rules, scalar/array/matrix values, a
deep branching instance tree, and a wide set of reference relations).
The same bytes are then loaded into **real** servers (`ServerFixture<T>`
+ `ReferenceServer`) through three independent pipelines and validated
node-by-node, attribute-by-attribute, reference-by-reference:

1. **Source-generation, hand-written manager** - the XML is consumed at
compile time via `<AdditionalFiles
ModelSourceGeneratorPrefix="Opc.Ua.CoverageTest">`; the generated
`AddOpcUaCoverageTest(context)` composes the predefined nodes inside a
`CustomNodeManager2`, so every node materialises as its generated
`NodeState` subclass.
2. **Runtime import** - the identical XML is imported at startup through
`RuntimeNodeSetNodeManagerFactory` / `AddRuntimeNodeSet`.
3. **Source-generation, generated fluent manager** - a
`[NodeManager]`-attributed partial makes the generator emit the whole
`FluentNodeManagerBase` manager, factory and typed fluent-builder
surface.

A shared, data-driven `[TestCaseSource]` battery runs the identical
assertions against all three servers, and a cross-pipeline equivalence
fixture asserts the three address spaces agree. A dependent **second
namespace** (`.../Secondary/`) exercises cross-namespace
`HasTypeDefinition`, `HasSubtype` and `HasComponent` references in every
pipeline.

Authoring the model surfaced two latent source-generator bugs, both
fixed here:

- `NodeSetToModelDesign` crashed on a top-level `<ServerUris>` table
(`StringTable.Append(null)` for the reserved local-server slot). Added a
null guard plus a focused regression test (`NodesetServerUrisTests`).
- `FluentBuilderGenerator` emitted colliding typed-builder accessors for
any model with more than one structure encoding, because
`DataTypeEncoding` objects hang off their `DataType` and surfaced as
parent-less "root" instances sharing the reserved
`DefaultBinary`/`DefaultXml`/`DefaultJson` symbolic names. They are
encoding metadata, never fluent-wired, so they are now excluded from the
typed builder.

A few edges are intentionally not asserted for cross-pipeline
equivalence because the pipelines legitimately differ (documented in
`CoverageNodeSet/README.md`): type-dictionary / `DataTypeDescription`
nodes (the generator regenerates schema rather than emitting them), a
handful of ambiguous duplicate-hierarchical / View-`Organizes` edges,
and runtime structure stand-ins (structure definitions are validated via
the `DataTypeDefinition` round-trip instead).

## Related Issues

- Fixes #4251

## Checklist

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

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-08-17 14:59:58 -07:00
romanett 6e0ed89d3f Make UANodeSetSerializers.g.cs generation deterministic (#4252)
## Problem

Regenerating `src/Opc.Ua.Types/Schema/UANodeSetSerializers.g.cs` via
`UANodeSetSerializers.ps1` produced ~144 lines of no-op churn on every
run. The sgen tool (Microsoft.XmlSerializer.Generator) emits the Reader
`idNN_Name` field declarations and the matching `InitIDs()` initializers
in a hash-seed-dependent order: the assigned id numbers are stable, but
their emission order shuffles between runs. This made an otherwise no-op
regeneration produce a large, noisy diff that was painful to review.

## Fix

- Added a canonicalization pass in `UANodeSetSerializers.ps1` that sorts
each of the two affected contiguous blocks (the `string idNN_Name;`
field declarations and the `idNN_Name = Reader.NameTable.Add(...)`
initializers) by numeric id ascending, so re-running on unchanged input
yields byte-identical output.
- The name regex was widened to `@"[^"]*"` so entries with an empty name
(e.g. `id3_Item`) or URI-style names do not split the block into
independently-sorted runs (which would keep it unstable).
- Regenerated the committed `.g.cs` once through the improved script;
the resulting diff is sort-only.

## Also fixed: tool download

Replaced `Expand-Archive` (which rejects the `.nupkg` extension on
Windows PowerShell) with
`System.IO.Compression.ZipFile.ExtractToDirectory`, so a clean run with
an empty tool cache works end-to-end on both Windows PowerShell 5.1 and
PowerShell 7+.

## Validation

- From an empty tool cache, the full download + extract + generate path
succeeds.
- A second run is a byte-identical no-op (`git diff --exit-code` = 0).
- `dotnet build src\Opc.Ua.Types\Opc.Ua.Types.csproj -c Release -f
net10.0` builds with 0 warnings and 0 errors.
- All AOT post-processing is preserved: custom header, `#if
NET5_0_OR_GREATER` wrapper, module IL2026/IL3050 suppressions,
`[RequiresUnreferencedCode]`/`[RequiresDynamicCode]` on the
InitCallbacks() overrides (Writer = RequiresUnreferencedCode only,
Reader = both), pragmas, UTF-8 BOM, and CRLF line endings.

No hand-edits to `.g.cs`; the fix lives entirely in the script so future
regenerations stay stable.

## Also: Load UserAccessLevel from NodeSet2 model

Bundled here because it regenerates `UANodeSetSerializers.g.cs`, which
this PR owns.

- `UANodeSet.cs`: adds the `{Member}Specified` XmlSerializer convention
to `UAVariable.UserAccessLevel` (new `UserAccessLevelSpecified`
companion; removed the `[DefaultValue((uint)1)]`) so an
explicitly-present `UserAccessLevel` attribute is distinguishable from
the schema default of 1.
- `UANodeSetSerializers.g.cs`: regenerated - the writer now emits
`UserAccessLevel` when `UserAccessLevelSpecified`, and the reader sets
`UserAccessLevelSpecified = true`. Regenerating produced only these two
behavioral changes plus removal of pre-existing empty `if
(o.@MemberSpecified) { }` no-op blocks (see below); no `idNN` re-sort,
so determinism held.
- `UANodeSetHelpers.cs`: the runtime importer honors an
explicitly-specified `UserAccessLevel` instead of always deriving it
from `AccessLevel`.
- Source generator (`ModelDesign.cs` `RawUserAccessLevel`,
`ModelDesignExtensions.GetUserAccessLevelAsCode`,
`NodeSetToModelDesign.cs`): emit the verbatim `UserAccessLevel` when the
NodeSet sets it, otherwise derive from `AccessLevel` (matching the
runtime importer).

### Generator normalization for `{Member}Specified`

sgen emits an extra empty `if (o.@MemberSpecified) { }` no-op block near
`WriteEndElement` for every member that uses the `{Member}Specified`
convention. Adding the companion to `UserAccessLevel` would have
introduced one such block, and the committed file already contained
several for other members (`LastModified`, `AccessRestrictions`,
`PublicationDate`). `UANodeSetSerializers.ps1` now strips these empty
guard blocks during post-processing, so the generated file stays free of
dead code and a re-run of the generator remains a byte-identical no-op.
The real, non-empty guard around the `WriteAttribute` call is preserved.

The ReferenceServer sample application of this feature is intentionally
excluded and lands with #4220.

### Validation (UserAccessLevel)

- `dotnet build src\Opc.Ua.Types\Opc.Ua.Types.csproj -c Release -f
net10.0` and `dotnet build
tools\Opc.Ua.SourceGeneration.Core\Opc.Ua.SourceGeneration.Core.csproj
-c Release`: both 0 warnings / 0 errors.
- `dotnet test tests\Opc.Ua.SourceGeneration.Core.Tests -c Release -f
net8.0 --filter
"FullyQualifiedName~NodeSetToModelDesign|FullyQualifiedName~UserAccessLevel"`:
16 passed, 0 failed.
- Re-running the generator remains a byte-identical no-op.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-08-14 08:00:13 -07:00
romanett cc4791e2fd Import NodeSet2 Views as standalone top-level nodes in the source generator (#4259)
## Problem

The NodeState source generator mishandled `UAView` nodes imported from a
NodeSet2 model. When an exporter set a View's `ParentNodeId` to an
organizing folder, the importer/generator absorbed the View into that
folder's `Children` and tried to emit an invalid `AddChild(...)` call.
Because `ViewState` derives from `NodeState` (not `BaseInstanceState`),
a View can never be an `AddChild` component, so the generated code was
invalid.

## Fix

Make Views standalone top-level predefined nodes linked purely via
`Organizes` references — mirroring how `DataType`/`ReferenceType` type
nodes and `DataTypeEncoding` are handled:

-
**`tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs`**
— clear the `UAView` `ParentNodeId` so the View stays a standalone
top-level node, keep the organizing folder's forward `Organizes`
references to those Views, and carry the View's `ContainsNoLoops`
attribute through to the `ViewDesign`.
-
**`tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs`**
— return `null` for `ViewDesign` in the `AddChild` and `ReplaceChild`
template paths so Views are never emitted as components.
-
**`tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs`**
— new `ViewImport` NodeSet constant and
`ImportPreservesFolderOrganizesReferencesToViews` test verifying Views
are imported as top-level nodes while the organizing folder retains its
forward `Organizes` references to them.

## Validation

- `dotnet build tools\Opc.Ua.SourceGeneration.Core` (Release): 0
warnings / 0 errors.
- `dotnet test tests\Opc.Ua.SourceGeneration.Core.Tests` (net8.0,
filtered to `NodeSetToModelDesign`): all 17 tests pass, including the
new `ImportPreservesFolderOrganizesReferencesToViews`.

Extracted from the larger PR #4220 work — this PR contains only the
source-generator View fix (no WriteMask, UserAccessLevel, NodeSet2.xml,
or ReferenceNodeManager changes).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-14 07:57:06 -07:00
Marc Schier e73e71184d [WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on (#4128)
## 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

- Complete `ExtensionObject` raw-body decoding for binary, XML and JSON
bodies by resolving the concrete type through the message context's
encodeable factory, and simplify the `Variant` structure helpers by
delegating to it (#4128).
- Add `IAtomicFileReplace` as an optional `IFileSystem` capability so
existing external implementations keep working, with atomic publish for
`LocalFileSystem` via `File.Replace`/`File.Move` and for
`VirtualFileSystem` by re-keying the in-memory entry (#4128).
- Add lossless conversion between WoT documents and NodeSet2
(`Opc.Ua.Wot.WotNodeSetConverter`). A byte-exact `uav:nodeSet` envelope
is preserved when requested, the structured `uav:nodes` projection is
used when the readable vocabulary is incomplete, and NodeSet2 is
otherwise synthesized from readable WoT terms. Unmapped JSON members
survive a round trip as pointer-addressed residue (#4131).

## Source generation

- Improve the NodeSet to ModelDesign conversion and the generated node
state (#4132).
- Generate OPC UA models directly from WoT Thing Description files
(#4134).

## 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 (#4147).
- Make subscription transfer between sessions transactional, so a failed
transfer leaves neither session holding a partially moved subscription
(#4150).

## xRegistry

- Allow clients to use explicit registry roots rather than assuming a
well-known location (#4130).

## WoT Connectivity

- Add the WoT Connectivity 1.1 information model: a registry-first
revision layered on the abstract xRegistry base model, incorporating the
complete published OPC 10100-1 v1.02 surface as deprecated nodes in the
same namespace so existing 1.02 clients keep working (#4139).
- Add the protocol binding abstractions and planners that compile WoT
forms into executable plans (#4140).
- Add the HTTP, Modbus, OPC UA and MQTT binding executors (#4144).
- Add the registry and its stores, including document versioning,
validation, dependency resolution and refresh (#4146).
- Add the registry client (#4142).
- Materialize WoT Thing Descriptions into the server address space,
deriving types from Thing Models and instances from Thing Descriptions
(#4154).
- Restore the WoT asset registry bridge so the deprecated 1.02 asset
surface is backed by the registry (#4171).
- Add the WoT Connectivity samples and complete the documentation
(#4156), and restore the remaining coverage and docs (#4172).

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
2026-08-05 10:14:15 +02:00
Marc Schier 0357380335 Source generator: typed NodeSet method signatures and referenced model accessors (#4124)
**Stack 2/5** — base #4123. Part of the #4096 split; see #4096 for the
overall picture.

> Review after #4123. The diff shown against `master` will include #4123
until that merges.

## What this is

Source generator only; no runtime library changes.

## Contents

- **Typed NodeSet method signatures.** A method imported from a NodeSet
now gets a typed signature. Methods that share a declared signature are
grouped onto one generated `MethodType`, named after its owner when the
plain name would collide, so two unrelated `Execute` methods no longer
share a class.
- **An override that keeps the signature it inherits keeps the
declaration it overrides.** Pointing `MethodDeclarationNode` at the
override itself made `MethodDeclarationId` self-referential. OPC 10000-4
lets a caller invoke a method with the NodeId of its declaration in the
ObjectType *or any supertype*, and `MethodState.MethodDeclarationId` is
what the server matches on, so `Call` stopped resolving and returned
`Bad_MethodInvalid` for every alarm subtype. An override that genuinely
changes its signature still becomes its own declaration.
- **`StateNumbers` / `TransitionNumbers` were emitted empty for every
NodeSet-sourced model**, because such children arrive as
`VariableDesign` rather than `PropertyDesign` and were filtered out. Any
companion model built from a NodeSet was writing `StateNumber = 0` on
the wire — a Part 16 defect well beyond the models in this stack. The
ModelDesign path is unchanged.
- **Referenced models can contribute fluent accessors**, carried by a
model dependency payload so a downstream model can extend an upstream
one.

## Notes for the reviewer

- `tests/Opc.Ua.SourceGeneration.Tests` is left at its `master` content
here. Its new vendor-subtype test uses the Robotics NodeSet as its
fixture, so it can only land with the Robotics change at the top of the
stack (5/5).
- `PumpInstanceNodeIdRegressionTests` gains six
`InputArguments`/`OutputArguments` entries for argument-less standard
condition methods. Typed method signatures materialise those properties
from the `ConditionType` declaration even where the method takes none.
That is a real deviation, pinned rather than hidden so it stays visible;
it is worth a follow-up.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.SourceGeneration.Core.Tests | 3739 / 0 |
| Opc.Ua.SourceGeneration.Tests | 81 / 0 |
| Opc.Ua.Di.Tests | 304 / 0 |
| Opc.Ua.History.Tests | 506 / 0 |
| Opc.Ua.Gds.Tests | 1095 / 0 |

History and GDS are listed because they are what the method-declaration
fix above is load-bearing for: without it they fail with 10 and 370
errors respectively.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
2026-08-01 07:55:57 +02:00
Marc Schier 8a10139b8c Fix OPC UA address-space compliance in the device integration stack (#4117)
# Description

A live OPC UA compliance audit of `samples/PumpDeviceIntegrationServer`
against the **OPC 40223 Pumps** and **OPC 10000-100 (DI)** companion
specifications found **13 address-space violations** plus a
serialization bug in the MCP tool. Every fix lands at its **root cause
in the shared stack** rather than as a workaround in the sample, so all
servers benefit.

The sample now exposes **N fully simulated pumps** (`--pumps N`) and
advertises the DI conformance facets it actually satisfies.

> This branch has been merged with `master` including **#4116** ("Add
simulation to PumpDeviceIntegrationServer and Event modelling"), which
independently reworked the same pump simulation and the same fluent
alarm builders. See [Relationship to #4116](#relationship-to-4116) below
for how the two were reconciled.

## Findings and fixes

| # | Finding | Root cause | Fix |
|---|---------|-----------|-----|
| 1 | `OverTempAlarm` created by the fluent API did not exist in the
address space | `AttachAlarm` attached the alarm with `AddChild` but
never indexed it into the node manager | Register the node; the same gap
existed in `AddObject`, `CreateInstance` and the state-machine creators
|
| 2 | Alarms were unsubscribable — `EventNotifier = 0` everywhere, no
`HasNotifier` from `Server` | No event-source wiring at all | Set
`HasCondition`, initialise
`SourceNode`/`SourceName`/`ConditionName`/`InputNode`, promote
`EventNotifier` up the ancestor chain, publish `HasNotifier` from the
`Server` object |
| 3 | Every instance NodeId was minted in the **DI standard namespace**
(`ns=4;s=5001_Pump #1`) | `DiNodeManager.New` inherited the parent's
namespace index | New `DiNodeManager.InstanceNamespaceIndex`; instances
move to the server's application namespace |
| 4 | `Server/Namespaces` described only 2 of 6 namespaces | The
NodeSet2 importer drops nodes parented to namespace 0, so companion-spec
metadata objects never reached the address space — and no route covered
the server's own namespace | New `NamespaceMetadataPublisher` walks the
`NamespaceArray` and fills version/publication date from
`ModelDependencyAttribute` |
| 5 | Vendor BrowseNames (`Diagnostics`, `LastError`, …) sat in the DI
namespace | Builders defaulted to the parent's namespace | Default to
the server namespace; **spec-defined BrowseNames are unchanged** |
| 6 | `AccessLevel="5"` from the Pumps NodeSet was reported as `1` | The
ModelDesign `AccessLevel` enum is not `[Flags]` and cannot represent
`CurrentRead \| HistoryRead` | Carry the verbatim NodeSet2 bitmask and
emit named `AccessLevels` constants |
| 7 | `Historizing = true` while `HistoryRead` returned
`BadHistoryOperationUnsupported` | Nothing reconciled the declared
history surface with the wired historians | Startup clears the
advertisement (including masking the attribute read callbacks) when no
provider resolves; the sample wires a real historian |
| 8 | Mandatory `ProductInstanceUri` unset | Sample omission | Populated
on every pump |
| 9 | Mandatory `TrueState`/`FalseState` empty on every supervision
boolean | Absent in the official NodeSet; a server must still populate
them | Populated on every pump |
| 10 | Machinery `Machines` folder empty (OPC 40001-1 §9.2 says
**shall**) | `TryAddToMachinesFolder` used `AddChild`, which
*re-parents* the device away from `DeviceSet` | Add the `Organizes`
reference instead |
| 11 | BrowseName contained a space and `#` | Sample naming | `Pump_1`
with the readable label in `DisplayName` |
| 12 | `Maintenance` group materialised but empty | Sample omission |
Populated |
| 13 | Uninitialised values reported `Good` | No initial-value status |
`BadWaitingForInitialData` until the first simulation tick |
| — | `ServerProfileArray` advertised only `StandardUA2017` |
`DiNodeManager` did not implement the existing `IConformanceContributor`
| Contribute DI 1.05 server facets computed at runtime from what is
actually wired, merged with `StandardUA2017` |
| — | MCP tool serialised boolean `false` and numeric `0` as JSON `null`
| `Variant.Null` is `default`, so `Variant.Equals` adopts the non-null
type and compares against the default value | Dispatch on `BuiltInType`
and use typed `TryGetValue` accessors instead of the prohibited
`AsBoxedObject` |

## Relationship to #4116

#4116 landed on `master` while this branch was in flight and targeted
overlapping ground. The merge is a **union of both intents**, not a
fast-forward:

- **`AlarmBuilderExtensions`** — kept both sides. #4116 contributes the
parent-must-be-an-Object guard, `SetEnableState`, and the
`HasEventSource` references; this branch contributes the `HasCondition`
reference, condition source initialisation, **registration of the alarm
with the node manager so it is browsable**, notifier promotion up the
whole ancestor chain, and root-notifier registration.
- **`SupervisionBuilderExtensions`** — took #4116's `SetAlarmActive`. It
is more spec-correct than this branch's version: it honours
`EnabledState`, resets Acked/Confirmed on activation, and computes
`Retain` per OPC 10000-9 so an unacknowledged Condition stays retained
after going inactive. This branch's null guard was kept so a malformed
alarm cannot dereference a missing `EnabledState`.
- **`PumpNodeManager.Configure.cs`** — took #4116's **push-based**
simulation (`RegisterPumpSimulation`, `CreatePumpSimulation`,
`IValueUpdater<T>`, `Initialize`/`Advance`/`Publish`, deterministic
per-pump phase offsets) as the structure, then folded in this branch's
compliance work: configurable pump count, Maintenance group,
`TrueState`/`FalseState`, historian wiring, per-pump mandatory
Identification values, and `BadWaitingForInitialData` until the first
tick.
- **README** — union, keeping #4116's address-space validation workflow
section.

Two test adjustments were needed, both because the merge legitimately
changed behaviour the tests pinned:

- `PumpHostedReferenceTests` now resolves pump NodeIds from the browse
result instead of hard-coding the DI namespace, and waits for the first
published value. `PumpCreatedAfterStartupJoinsTheLiveSimulationAsync`
was **added** to restore #4116's coverage that a pump created after
startup through `ConfigureDevicesFor` joins the live simulation.
- `FluentAlarmRegistrationIntegrationTests` no longer asserts that
`Retain` clears when an alarm goes inactive — that pinned this branch's
weaker semantics, and #4116's Part 9 behaviour is correct.

## Breaking change

Runtime-created DI device instance NodeIds move from the companion-spec
namespace to the server's application namespace, e.g.
`ns=<DI>;s=5001_Pump #1` becomes `ns=<application>;s=5001_Pump #1`. This
invalidates NodeIds cached by clients from earlier 2.0 previews. It is
**not** a model change — standard DI/Machinery/Pumps type NodeIds and
spec-defined BrowseNames are untouched.

Device integration hosting is new in 2.0, so there is nothing to migrate
from 1.5.378 and this is deliberately *not* in `docs/MigrationGuide.md`.
Anyone tracking 2.0 previews should rediscover devices by browsing
`DeviceSet` rather than persisting NodeIds, and resolve namespace
indexes from `NamespaceArray` per connection.

## Verification

Compliance is pinned by regression tests that assert **through the
server's service surface** (`BrowseAsync` / `ReadAsync` /
`HistoryReadAsync`), not by inspecting `NodeState` objects in memory.
That distinction mattered: three defects survived in-memory assertions
and were only caught through the read path — most subtly, `Historizing`
was cleared while `AccessLevel` still advertised `HistoryRead`, because
attribute read callbacks re-applied the NodeSet bits.

Post-merge on net10.0:

| Suite | Result |
|---|---|
| `Opc.Ua.Di.Tests` | 326 passed |
| `Opc.Ua.Server.Tests` | 3846 passed, 5 skipped |
| `Opc.Ua.Tools.Tests` | 288 passed |
| `Opc.Ua.History.Tests` | 506 passed, 27 skipped |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |

The full solution was also built and tested on **net48** before the
merge.

Review feedback from `copilot-pull-request-reviewer` has been addressed
in commit `badad31dc`: the shared `ServerObjectState` lock was removed
(the forward `HasNotifier` edge is published through the owning node
manager instead), MCP array serialization now preserves element types,
and the pump option guards report the specific rejected option. The only
failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
pre-existing certificate-store issue that **passes when run in
isolation** and is unrelated to this change.

## Documentation

Updated `docs/DeviceIntegration.md`, `docs/MigrationGuide.md`,
`docs/HistoricalAccess.md`, `docs/DeveloperGuide.md`,
`docs/McpServer.md`, `docs/README.md`,
`samples/PumpDeviceIntegrationServer/README.md` and
`tools/Opc.Ua.Mcp/README.md`, and consolidated the four node-manager
documents into a single [`docs/NodeManagers.md`](docs/NodeManagers.md) —
overview, built-in managers (including `MasterNodeManager`), core vs
custom, registration, server address-space metadata, and source
generation — with a TOC and all inbound links repointed.

## Related Issues

No tracking issue exists for this work yet — it originated from an
ad-hoc compliance audit of the pump sample. Given the size of the change
and the runtime-breaking NodeId move, please open (or link) a tracking
issue so the design is recorded as an ADR before merging.

- Fixes #

## Checklist

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

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0d50c3fc-58b1-4c99-85a4-1e32c88666d9
2026-07-30 16:04:18 +02:00
Marc Schier 5b9bcedc62 Add OPC UA ISA-95 Common Model and Job Control support (#4089)
# Description

Adds first-class support for the OPC UA ISA-95 companion specifications:

- OPC-10030 Common Model.
- OPC-10031-4 Job Control V1.
- OPC-10031-4 Job Control V2.

The change introduces `Opc.Ua.ISA95`, `Opc.Ua.ISA95.Client`, and
`Opc.Ua.ISA95.Server`, together with a NativeAOT-compatible minimal
server sample and comprehensive tests.

## Highlights

- Compiles the three canonical NodeSets and identifier mapping CSV files
through the source generator.
- Adds NodeSet identifier mapping validation, typed method
arguments/results, companion event records, and generated method
argument nodes.
- Provides typed Common Model builders with normative relationships and
an injectable `GeoSpatialLocationType` provider.
- Implements lossless V1/V2 Job Control providers over a shared
version-neutral store, including state paths, status events, comments,
response queries, catalog changes, and cohesive DI registration.
- Adds subtype-aware typed clients, standalone encodeable registration,
`IIsa95ClientFactory`, and V2 status-event streaming.
- Adds the `MinimalIsa95Server` sample, package documentation, and an
ISA-95 guide with conformance and HA guidance.
- Tracks typed RSL/GPOS integration as follow-up issue #4095.

## Validation

- `Opc.Ua.ISA95.Tests`: 137/137 passed on `net10.0` and `net48`.
- Source-generator suites: 81/81 model, 90/90 stack, 3718 passed / 8
skipped core.
- `EventRecordFieldReaders` feedback tests: 6/6 on `net10.0` and
`net48`.
- `EventRecordFieldReaders` changed-method patch coverage: 100% line
coverage (36/36) and 100% branch coverage (22/22).
- `Opc.Ua.ISA95`, `.Client`, and `.Server` build across all configured
target frameworks without new diagnostics.
- Cobertura line coverage: 88.08% model, 90.04% client, 91.19% server.
- NativeAOT sample publishes and passes a live typed V2
Store/status-event validation.
- The ISA-95 NativeAOT smoke test passes in the source-generated native
test executable.
- OPC UA KB NodeSet validation reports zero errors for Common, V1, and
V2.

The broader NativeAOT executable still reports unrelated existing GDS
and complex-type test failures; the ISA-95-specific native test passes.

## Node state generator fixes

Generating method arguments inline for imported nodesets regressed every
model that already emits `InputArguments` / `OutputArguments` child node
factories:

- The inline block ran before the child factories.
`MethodState.CreateOrReplaceInputArguments` keeps the property that was
assigned first, so the child carrying the well-known NodeId was dropped
and a NodeId-less placeholder remained in the address space. This broke
browsing (reference descriptions without a target) and made method
arguments unreachable at their well-known NodeIds.
- The `MethodDeclarationId` fallback keyed on an autogenerated display
name redirected every declared method of the standard address space to
its own NodeId, which made method calls fail.

The inline block is now emitted after the child factories and reduced to
a value assignment when a child factory exists. The value is only
re-emitted when an argument references a namespace other than OPC UA,
because the value of an imported property is the verbatim XML of the
source nodeset whose namespace indices do not survive the import. The
`MethodDeclarationId` fallback is restricted to declarations without a
resolvable NodeId. The generated OPC UA address space is byte-identical
to `master` again.

## Related Issues

- Fixes #4088
- Follow-up: #4095
2026-07-28 12:01:57 +02:00
Marc Schier 38c7356bc7 Add generic xRegistry libraries (abstract registry base model) (#4097)
# Description

xRegistry is the *abstract registry base model*: a registry-agnostic,
content-addressed
resource registry. It knows nothing about what a resource contains — a
concrete registry supplies its
own companion namespace and a fingerprinting strategy and reuses
everything else. The PubSub Schema
Registry in #4007 is the first specialization (its resources are schema
documents),
and #4093 (WoT Binding) carries a byte-identical copy of the same base
NodeSet, so a
shared canonical home is worth having.

## Packages

| Package | Depends on | Contains |
| --- | --- | --- |
| `OPCFoundation.NetStandard.Opc.Ua.XRegistry` | `Opc.Ua.Core` |
Abstract base companion model (compiled by the model source generator),
`XRegistryWellKnown`, `IResourceContentIdProvider` |
| `OPCFoundation.NetStandard.Opc.Ua.XRegistry.Client` | +
`Opc.Ua.Client` | `XRegistryClient` (abstract) /
`GenericXRegistryClient` (sealed), built on the generated ObjectType
proxies |
| `OPCFoundation.NetStandard.Opc.Ua.XRegistry.Server` | +
`Opc.Ua.Server` | Fast-path / registration / federation node managers,
`XRegistryServerOptions`, `IXRegistryResourceStore` |

`Opc.Ua.XRegistry` deliberately depends on neither SDK, so a codec or a
shared contracts assembly can
reference the identity abstraction without pulling in the client or the
server.

## The model is compiled, not loaded

The companion model is compiled into the assemblies by the OPC UA model
source generator.
`Opc.Ua.XRegistry.NodeSet2.xml` is an `AdditionalFiles` generator input
only — there is no
`EmbeddedResource`, no runtime NodeSet parsing and no hand-written
NodeId tables for anything the
model declares. All three node managers load it the same way:

```csharp
protected override NodeStateCollection LoadPredefinedNodes(ISystemContext context)
{
    return new NodeStateCollection().AddOpcUaXRegistry(context);
}
```

`XRegistryWellKnown` is reduced to the namespace URI plus the
identifiers of the *instances* a
registry materializes at runtime, which live above the model's own
63000-63999 range so they can
never collide with it.

## What it does

- **Content-derived identity** — `IResourceContentIdProvider` maps a
document + format to a
fingerprint and names the (canonicalization, hash) algorithm. Identity
is derived from the bytes,
not assigned by the server, so it is stable across registries — which is
what makes de-duplication
  and federation work.
- **Opaque-NodeId fast path** — a registered resource is reachable at an
Opaque `NodeId` whose
Identifier is the raw content-id bytes, so a consumer that received the
id on the wire resolves the
document in a single `Read`: no Browse, no fingerprint recomputation.
The client reads it through
the `ReadBytesAsync` session extension, so a document larger than
`MaxByteStringLength` is fetched
with range-based reads rather than failing. Optionally pre-publishes a
seed resource so a fresh
  server can resolve one resource before any registration.
- **The model's own lifecycle** — the registry root is a `RegistryType`
instance serving
`CreateGroup` / `GetOrCreateGroup`; each group serves `CreateResource` /
`GetOrCreateResource`; and
because `ResourceType : FileType`, the document *is* the file — content
is streamed through the
inherited `Open` / `Read` / `Write` / `Close`. `Close` fingerprints the
accumulated bytes and
publishes the Opaque fast-path node at runtime. Identical bytes reuse
the existing node, whose
lifetime is reference counted so it survives until the last resource
resolving to those bytes is
gone and is retired when a resource is re-written with different
content.
- **Optimistic concurrency** — `Delete(ExpectedEpoch)` on both resources
and groups, and the same
epoch check on label mutations, so a caller working from a stale read is
rejected rather than
  clobbering a concurrent change.
- **Labels** — `RegistryType`, `GroupType` and `ResourceType` each
expose an `AttributesType` Object
with `AddAttribute` / `RemoveAttribute`; labels are published as
addressable `String` Properties.
- **Federation** — a resource hosted by another registry is published as
a real `ResourceType`
instance whose `ExternalReference` (`ExpandedNodeId` + `ServerIndex`
into the local `ServerArray`)
and `ResourceUrl` carry the link and whose `Xid` carries the content-id.
Because it is an ordinary
`ResourceType`, a generic client drives a federated resource through
exactly the same proxy as a
  local one.

## Extensibility

A domain registry subtypes the base model — the PubSub Schema Registry
declares
`SchemaFileType : ResourceType` — and the generator mirrors that
hierarchy across assemblies, so the
proxy chain follows automatically (`SchemaFileTypeClient :
ResourceTypeClient : FileTypeClient`).

```text
abstract XRegistryClient
   ├── sealed GenericXRegistryClient   // any registry namespace
   ├── SchemaRegistryClient  (domain)
   └── WotRegistryClient     (domain)
```

* A **domain client** derives from `XRegistryClient` and inherits the
whole lifecycle. Helpers
written as extension methods over a base proxy (such as
`WriteDocumentAsync` on
`ResourceTypeClient`) are callable on the domain proxy with no
inheritance in the client layer.
* A **generic client** still drives a domain registry, because a domain
instance *is* an instance of
the base type. This is covered by a test that defines a domain subclass
over the base.

## State

Document bytes live behind an injectable `IXRegistryResourceStore`.
Because a resource is a
`ResourceType`, which *is* a `FileType`, the store mirrors the file
access model: reads and writes are
**offset and length based**, so a document never has to be materialized
as a whole and a ranged read
touches only the bytes it needs.

The document is transferred with the inherited `FileType` Methods, whose
mode bits are honoured: a
write that does not set `EraseExisting` starts from the stored bytes and
replaces only the range it
writes rather than truncating the rest. A handle is valid only on the
resource and the session that
opened it, and a session's handles are released when it closes.

Two implementations ship: `InMemoryResourceStore` (the default,
in-process) and
`FileSystemResourceStore`, built on the `IFileSystem` abstraction so
documents outlive the
process and a shared volume can back a cluster.
`XRegistryResourceStoreContractTests` is a reusable
fixture that validates any implementation against the contract.

## Transport security

Registry **writes always require a `SignAndEncrypt` secure channel** — a
document and its
content-derived identity are integrity-critical, so every mutation is
rejected with
`BadSecurityModeInsufficient` on a channel that is only signed or
unprotected. This is not
configurable. Reads are allowed on any secure channel by default;
`RequireEncryptionForReads` extends
the requirement to them when the documents themselves are confidential.
An in-process call carries no
channel and is always allowed.

## Hardening

The lifecycle Methods are remotely callable, so every unbounded
dimension is bounded via
`XRegistryServerOptions`; exceeding a bound fails the call rather than
the server:

| Option | Default | Enforced on | Status code |
| --- | --- | --- | --- |
| `MaxConcurrentUploads` | 64 | `CreateResource`, `Open` |
`BadTooManyOperations` |
| `MaxResourceBytes` | 16 MiB | `Write` | `BadRequestTooLarge` |
| `MaxRegisteredResources` | 4096 | `CreateResource` |
`BadTooManyOperations` |

## A source generator bug fixed along the way

Building on the compiled model surfaced a defect in shared generator
infrastructure.
`NodeSetToModelDesign.CreateDecoder` decoded values with an empty
`EncodeableFactory`, so a NodeSet2's
method `InputArguments` / `OutputArguments` — encoded as a list of
`Argument` ExtensionObjects — never
decoded, while `HasArguments` was still set. **Every** NodeSet2-sourced
model in the stack was
silently generating argument-less `MethodState`s and ObjectType proxies.
ModelDesign-sourced models
(the core stack) were unaffected, which is why this went unnoticed.
Fixed in `b87726af` with a
regression test that fails without it.

Two defects in the xRegistry NodeSet itself were fixed at the same time:
`BrowseName="1:InputArguments"`
→ ns-0 `InputArguments` (published NodeSets never namespace-qualify
these), and the `CreateResource` /
`GetOrCreateResource` **output** `VersionId` renamed to
`AssignedVersionId`, which collided with the
input of the same name and produced a duplicate C# parameter.

## Other fixes applied while extracting

- `XRegistryRegistrationNodeManager` uses `System.Threading.Lock`
instead of an `object`-typed sync
  root, per the repository locking rules.
- Dropped the `InternalsVisibleTo` entries granting internals to
`Opc.Ua.PubSub.Server.Tests`. That
  project only consumes public `XRegistryWellKnown` API.
- The `UA.slnx` edit in #4007 accidentally **replaced** the
`tests/Opc.Ua.Redundancy.Kubernetes.Tests` entry instead of adding a
line, silently dropping that
project from the solution. The entry is preserved here and the change is
purely additive.

## Tests

`tests/Opc.Ua.XRegistry.Tests` (168) covers the group and resource
lifecycles, the `FileType`
transfer, delete and epoch conflicts, labels, federation, both resource
stores, the fast path,
transport security and the extensibility contract — positive and
negative paths. Passing on `net10.0`
and `net48`. Patch line coverage is **98.5%**.

Also adds `tests/Directory.Build.props` / `.targets`: the .NET 10 SDK
prunes
`Microsoft.Extensions.Logging.Abstractions` out of the test output, and
coverlet's Mono.Cecil
resolver only searches the output directory, so it silently fails to
instrument every module that
references `ILogger` and reports 0% coverage for them. These force the
package back into the restore
graph and copy its implementation next to the test binary.

## Documentation

New [`docs/XRegistry.md`](docs/XRegistry.md), linked from the
documentation index.
2026-07-28 08:45:03 +02:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00