4 Commits
Author SHA1 Message Date
Marc Schier 9bfc780e14 Update NuGet dependencies and migrate tests to NUnit 5 (#4840)
# Description

Update 55 NuGet dependencies to the latest stable releases available on
2026-10-19, preserving documented compatibility pins and
framework-specific version bands. Existing OpenUSD prereleases remain
unchanged because no newer releases are available.

Migrate existing tests to NUnit 5: await asynchronous assertions,
propagate async through helpers, handle nullable predicates, and replace
obsolete `Order` attributes with sequencing dependencies. Sequencing
dependencies use `AllowFailure = true`; filtering a dependent test also
runs prerequisites. Adjust fixture-level parallelism where NUnit
dependency scheduling requires it.

Keep Roslyn API/runtime pins intact; select newer Immutable/Metadata
runtime packages only for test and profiling hosts requiring them.
Update the approved SDK pin to 10.0.402 across local, CI, and container
builds. Document NUnit 5 conventions.
2026-10-19 15:23:29 +00:00
romanettandClaude Opus 5.5 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

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

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00
romanettandClaude Opus 5 70161bc145 Fix Part 18 RoleSet: AddRole overwriting nodes, and Roles configured at startup never getting one (#4367)
# Description

Two defects in the Part 18 Role management implementation added by
#3735. They are separate root causes but both concern Roles that are not
one of the nine well known ones.

## 1. `AddRole` could allocate a NodeId that is already in use

`RoleManager.AllocateDynamicNodeId` counted up from `i=1` in whatever
namespace it was given, with no check that the identifier was free. The
namespace came from the `NamespaceUri` argument of the standard
`RoleSet.AddRole` Method, so a client holding `SecurityAdmin` could pass
the namespace URI of a node manager's own model and receive the first
identifier in it — which every model compiler and the source generator
have already used. `MaterializeDynamicRoleAsync` then created the Role
at that NodeId, replacing the model's node in the address space of a
running server.

The root cause is that `NamespaceUri` was treated as selecting the
namespace the Role's NodeId is allocated in. Per Part 18 §4.2.2 it
qualifies the **BrowseName**; the NodeId is server-assigned. The fix
works on three levels:

- `RoleManager.AddRole` resolves `NamespaceUri` into the BrowseName
namespace only — recorded as both an index and a URI on the new
`RoleEntry.NamespaceUri`, so the binding can re-resolve it against the
server namespace table — and always allocates the NodeId in
`defaultNamespaceIndex`, which `RoleStateBinding` picks from a namespace
the RoleSet's NodeManager owns. A client can no longer steer allocation
into another node manager's model at all. This also fixes a second-order
problem: a Role node materialized into a foreign namespace was indexed
in the diagnostics manager but routed elsewhere by the
`MasterNodeManager`, so it could never be resolved.
- `AllocateDynamicNodeId` skips identifiers that are already taken — by
another Role, or by a node in the address space, via a probe
`RoleStateBinding` installs. This is the fix suggested in the issue.
- Materialization refuses rather than replaces. An occupied NodeId
answers `Bad_NodeIdExists`, one outside the manager's namespaces answers
`Bad_NodeIdInvalid`, both logged; `AddRole` rolls the manager back so it
is all-or-nothing and never leaves a Role that grants access but has no
node.

## 2. A Role configured at startup was enforced but got no node in the
`RoleSet`

`RoleStateBinding.Initialize` discovered Roles by walking the address
space only, and `MaterializeDynamicRoleAsync` ran from `OnAddRoleAsync`
alone. `RoleConfigurationChangeKind.RoleAdded` for an unbound Role did
nothing. Every documented way of configuring a custom Role before the
server is up — overriding `StandardServer.CreateRoleManager`,
`IOpcUaServerBuilder.ConfigureRoles(...)`, `AddRoleManager<TManager>()`
with a pre-populated manager — therefore produced a Role that
`MasterNodeManager.ValidateRolePermissions` honours but that a client
cannot browse, read `Identities` from, or call `AddIdentity` on. Part 18
§4.2 expects the `RoleSet` to describe the Roles the server supports, so
this is a conformance gap as well as an ergonomic one.

- `InitializeAsync` now materializes every `IRoleManager.RoleIds` entry
the address-space walk did not return.
- `OnRoleConfigurationChanged` handles `RoleAdded` for Roles that are
not bound yet, so a Role added by a custom `IRoleManager` at runtime
also appears. The manager raises the event synchronously from inside
itself, so that work is queued rather than awaited in the handler.
- Both go through one idempotent, serialized
`EnsureRoleMaterializedAsync`, which is also where the NodeId checks
from defect 1 live.

## Third-order fix: `ConfigureRoles` allocated NodeIds against a
throwaway namespace table

`ApplyRoleConfiguration` ran inside the DI factory for `IRoleManager`,
where the server's namespace table does not exist yet. It built a local
`NamespaceTable` and defaulted to index 1, so a configured custom Role
was allocated in a namespace the RoleSet's NodeManager does not own —
which the refusal added above would then correctly reject, leaving the
Role invisible again and defect 2 unfixed for the `ConfigureRoles` path.

The options are now staged on the manager
(`RoleManager.PendingConfiguration`) and applied by the binding once the
address space is up, against the real namespace table. Settings on Roles
that already exist — the nine well-known ones — are still applied
eagerly in the DI factory, so resolving `IRoleManager` before start-up
behaves as it did.

## Notes for reviewers

- **`RoleStateBinding.Bind` becomes `BindAsync`.** Materializing needs
`AddPredefinedNodeAsync`. The only caller was `ServerInternalData`, in
an already-async method. This is a public API change on a type
introduced in `2.0.0-preview.2`.
- **`AddRole`'s returned NodeId namespace changes** for a call that
passes a non-empty `NamespaceUri`: it is now the server-chosen namespace
rather than the caller's. This is the fix, and the existing XML doc on
`IRoleManager.AddRole` already described this behaviour ("a fresh NodeId
is allocated in `defaultNamespaceIndex`") — the implementation was what
deviated. The BrowseName namespace is unchanged.
- **`AddRole` is now all-or-nothing.** Materialization failure used to
be logged and swallowed, returning `Good`. It now rolls the manager back
and returns the reason, per the issue's "refusing would at least turn
silent corruption into a diagnosable error".
- **`RoleEntry` gains `NamespaceUri`** and its internal constructor
gains a parameter.
- The unit fixture in `RoleStateBindingTests` now logs "not in a
namespace owned by the RoleSet's NodeManager" for eight well-known
Roles. That is a fixture artifact — its test node manager does not own
`ns=0`, unlike a real `DiagnosticsNodeManager` — not a real-server
condition.
- Unrelated but adjacent:
`AddRoleHandler_TypedRoleStateUpgradedByBinding_CanCallAddIdentity`
added a Role named `Engineer`, which `AddRole` rejects as a duplicate
browse name. Its `Assume` never passed and its body never ran, on master
too. Renamed to a non-well-known name so it exercises what it claims.

## Testing

- `RoleManagerTests`: NodeId allocated in the default namespace while
the BrowseName keeps the requested URI, unregistered URI rejected,
identifiers reported in use by the address space skipped, an existing
Role's NodeId never reused.
- `RoleStateBindingTests`: `AddRole` with a foreign model namespace
leaves the model's own node at `i=1` untouched; a stub `IRoleManager`
that hands back an occupied NodeId gets `Bad_NodeIdExists` and a
rollback; a Role configured before the address space existed is
materialized and referenced by the `RoleSet`; `ConfigureRoles` options
are applied at bind time; a Role added on the manager after bind is
materialized.
- `ServerFluentApiHostingTests`: end-to-end on a hosted server — a
custom Role from `ConfigureRoles` has a node under the `RoleSet`
carrying its configured identity mapping.
- `RoleManagementIntegrationTests`: end-to-end against the reference
server over an encrypted channel — `AddRole` with the model namespace
URI qualifies the BrowseName only and the Role is browsable.

Locally on .NET 10: `Opc.Ua.Server.Tests` 4801 passed,
`Opc.Ua.Features.Tests` 136 passed, `Opc.Ua.Core.Security.Tests` 556
passed, `UA.slnx` builds with no new warnings.

## Related Issues

- Fixes #4361

Found while building an OPC UA Part 18 sample for
OPCFoundation/UA-.NETStandard-Samples#818.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:28:43 +00:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00