5 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
Marc Schier 1bb4bdca1d Add Machine Vision Parts 1 and 2 (#4733)
# Description

Implement the released [OPC
40100-1](https://reference.opcfoundation.org/specs/OPC-40100-1) and [OPC
40100-2](https://reference.opcfoundation.org/specs/OPC-40100-2)
specifications with generated models, provider-backed runtimes, typed
clients, MCP workflows, and DI/Machinery integration.

- **Part 1 stays in `Opc.Ua.Vision`** and its client/server packages
alongside the distinct Perception model. Provide control/acquisition
state machines, jobs and synchronized steps,
recipe/product/configuration/result management, file transfers,
diagnostics, events, and conditions.
- **Part 2 is in `Opc.Ua.MachineVision`, `.Client`, and `.Server`.**
Provide asset categories, identification, image-handling aspects,
health/maintenance data, topology, and transactional updates. The base
Vision assemblies do not reference these optional packages; the Part 2
wire model does not depend on Part 1.
- Preserve the release-pinned model bytes, wire namespaces and NodeIds.
Perception types are in `Opc.Ua.Vision.Perception`; the AMCM generated
namespace remains `Opc.Ua.Vision.MachineVision.AMCM`, and asset
client/server extension namespaces remain `Opc.Ua.Vision.Client` and
`Opc.Ua.Vision.Server`.
- Use `AddOpcUaMachineVision` and `AddOpcUaMachineVisionAMCM` for model
loading and factory registration. Each has node-collection,
encodeable-factory-builder and node-state-factory-builder overloads.
These short entry points delegate to the generated implementations;
existing namespace-derived generated names remain available for
compatibility.
- Support multiple independent named systems/assets under one
namespace-owning manager. Reuse injectable providers, file binding,
bounded memory stores, optional transactional `IFileSystemProvider`
persistence, state machines, telemetry and conformance infrastructure.
- Keep one `docs/Vision.md` guide, titled `Machine Vision (OPC 40100-1
and OPC 40100-2)`, and one `Opc.Ua.Mcp.Vision` package. Include
deterministic server/client samples.
- Expose 17 Machine Vision MCP tools, including capabilities/status,
bounded relationships and observation, complete small-file
uploads/downloads, management, explicit control, conditions, and chunked
transfers. Catalogue counts: 39 Vision-specific, 43 with connection
tools, 81 Vision+Robotics, 155 Full. Every Machine Vision wire parameter
has a schema description.

**Compatibility and limits:** `AddVision()` remains Perception-only
unless released parts are selected. Standalone Part 2 uses
`AddVision().AddMachineVision()`; Machinery composition uses
`AddVisionToMachinery()`. Existing tool defaults and command signatures
are preserved. Observation owns and cleans up its subscription: 5
seconds/50 notifications by default, capped at 60 seconds/100
notifications. Complete-file tools are capped at 1 MiB; larger files use
chunked transfers. Domain Error values remain distinct from UA service
failures.

**Versioning:** the seven Vision/MachineVision/MCP packages and their
Debug variants follow the repository root version instead of being
forced to preview. Vision.OpenUsd and its descendants remain
preview-only. Development builds still inherit the root prerelease
version. No release version, tag, release branch, publication policy, or
feed promotion is changed.

**Review actions:** all fourteen review threads have action/evidence
replies and are resolved. Changes include the unified guide, Perception
namespace, separate Part 2 packages and final naming, multi-system
ownership explanation, interrupted-write cleanup, observer failure
isolation, complete MCP parameter descriptions, and explicit package
inventory. The preview-layout migration topic and the requested
ReleaseProcess caveat were removed. The test Publish collector now
retries recognized transient failures within one cancellation budget and
propagates other exceptions. Commit 69b1f5db9 applies the requested
short loader names and renames standalone hosting to `AddMachineVision`,
with callers, tests and documentation updated. The intervening master
merge is preserved.

### Verification

**Latest API naming changes, verified locally:**

| Scope | Result |
| --- | --- |
| Full Vision suite, net10 | 572 passed, 1 existing skip, 0 failed |
| Full Vision suite, net48 | 515 passed, 0 skipped, 0 failed |
| Final loader/hosting cases after cross-TFM null-guard adjustment | 9
passed, 0 failed, 0 skipped |
| Machine Vision MCP and profile cases | 198 passed, 0 failed, 0 skipped
|
| Main and MCP AOT test-project compilation | Both built with 0
warnings/errors |
| MCP AOT companion, managed execution | 5 passed, 0 failed, 0 skipped;
not claimed as native execution |

`ModelLoaderNamesRegisterTypedFactories`,
`ModelLoaderNamesRejectNullArguments`, and
`GeneratedModelsLoadTogetherAndCreateTypedInstances` cover all six
loader overloads, fluent return identities, typed instances, namespace
coexistence and null guards. Existing standalone Part 2 tests verify
idempotent registration, one-owner enforcement and null handling through
`AddMachineVision`. No source-generator implementation, CLR namespace,
wire identity or model payload was changed for these naming updates.

The current 69b1f5db9 revision is running
[CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826937),
[package
validation](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165823359),
[MCP
usability](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826874),
[CodeQL](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826906),
and [Images
CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826884).
These runs are not claimed as completed results.

**Earlier completed verification:** all seven default-TFM package
archives were inspected after the package rename: exact IDs, 27 clean
DLLs, correct dependencies/readmes/licenses, and unchanged pinned model
hashes. [CI on
c75ec4fa6](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38142519760)
passed, including the formerly failing Linux batch and required summary.
Its unchanged coverage gate passed at **88.56% lines, 78.96% branches,
84.28% patch** against the 75% patch floor. The later
bounded-Publish-helper regressions reproduced 13 failures before the fix
and passed afterward; the full mainline triggering fixture passed 67
tests on each of net10/net48. These are historical results, not current
full-matrix results.

<details>
<summary>Executed API naming validation commands and outcomes</summary>

Run from the worktree root using the established artifact trees.
Dependency builds were performed before the no-build full net10 run; the
focused loader/hosting selection was rebuilt and rerun after the final
null-guard adjustment.

```powershell
$files = 'C:\Users\mschier\.copilot\session-state\deb2ba10-b0e9-4a44-8783-8331bc850814\files'
$env:DOTNET_CLI_USE_MSBUILD_SERVER = '0'
$env:TEMP = 'C:\Temp\mv-deb2\followup'
$env:TMP = $env:TEMP

# Passed: 572; Failed: 0; Skipped: 1.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-build --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net10" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 9; Failed: 0; Skipped: 0, on the final source.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~MachineVisionModelTests|FullyQualifiedName~StandalonePartTwoRegistrationUsesOneOwnerAndOneModelProvider|FullyQualifiedName~StandalonePartTwoRequiresRegistrationAndRejectsCompetingOwnership' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-model" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 198; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~VisionMachineToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~McpToolProfileCompositionTests' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-mcp" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

$env:TEMP = 'C:\Temp\mv-deb2\followup48'
$env:TMP = $env:TEMP

# Passed: 515; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Debug -f net48 --no-restore --artifacts-path "$files\solution-net48-artifacts" -p:CustomTestTarget=net48 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net48-final" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup48'
```

Opt-in long-running/stress/durable tiers have not been rerun locally;
the unqualified **all tests** checklist item remains open. Original
implementation matrix counts are not presented as current results.

</details>

## Related Issues

- Fixes #4732.

The requester reviewed and approved the implementation plan and
subsequent packaging/API decisions in the development session. The
tracking issue records the design; maintainer architectural
discussion/sign-off there is still pending. This PR remains a draft.

## 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.
- [ ] 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.
2026-10-12 07:01:27 +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
Marc Schier 275c3800b5 [Server] Add live NodeManager lifecycle support (#4094)
# Description

Adds supported live add, reload, and remove lifecycle operations for
NodeManager instances on a running server.

The change introduces a DI-backed `INodeManagerLifecycle` service with
direct `StandardServer` access, generation-aware registration handles,
copy-on-write routing, transactional prepare/commit/rollback, runtime
NodeSet convenience APIs, append-only namespace updates, model and
semantic change notifications, additive-compatible complex type priming,
and complete teardown behavior.

Lifecycle publication is coordinated with requests, sessions,
subscriptions, monitored items, continuation points, and shutdown so
retired managers cannot remain reachable or be disposed while still in
use. Compatible monitored items transfer to replacement generations
without a transient bad status. Removed or incompatible Nodes publish
`BadNodeIdUnknown` as required by OPC UA Part 4 section 5.8.4.1, retain
their MonitoredItem identity, and automatically recover when a
compatible Node with the same NodeId returns.

Registration is documented end to end in the new
[docs/NodeManagerRegistration.md](docs/NodeManagerRegistration.md),
which covers compile-time, startup, and runtime registration together
with the guarantees for MonitoredItems, Browse continuation points,
namespace indexes, DataTypes, and change notifications.
## Review feedback addressed

Interface surface reduced, as requested:

- `INodeManagerMonitoredItemTracker` folded into
`ISubscriptionMonitoredItemLifecycle`.
- `INodeManagerMutationCoordinator` and the asynchronous MonitoredItem
recovery entry point folded into `IDynamicNodeManagerHost`.
- `ISessionClosingRegistry` folded into `IServerInternal`, then replaced
by `ISession.IsClosing`.
- `ISubscriptionDeletionRegistry` folded into `ISubscriptionManager`,
then removed entirely.
- `INodeManagerContinuationPointTracker` folded into `ISession`.
- The synchronous recovery entry point kept on its own
`ISyncNodeManagerMonitoredItemRecovery`, because `AddPredefinedNode`
cannot await; it still routes through the asynchronous
`INodeManagerMonitoredItemLifecycle` that `AsyncNodeManagerAdapter`
exposes.
- `IMonitoredItemLifecycle` renamed to `IDetachableMonitoredItem`.

Reach into the Subscription area reduced to the minimum the feature
needs:

- The lifecycle feature itself leaves `SubscriptionManager` almost
untouched: two guards that reject a request from a Session that is
closing, on `CreateSubscription` and on `TransferSubscriptions`, because
it would create state that is about to be torn down. The deletion claim
registry, the session closing gates, the closed-session bookkeeping and
the deletion restore paths introduced by earlier revisions are gone.
Transferring a subscription away from a session that is closing stays
allowed, since that is the recovery path the service exists for; the
race that exposed is fixed by serialising source-queue closure with the
transfer rather than by rejecting it.
- Queueing is disabled again for a MonitoredItem with `QueueSize <= 1`,
as before this PR. Without a queue the Client is served the last sampled
value, so the missing-Node notification simply becomes that value;
losing values is expected for a MonitoredItem without queuing.
- What remains of the notification is one `DataValue` plus one bool in
`DataChangeQueueHandler`, mirroring the existing overflow pair, which
stops it being the value discarded when a configured queue is full.
- `MonitoredItem` keeps its original publish path, its original queue
creation and restore paths, its original triggering behaviour, and no
per-epoch bookkeeping.
- `IStoredMonitoredItem` carries `IsDeleted` and `IsDetached`, so a
restored item reports the state it was stored with rather than one
inferred from the last value's StatusCode.

Other feedback:

- Documented `HostedNodeManagerLifecycle`, `IDynamicNodeManagerHost`,
`NodeManagerRoutingTable`, and the `RequestManager` validation and
request scopes.
- Renamed `ValidateMonitoredItemAsync` to `CanAttachMonitoredItemAsync`
and `RestoreMonitoredItemAsync` to `RecoverMonitoredItemAsync`, so the
lifecycle API no longer collides with the durable Subscription
`RestoreMonitoredItem` path used at server startup.
- Removed the `CustomNodeManager2` dependency from the synchronous
MonitoredItem recovery path.
- `DataChangeQueueHandler` is back to the two constructors 1.5.378
shipped, each taking an optional `TimeProvider`.
- Session closing is a one-way latch, and the Session is removed even
when teardown throws.
- Documented what a custom NodeManager has to implement to take part in
live lifecycle operations, and which NodeManager implements
`INodeManagerReloadParticipant` today.

## Fixes found during review

- The guard that stops a NodeManager lifecycle operation being started
from inside a service handler never fired. It was backed by an
`AsyncLocal` written inside `async ValidateRequestAsync`, which is
invisible to the caller that awaited it, so a lifecycle call from a
NodeManager callback waited for its own request and held the lifecycle
semaphore forever. The mark is now applied by the service dispatcher,
and the drain is bounded so no path can wedge the lifecycle permanently.
- `ValidateRequestAsync` is no longer virtual. An override that rejected
a request after calling `base` left it registered forever; subclasses
now use `OnRequestValidatedAsync`, whose failures complete the request.
- A MonitoredItem deleted and disposed while a reload was handing it to
the replacement NodeManager could be attached anyway. The item now
reserves itself for the hand-over and the transition takes it back if it
was disposed meanwhile.
- The Overflow bit is recorded and applied when the value is published,
as the surrounding design does, rather than written into the value at
enqueue time.
- MonitoredItem cleanup for a Subscription being deleted no longer
passes null entries to `DeleteMonitoredItemsAsync`.
- Browse continuation points of a retired NodeManager are persisted and
disposed outside the Session lock.

## Open review discussions

Three threads are open, replied to and awaiting reviewer decisions:
separating service-call dispatch from NodeManager handling in
`MasterNodeManager` (tracked as #4098), the request-scoping shape in
`StandardServer`, and how much of the async MonitoredItem service
implementation should stay in `Subscription`.

Issue #4102 records a specification ambiguity found while implementing
the missing-Node notification: Part 4 §5.8.4.1 makes it mandatory, but
the protected over-capacity queue entry §5.13.1.5 defines applies to
`EventQueueOverflowEventType` only, so the specification does not say
how it survives a full queue.

## Validation

- Full `Opc.Ua.Server.Tests` on net10.0: 3,768 passed, 5 skipped.
- Lifecycle, MonitoredItem, Subscription and continuation point suites:
185 passed on net10.0 and 75 on net48.
- Server builds complete with zero warnings or errors.
- Two failures are pre-existing and unrelated:
`DurableDataValueQueueVerifyReferenceBatchingAsync` (flaky
`BatchPersistor` file contention, passes in isolation on net10.0 and
net48) and
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`
(fails on a clean tree with these changes stashed).

## Related Issues

- Fixes #3993
- Follow-up tracked in #4098
2026-07-28 16:06:28 +02:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00