5 Commits
Author SHA1 Message Date
651beaee1e Fully support AliasNames in ConsoleReferenceServer (#4012) (#4321)
# Description

The CTT AliasName conformance units skip or fail against the
ConsoleReferenceServer. `AliasName Base` skips with *"No instance of
AliasNameType has been found in the address space"*; `Category Tags`,
`Category Topics` and `Hierarchy` fail. Part 17 support landed in #3779,
but aliases live only inside the `IAliasNameStore` — nothing is
browsable, no `AliasNameCategoryType` is nested under another, and the
Topics aliases point at the Server object rather than at a
`PublishedDataSetType` instance.

Part 17 §6.2 clients — the CTT among them — discover aliases by
browsing, not only by calling `FindAlias`. This PR materializes the
store contents as real nodes and fills the three gaps.

### SDK — `Opc.Ua.Server`

* **`DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync`**
(new, opt-in) walks every registered `IAliasNameStore` and creates one
`AliasNameType` instance per alias — BrowseName carrying the alias name,
`AliasFor` references to its targets, and the inverse `HasAlias`
reference on each local target (via `externalReferences`, since targets
usually live in another node manager) — plus an `AliasNameCategoryType`
instance for every store category the standard NodeSet does not already
ship. Categories whose NodeId belongs to another node manager are
skipped, so an `AliasNameNodeManager` that also registers its store with
the server-wide registry does not get its NodeIds claimed twice.

Opt-in: servers that only need the well-known `FindAlias` to answer from
their store pay nothing. The nodes are a snapshot taken at address-space
creation — later mutations change what `FindAlias` returns but do not
add or remove nodes.

* **Optional Part 17 methods on the well-known categories.** The same
pass instantiates whatever a category declares through
`AliasNameCapabilities`: `FindAliasVerbose` (§6.3.3),
`AddAliasesToCategory` (§6.3.4), `DeleteAliasesFromCategory` (§6.3.5)
and `LastChange` (§6.3.1). Mutations require a `SecurityAdmin` caller on
a `SignAndEncrypt` channel; the gate previously private to
`AliasNameNodeManager` moved to `AliasNameMethodDispatcher` so both
paths share one implementation.

* **`LastChange`** is now tracked per category rather than only for the
standard `Aliases` node, and the duplicated seeding logic collapsed into
one helper.

### Reference server

* A nested **`Devices`** category under `TagVariables` holds
`Pump1_Status` and `Heater_Power`. `FindAlias` on `TagVariables` still
returns all six names through subtree recursion, while `FindAlias` on
`Devices` returns exactly two — which is what the Hierarchy unit checks.
* `ReferenceServerConfigurationNodeManager` creates three
**`PublishedDataSet` instances** under `PublishedDataSets` (`i=17371`)
and the Topics aliases now target them, satisfying the *"is a Dataset"*
check. These are address-space instances only — no PubSub runtime is
added, and the CTT only inspects the target's type definition.

## Note for reviewers — the reserved NodeIds

The optional children are created at the NodeIds the OPC Foundation
reserves for them (`Aliases.FindAliasVerbose` = `i=24054`,
`TagVariables.LastChange` = `i=32854`, …). Those identifiers are
allocated in `StandardTypes.csv` but are **not** declared in the
ModelDesign and **not** present in the published NodeSet, because the
standard address space does not instantiate optional children. The
source generator therefore emits no parent-to-instance mapping and no
`MethodIds` constants for them, so
`DiagnosticsNodeManager.ReservedChildIds` supplies the nine method ids
and the two missing `LastChange` ids explicitly, each traceable to its
registry row. The 18 argument-property ids do have generated
`VariableIds` constants and are referenced through those.

I deliberately did **not** "fix" this by declaring the children in
`tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml`: that file
is a verbatim copy of upstream
`UA-ModelCompiler/Design.v105/StandardTypes.xml` (the only differences
today are upstream being newer), so a local edit is lost on the next
sync and the NodeIds would silently revert to factory-minted ones.
`docs/AliasNames.md` records this.

Worth raising upstream separately: `StandardTypes.csv` allocates
`Aliases_FindAliasVerbose` and siblings while the ModelDesign never
declares those children, so the ModelCompiler cannot emit ids it has
itself reserved. If that is ever reconciled, `ReservedChildIds` becomes
redundant and deletes cleanly.

## Related Issues

- Fixes #4012

## 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. — **not
done: verified on net8.0 only** (Server 4710, InformationModel 1099,
Core 4443, plus the client and features alias suites; AOT build clean).
Needs a run against net framework and net10 before this leaves draft.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Tests

`tests/Opc.Ua.InformationModel.Tests/Aliasname*Tests.cs` mirror the four
CTT conformance units and no longer `Assert.Ignore` where the CTT
reported failures — the alias suite is 37 tests with no skips. New
`AliasnameOptionalMethodsTests` covers the optional methods, their
reserved NodeIds, the anonymous-caller denial, an add/find/delete round
trip and `LastChange` advancing on mutation.

One pre-existing test changed premise:
`EnumerateSubCategoriesAsyncOnLeafReturnsEmptyAsync` asserted that
`TagVariables` has no sub-categories, which this PR deliberately makes
false. It now targets `Topics` (still a leaf) and a new test guards the
nesting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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: marcschier <11168470+marcschier@users.noreply.github.com>
2026-08-29 18:05:42 +00:00
romanettandCopilot App d547747e22 Generate ReferenceNodeManager address space from a NodeSet2 model (#4220)
# Description

The CTT/compliance `ReferenceNodeManager` built its ~6,000 node address
space entirely in imperative C#. This PR migrates it to a
source-generated `[NodeManager]` partial driven by an exported NodeSet2
model (`ReferenceServer.NodeSet2.xml`), mirroring the
`MinimalBoilerServer` sample and completing the goal of issue #4204.

The address space is now materialized from the model by the source
generator. The large orchestration body was kept but converted from
create-based to **find-instead-of-create**: each `Create*` primitive now
looks up the model-materialized node via `FindPredefinedNode<T>` and
re-applies runtime values, DataAccess metadata, and write handlers,
rather than constructing nodes from scratch. This keeps the
deterministic value seeding and behavior wiring intact while the
structure comes from the model.

## Notable details for reviewers

- **Idempotent reference wiring.** The mesh references in the
`References` folder are already present in the model, so the historic
manual `AddReference` calls would throw on duplicates. Added a small
`AddReferenceIfMissing` helper so those calls are safe.
- **Non-string NodeIds.** The `NodeIds` folder assigns numeric / GUID /
opaque NodeIds. The model exports those nodes with their final ids, so
find-by-string-path no longer works; added `CreateVariableById(NodeId)`
to locate them by their real id.
- **Generator bug fix (worth a careful look).**
`ModelDesignExtensions.GetValueRankAsCode` reconstructed `ValueRank` as
`dimensions.Length + 1`, so a node with `ArrayDimensions="3,3"` was
emitted as `ValueRank = 3`. This affected **any** NodeSet2 with a fixed
multi-dimensional array (rank-3 matrices, `BadTypeMismatch` on
whole-matrix writes, wrong decoded dimensions). Corrected to
`dimensions.Length` and updated the unit tests that had codified the
off-by-one.

## Validation

- `ReferenceServerTest`: 44/44 on net10.0 and net48
- `Opc.Ua.SourceGeneration.Core.Tests`: pass on
net8.0/net9.0/net10.0/net48
- Integration on net10.0: ComplexTypes, Client, History, Sessions,
Subscriptions all pass
- Clean build, no new analyzer warnings

## Related Issues

- Fixes #4204

## 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.
- [ ] 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>
2026-08-15 05:22:31 -07:00
Marc SchierandCopilot 9375a2431b Retire the IServerInternal bind mutators and freeze the bind phase (#4197)
## Stacked on #4193

Base is `marcschier/server-context`, which is itself based on #4188.
**Review #4188 and
#4193 first.** GitHub retargets each base automatically as the stack
merges.

Closes #4184.

## What this does

`IServerInternal` published twelve `Set*` binding methods plus
`CreateServerObjectAsync`,
so any holder of the interface could rewire a running server. This takes
them off the
interface and makes a late bind fail loudly.

## The audit changed the shape of the work

The plan this came from said "design a frozen bind phase". **One already
exists.**
`StandardServer` has a seventeen-method `Create*` factory seam, and
`DependencyInjectionStandardServer` overrides seven of those to resolve
from the container:

```csharp
protected override IRoleManager CreateRoleManager(IServerInternal server, ApplicationConfiguration configuration)
    => m_services.GetService<IRoleManager>() ?? base.CreateRoleManager(server, configuration);
```

`StandardServer` then carries each factory result into the datastore:

```csharp
ServerInternal.SetRoleManager(CreateRoleManager(m_serverInternal, configuration));
```

So an integrator already had two supported routes — override the
virtual, or register in
DI. `Set*` on the interface was a **third route that bypassed the
ordering**, and the only
one that let a caller rebind a running server.

The job was therefore to stop publishing plumbing, not to build a phase.

## Measurement

Every call site of the twelve mutators plus `CreateServerObjectAsync`:

| Area | Count | Where |
|---|---:|---|
| `Opc.Ua.Server` | **11** | `StandardServer.cs:3729–3844`, one
contiguous block |
| samples | **1** | `ReferenceServer.cs:219`, inside a
`CreateMasterNodeManager` override |
| tests | 20 | `ServerInternalDataTests` (18), `SubscriptionTests` (2) —
**all on the concrete `ServerInternalData`** |

`SetIdentityRegistry` had **zero** call sites anywhere.

The earlier estimate for this work was 268 files. That number counted
files *referencing*
`IServerInternal`, which this change never touches: the interface
survives and only the
mutators move off it.

## Commits

### `e719a88a` Add the missing `CreateUserManagement` factory seam

The one gap, and the reason this had to land first. User management was
the only subsystem
with **no factory seam** — the only way to populate
`IServerInternal.UserManagement` was
`SetUserManagement`, which is what `ReferenceServer` called from inside
a
`CreateMasterNodeManager` override. Removing the mutator without a
replacement would have
broken that scenario with no migration path.

The DI path at `OpcUaServerBuilderExtensions.cs:1178` builds a
`UserNamePasswordAuthenticator` from a registered `IUserManagement` — a
different thing
entirely that never reaches the Part 18 §5 model on the address space.

Purely additive. `ReferenceServer` moves onto the override.

The test covers the **override** seam rather than the DI seam
deliberately: registering an
`IUserManagement` in the container also switches on the
username/password authenticator, so
a DI-based test exercises two unrelated paths and fails for reasons
unrelated to binding.

### `cbcfda47` Remove `SetIdentityRegistry`

Zero call sites, despite a doc comment advertising it as the integrator
seam. The supported
route has always been
`ServerIdentityRegistryExtensions.RegisterDefaultAuthenticators`,
which *adds* authenticators to the default registry rather than
replacing it.
`IdentityRegistry` is read five times outside `Opc.Ua.Server`, so the
default is what every
consumer already uses.

**The analyzer confirmed the reading**: with the setter gone, `RCS1170`
fired on the
property — it is provably never reassigned after construction, so it is
now read-only.

### `ea0937a2` Take the bind mutators off `IServerInternal`

The mutators stay `public` on `ServerInternalData`, which owns the
state. Only the interface
loses them. The eleven call sites switch from the `ServerInternal`
property to the
`m_serverInternal` field, already typed `ServerInternalData?` — the same
one-token change
`GetStatus` and `EnterState` took in #4193.

**No test changed.** All twenty test call sites already used the
concrete type, which is the
clearest evidence these were never interface-level API.

`CS1574` found six doc comments across `IRoleManager`, `RoleManager`,
`IUserManagement`,
`UserManagement`, `IServerInternal` and `SessionManager` that pointed
integrators at the
mutators. They now point at the factory seam.

### `f3c4b9d3` Refuse to bind after the server has started

Each bind goes through a guard; `StandardServer` closes the phase once
the session manager
is in place. A late `Set*` throws
`ServiceResultException(BadInvalidState)` **naming the
operation** and pointing at the factory seam — otherwise the failure is
indistinguishable
from a configuration problem.

Rebinding *during* startup stays legal, which is what let both audit
hazards resolve with no
behaviour change:

- **`SetNodeManager` also binds three more managers**
(`DiagnosticsNodeManager`,
`ConfigurationNodeManager`, `CoreNodeManager`). Intentional — they are
reached through the
master node manager and have no independent lifetime — and now
documented rather than
  silent.
- **`SetSessionManager` unhooks the previous manager's handlers**, which
reads as
written-for-repeat-calls. Nothing rebinds it, but the freeze does not
make the path dead
either, since binding twice during startup is still allowed. Left as-is;
removing it is a
  separate question.

## Tests

Four new tests on the freeze: the refusal itself across four
representative mutators, that
the message names the operation, that rebinding stays legal until the
phase closes, and that
closing twice is harmless. Plus one hosted-server test for the
`CreateUserManagement` seam.

| Suite | net10.0 | net48 |
|---|---|---|
| `Opc.Ua.Server.Tests` | 4033 passed, 1 failed | 230 passed, 1 failed
(filtered) |
| `Opc.Ua.Redundancy.Server.Tests` | 573 passed | — |
| `Opc.Ua.Redundancy.Kubernetes.Tests` | 97 passed | n/a (net8/net10
only) |

The single failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
local certificate-store problem verified to reproduce unchanged at HEAD
with these changes
stashed. Not caused by this PR.

## Docs

`docs/MigrationGuide.md` gains a table mapping every removed mutator to
the `Create*`
override and the DI registration that replace it, plus the
`SetIdentityRegistry` removal
with its rationale.

## Net effect across #4193 and this PR

`IServerInternal` goes from ~460 lines to **~200**. It no longer
publishes locks, dead
members, or the ability to rewire a running server.

## Reviewer questions

- `CompleteBindPhase()` is public on `ServerInternalData` but
deliberately **not** on
`IServerInternal` — only the server owning the startup sequence should
decide when binding
  is over. Reasonable, or should it be `internal`?
- `SetSessionManager`'s unhook path is now unreachable in practice but
still legal during
startup. Leave it, or remove it and assert single-bind for that one
subsystem?

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e9c13dd0-a6e3-4ad2-8ad6-1c3fe6323041
Copilot-Session: b3c0e146-5b9e-4d08-a0f6-db3f940484b1
Copilot-Session: d18f87e9-2484-44ce-98ce-463ba6cf5f63
2026-08-11 00:27:45 +02:00
537b10c8b3 Fix History User Access Levels and add CTT coverage nodes (#4026)
# Description

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

## Reference Server coverage

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

## Latest CTT findings

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

## Related Issues

- Follow-up to #3960 and #4002.

## Checklist

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@hotmail.com>
2026-07-21 13:52:12 +02:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00