7 Commits

Author SHA1 Message Date
romanett 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
Copilot e78c648295 Images CI: publish samples once natively and stop compiling under QEMU (#4679)
# Description

Images CI took 45–100 min per image. The arm64 half of every
`linux/amd64,linux/arm64` build ran `dotnet restore` and `dotnet
publish` in an arm64 SDK container under QEMU. For `refserver`, publish
took 8 min natively for amd64 and 92 min emulated for arm64. On top of
that, every one of the 10 matrix legs recompiled the whole stack.

The output is framework-dependent portable IL, so the compile is now
done once, natively. The image legs only copy files onto the
per-platform runtime base image.

### Dockerfiles (all 10)
- **SDK stage runs on the build platform.** The `build` stage is pinned
to `--platform=$BUILDPLATFORM`. `-p:UseAppHost=false` keeps an x64 app
host out of the arm64 image.
- **New `prebuilt` stage.** It is chosen with a global `ARG
PUBLISH_SOURCE`, which defaults to `build`. Local `docker build` and the
`docker-compose` files are unchanged.
  ```dockerfile
  FROM scratch AS prebuilt
  ARG PUBLISH_DIR=artifacts/docker/publish
  COPY ${PUBLISH_DIR}/ /app/publish/

  FROM ${PUBLISH_SOURCE} AS publish
  ```
- **Pump: final-stage `RUN`s moved to a native stage.** The `sed` on
`openssl.cnf` and the `mkdir`/`chown`/`chmod` steps now run in a
`$BUILDPLATFORM` `layout` stage. The final stage copies `/layout/` to
`/`, so `/app` and `/diag` keep the `$APP_UID:0` owner and `g=u` mode
they had before. `openssl.cnf` therefore comes from the build-platform
base image. It is a plain config file, so it is expected to match
between architectures.

### `docker-image.yml`
- **Single image list.** The image list lives once, in `env.IMAGES`
(image, project, Dockerfile). `discover` emits it as the matrix.
- **New `publish-samples` job.** It runs `dotnet publish` for each
sample sequentially, so shared libraries compile once. It uses the same
options and NBGV version properties as the Dockerfiles and uploads the
output as the `docker-publish` artifact.
- **`build-and-push-image` legs.** Each leg:
  - downloads the artifact;
- builds with `PUBLISH_SOURCE=prebuilt` and
`PUBLISH_DIR=artifacts/docker/publish/<image>`;
  - no longer uses `setup-qemu-action`;
  - is named `build-and-push-image (<image>)`.
- **`images summary`.** It now also fails when `publish-samples` fails.

### Docs
- `ContainerReferenceServer.md` describes the publish-once / copy-only
flow and the build arguments.
- `DeveloperGuide.md` mentions the `publish-samples` job in the Images
CI section.

Expected: one native publish of about 7 min, then 10 parallel
copy-and-push legs of a few minutes each. Not yet confirmed on CI: the
sandbox could not pull MCR base images, so the prebuilt multi-platform
builds were checked against a Debian-based stand-in base, and the
build-from-source path was not built.

## Related Issues

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-10-06 14:21:44 +02:00
romanett a0d569d756 Fix Server and GDS audit findings, review findings and spec-conformance gaps (#4571)
## Summary

Fixes the findings of a bug audit of `Opc.Ua.Server`,
`Opc.Ua.Gds.Server` and `Opc.Ua.Gds.Client`. Rebased on master after
#4516.

What went in:
- **Audit:** 93 findings, each checked against the OPC UA specification.
- **Code review of those fixes:** 35 further findings.
- **Spec grounding of every fix commit:** 18 conformance gaps, now
closed.

Commits are one per finding, named `Fix <ID>`, `Fix review <ID>` or `Fix
spec gap <name>`. The IDs refer to the audit report (S = Server areas,
G1 = GDS server, G2 = GDS client).

Validation:
- Each finding was traced in code and checked against OPC 10000 (parts
3, 4, 5, 9, 11, 12, 13, 18, 20).
- Every fix commit was checked against the spec text again afterwards: 0
non-conformant, remaining gaps fixed.
- Regression tests were added for most fixes.
- Suites run on net10.0 before the rebase: Server, GDS, History,
Configuration, Features, Core.Security, DI, Redundancy, LDS,
InformationModel, PubSub.Server, Client, Sessions and the three
Subscriptions suites, all green. The same suites on the rebased branch
are still running; results will be posted as a comment.

## Behaviour and user-facing changes

### Secure defaults
- **`Historize()` / `HistorianBuilder`** grant HistoryRead only. History
writes need an explicit `historyAccessLevel: HistoryRead |
HistoryWrite`.
- **File-system mount:**
- Anonymous sessions can no longer create, delete, move or open files
for writing (`Bad_UserAccessDenied`).
  - UserWritable and UserExecutable now reflect the user.
- `AllowAnonymousWrite` restores the old behaviour; the ReferenceServer
sample turns it on.
- **Default PKI root** for the hosted server, hosted GDS, hosted LDS and
`OpcUaApplicationConfigurationProvider`:
- Moved from the shared temp directory to `{LocalApplicationData}/OPC
Foundation/{ApplicationName}/pki`, and startup fails if no per-user
directory exists.
- When certificate stores of an earlier version still exist under temp,
a warning names both paths. Existing deployments that relied on the
default get a new application certificate (and the hosted GDS a new CA)
unless `PkiRoot` is set to the old path.
- **GDS:**
  - `GdsServerOptions.AutoApprove` defaults to `false`.
- The default KeyCredential store grants no roles unless
`AuthorizeRoles` allows them.
- Certificate requests (StartSigningRequest, StartNewKeyPairRequest,
FinishRequest) need an encrypted channel.
- **Hosted server anonymous logins:**
- Anonymous is rejected (`Bad_IdentityTokenRejected`) when the default
authenticator options disable it.
- A custom `AddIdentityAuthenticator<T>()` on its own no longer disables
anonymous.
- With anonymous disabled and no configured token policies, the endpoint
advertises the authenticators' token types instead of Anonymous.
- **Runtime-added roles:** their properties and methods need
SecurityAdmin over an encrypted channel, like the well-known roles.
- **Disabled users:** users disabled through Part 18 UserManagement can
no longer log in through `RegisterDefaultAuthenticators(IUserDatabase)`.

### GDS server (Part 12)
- **Certificate requests:**
- FinishRequest / ReadRequest with another application's RequestId
returns `Bad_InvalidArgument`.
  - Every Start*Request returns a new RequestId.
- A CSR without the ApplicationUri is rejected
(`Bad_CertificateUriInvalid`).
- An abstract or unsupported certificate type is rejected at Start
(`Bad_InvalidArgument`).
- **Private-key password:** no longer persisted in the JSON applications
database. A password saved by an earlier version is read into memory
once and dropped on the next save.
- **CheckRevocationStatus:** validates offline against the GDS's own
trust lists and CRLs. It returns Good for good GDS-issued certificates
and `Bad_CertificateInvalid` for input that is not a certificate.
- **Certificate-group trust lists:**
- Writes need CertificateAuthorityAdmin; SecurityAdmin and application
admins are read-only.
  - Writes are validated with the Part 4 certificate validation.
- **AuthorizationService:**
- The legacy RequestAccessToken applies `AccessControl` and issues the
token for the session identity.
- StartRequestToken passes the session identity to every identity-aware
provider.
  - Unknown ResourceIds return `Bad_NotFound`.
- Expired token requests and refresh tokens are pruned, with at most 10
000 pending requests (`Bad_TooManyOperations`).
- **KeyCredential:** requests with PublicKey + SecurityPolicyUri are
rejected with `Bad_SecurityPolicyRejected` (secret encryption is not
implemented); a PublicKey without SecurityPolicyUri is rejected with
`Bad_InvalidArgument`.
- **Audit events:** access-token audit events no longer contain the
password or token data.
- **RegisterTickets / UnregisterTickets:** need the RegistrarAdmin role
when called from a session.
- **Managed application nodes:** moved from namespace 0 to their own
namespace (`ManagedApplicationsNamespaceUri`); the NodeId is the
ApplicationUri, and the mandatory child properties exist.
- **Hosted GDS:**
- New `GdsServerOptions.CertificateGroups`
(`GdsCertificateGroupOptions`), bindable from configuration.
  - The mandatory DefaultApplicationGroup is always present.
  - The default CA subject uses the resolved application name.
- **JSON applications database:**
- A corrupt file throws instead of being silently replaced by an empty
database.
- Saves are atomic and write in place on bind mounts; on net8+ builds
they also follow symlinks and keep the Unix file mode.
  - Issued certificates and trust-list links survive a reload.

### GDS client
- **Trust-list download:**
  - Reads until an empty chunk (Part 20 short reads).
- A size-limit rejection of the final end-of-file read, as sent by
released servers, counts as end of file.
- **Default certificate group:** a null group means
DefaultApplicationGroup, and vendor groups in other namespaces are no
longer mistaken for well-known groups.
- **Status codes and errors:**
  - Missing directory proxies report `Bad_ServerNotConnected`.
- Short or wrongly typed method outputs throw `BadUnexpectedError` /
`BadTypeMismatch`.
- **GdsAccessTokenProvider:** no longer caches a failed client task, and
one caller's cancellation no longer fails other callers.

### Trust lists, push and ConfigurationFile (Part 12 / Part 20)
- **AddCertificate:**
  - `IsTrustedCertificate=false` returns `Bad_CertificateInvalid`.
- Added certificates are validated (Part 4), and a certificate revoked
by a current CRL returns `Bad_CertificateRevoked`.
- **CloseAndUpdate:** validates the uploaded list against its own
certificates and CRLs, not the stores it replaces. Validation failures
are no longer recorded in the server's Rejected store.
- **RemoveCertificate:** removing a trusted or issuer CA that other
certificates still need returns `Bad_CertificateChainIncomplete`.
- **Handles:**
  - Several read handles may be open at once.
- Any open while a write handle exists returns `Bad_NotReadable` /
`Bad_NotWritable`.
  - A write open while any handle is open returns `Bad_NotWritable`.
  - Handles of closed sessions are released.
  - Idle handles close after ActivityTimeout (default 60 s).
- **Status codes:**
- Read clamps to the remaining bytes and no longer enforces the size
limit.
  - Oversized Write / AddCertificate returns `Bad_RequestTooLarge`.
  - Unsupported open modes return `Bad_NotSupported`.
- **ConfigurationFile.CloseAndUpdate:**
  - Empty Targets return `Bad_InvalidArgument`.
- The ConfigurationUpdatedAuditEvent is raised with the configuration
owner as source.
- With the new `IApplicationConfigurationFileTargetProvider`, providers
receive the Targets, a bad target returns Uncertain with per-target
results, and session-interrupting updates are applied after
RestartDelayTime.
- **Default namespace permissions:** namespaces registered after the
first permission lookup now get their NamespaceMetadata default
RolePermissions and AccessRestrictions (no stale negative cache).

### Sessions, identity and roles (Part 18)
- **Session timeouts:** a failure closing one timed-out session no
longer stops the session-timeout monitor.
- **ChangePassword:** an empty old password returns
`Bad_IdentityTokenInvalid`.
- **Removing or disabling a user:** closes only that user's
UserName-token sessions.
- **Role configuration changes:** roles granted by an `ImpersonateUser`
mapping are kept.
- **X509Subject rules:** a certificate subject value containing a quote
never matches an X509Subject rule.
- **`ImpersonateUser` callback:** no longer runs under the global
session lock.
- **Session diagnostics:** LocaleIds are kept as the client sent them,
while null LocaleIds are ignored for translation.

### Subscriptions and monitored items (Part 4)
- **Acknowledgements:** a failing acknowledgement returns a per-entry
Bad status instead of failing the whole Publish.
- **`DiscardedMessageCount`:** counts messages dropped from the
retransmission queue and unsent overflow messages.
- **Durable subscriptions:** transferring a restored anonymous
subscription works (no NullReferenceException).
- **Transferred subscriptions:** sampling and permission checks use the
item's current owner session.
- **Access denied:** a monitored item whose user may no longer read the
attribute gets a `Bad_UserAccessDenied` notification instead of going
silent. Non-Value attributes need Browse permission (RolePermissions
needs ReadRolePermissions), and User* attributes are evaluated per
subscriber.
- **ModifyMonitoredItems:** a SamplingInterval of -1 uses the
subscription's publishing interval.
- **AggregateFilter priming:** needs HistoryRead / ReadHistory
permission and is bounded (100 000 values, bounded page size).
- **Resource leak:** event monitored items no longer leak a consumer
task per create/delete cycle.

### Node management services (Part 3 / Part 4)
- **Reference types:** AddNodes, AddReferences, DeleteReferences and
Browse reject ids that are not ReferenceTypes with
`Bad_ReferenceTypeIdInvalid`. AddNodes, AddReferences and
DeleteReferences also reject abstract ReferenceTypes; Browse still
accepts them (e.g. `HierarchicalReferences` with includeSubtypes).
- **Concurrent AddNodes:** duplicate BrowseNames under one parent get
`Bad_BrowseNameDuplicated`.
- **TranslateBrowsePathsToNodeIds:**
- Requires Browse permission on intermediate nodes and final targets
(RolePermissions and AccessRestrictions).
  - A long RelativePath no longer rejects the whole request.
- **SemanticChangeEvent:**
- Raised once per actual change of a semantic property, whether or not
it is monitored.
  - Also raised for properties with the SemanticChange access-level bit.
  - Reports the owning node's TypeDefinition as AffectedType.
- **Localized diagnostics:** operation-level text is translated when
OperationLocalizedText is requested.

### History and aggregates (Part 11 / Part 13)
- **DeleteRawModified:** an unspecified or reversed range returns
`Bad_InvalidTimestampArgument`, and StartTime == EndTime deletes the
value at StartTime for every provider.
- **ReadAtTime fallback:** reads only the values around each requested
time. A time whose bound is not found within the search limit returns
`Bad_BoundNotSupported` instead of failing the request.
- **ReadProcessed:** a ProcessingInterval shorter than one tick returns
`Bad_AggregateInvalidInputs`.
- **Delta / DeltaBounds:** widen to the next larger signed type when the
result does not fit the source type (e.g. a decreasing unsigned counter,
or an Int16 overflow).
- **Range / Range2:** return the source data type.

### Alarms and fluent API (Part 9)
- **Suppression:** an alarm stays suppressed while any suppression group
is active, and re-evaluation without a state change does not move
TransitionTime.
- **`MonitorVariable(variable)`:** sets SourceNode, SourceName and
InputNode to the variable and makes it a ConditionSource (HasCondition
to the alarm, HasEventSource from the object).
- **Fluent input/output arguments:** InputArguments / OutputArguments
added through the fluent API to a registered method are readable.
- **Root notifiers:** a failed root-notifier registration no longer
leaves an orphaned event worker.
- **State-machine handlers:** exceptions are logged at Error.

### File system (Part 20)
- **Delete / MoveOrCopy:**
  - Only act on direct children of the directory.
- A NodeId whose kind does not match the entry returns `Bad_NotFound` (a
file NodeId can no longer delete a directory).
- Copying a directory into its own subtree returns
`Bad_InvalidArgument`.
- **Locking:**
- An open file, or a directory containing one, cannot be deleted or
moved (`Bad_InvalidState`).
  - The check is atomic with opens.
- While a Delete or Move runs, read opens get `Bad_NotReadable` and
write opens `Bad_InvalidState`.
- **Open:**
- Reserved mode bits or EraseExisting without Write return
`Bad_InvalidArgument`.
  - Read+Write returns `Bad_NotSupported`.
  - Read while a writer is open returns `Bad_NotReadable`.
- **Client `FileSystemErrors`:** maps `Bad_NotReadable` to
`IOException`.
- **RuntimeNodeSet:** well-formed NodeSets with a stray `Model` element,
or two models in one file where one requires the other, now load.

### Server core, hosting and lifecycle
- **`SessionEstablishmentQueueLimit` > 0:**
CreateSession/ActivateSession now queue for a permit. The permit is
acquired before the request is registered, and a waiting handshake
releases its worker through the request-parking policy.
- **Node manager lifecycle:**
  - Add, reload and remove no longer wait for parked Publish requests.
- A Migrate reload racing with CreateMonitoredItems no longer blocks
later lifecycle operations.
  - Late items on retired or removed managers get `Bad_NodeIdUnknown`.
- **HistoryUpdate:** a batch mixing data and event details is bounded by
both limits.
- **Server stop/start:** stopping the server disposes the configuration
watcher and certificate subscription; starting recreates them.
- **Monitored-item queue factory:** a factory supplied by the caller or
the DI container is no longer disposed by the server.
- **Audit events:** each certificate validation error raises its own
AuditCertificate event. A failed OpenSecureChannel without a service
result is audited as failed.

## New public API (additive)
- `IQueuedSessionEstablishmentLimiter`
- `StandardServer.OwnsMonitoredItemQueueFactory` (protected virtual)
- `DefaultPkiRoot.Get` (Opc.Ua.Configuration)
- `TrustList.SetCertificateValidation`
- `NamespaceMetadataRegistry.Invalidate`
- `IApplicationConfigurationFileTargetProvider` and
`ApplicationConfigurationUpdatePlan`
- `GdsServerOptions.CertificateGroups` / `GdsCertificateGroupOptions`
- `InMemoryKeyCredentialRequestStore.AuthorizeRoles`
-
`DefaultManagedApplicationsNodeManager.ManagedApplicationsNamespaceUri`
- `AuthorizationServiceManager.RequestAccessTokenAsync(...,
IUserIdentity?, ...)`
- `AllowAnonymousWrite` on the file-system node manager and its factory
- `IFileSystemHost.TryBeginMutation` / `EndMutation`, replacing
`HasOpenHandles`

## Not fixed / known limitations
- **KeyCredential:** secret encryption with the client's PublicKey
(requests needing it are rejected).
- **CSR SAN restriction:** restricting subject / SAN values copied from
a CSR (CA policy).
- **Ticket signatures:** ticket signature verification (needs a
trusted-signer configuration).
- **FileType Read+Write open:** needs a read/write stream contract in
`IFileSystemProvider`.
- **Custom `IUserDatabase` implementations:** the disabled-user check
still scans all users per login. The built-in stores do a single lookup.
- **64-bit Delta precision:** limited by the calculator's double
arithmetic.

## Tests with changed expectations
Existing tests were updated where they asserted the old behaviour:
- File-system status codes
- Trust-list handle eviction
- `DiscardedMessageCount`
- Localized diagnostics bits
- Delta on unsigned sources
- SourceNode of fluent alarms
- ReferenceServer transfer permission test (now expects
`Bad_UserAccessDenied` notifications)
- `UserDeactivationShutdownReviewTests` (the revoked session is a
UserName login)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:26:05 +02:00
romanett a05a360550 Adopt the fluent builder's node-creation surface in the remaining hand-rolled node managers (#4460)
Closes #4429.

PR #4428 added the node-creation surface to `INodeManagerBuilder`. This
picks up the remaining candidates that needed a base-class change first,
and reports on the two that turn out not to be candidates at all.

## Converted

**`samples/WotCon/FlatTagServer/FlatTagNodeManager.cs`** — now derives
from `FluentNodeManagerBase`. The private `CreateObject` /
`CreateVariable` / `CreateMethod` helpers are gone, along with every
`pumpNodeId + ".Operational.Measurements.MassFlow"` concatenation; nodes
are staged with `AddObject` / `AddVariable<T>` / `AddMethod` and the
hand-maintained `externalReferences[ObjectsFolder]` entry is replaced by
`CompleteConfigureAsync`.

The `EventNotifier` bits and `AddRootNotifierAsync(pump)` are kept
verbatim — the pump is the notifier an aggregating server subscribes to,
and OPC 10000-3 only delivers the supervision conditions to a client
that can reach one.

`SupervisionSignal` loses its `namespaceIndex` and `tagPath` parameters
and derives both from the supervision Object it hangs off, so the signal
follows wherever that Object was staged.

**`samples/Redundancy/RedundantServer/HaSampleNodeManager.cs`** — same
shape. The `Counter` still reaches `EnableDistributedValueParticipation`
through `INodeBuilder.Node`. The variables keep their `Organizes`
reference type, which the fluent default would otherwise have made
`HasComponent`.

**`samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs`** —
both `AddPredefinedNodeAsync` sites stage on the builder now, threaded
through `ConfigureAsync`; `DiNodeManager.CreateAddressSpaceAsync`
already calls `RegisterAuthoredNodesAsync` between `ConfigureAsync` and
the reverse-reference pass, so no new plumbing was needed. Both methods
became synchronous. The NodeIds are unchanged because the hand
`SystemContext.NodeIdFactory.New(...)` calls and
`AssignInstanceChildNodeIds` were already doing exactly what staging
does.

`CreateRepresentedComponent` is left alone: it sits on
`CreatePumpAsync`, a public runtime API, and so is out of scope for the
same reason as the DI device builders.

## NodeIds are part of two samples' contracts

The issue expected the staged path to mint through the manager's
`INodeIdFactory`, and it does — but for two of these samples the
identifiers are a published contract, not an implementation detail:

- the aggregating client's checked-in Thing Descriptions under
`samples/WotCon/AggregationClient/Documents/` spell out
`nsu=…SourceA;s=Pump1.Identification.Manufacturer`;
- `samples/Redundancy/RedundantClient/Program.cs` addresses `new
NodeId("Counter", ns)` directly.

Both managers therefore override `New` for those nodes instead of taking
the default factory's derived identifier, which satisfies the acceptance
criterion that each manager keeps its current address-space shape.
FlatTag mints the full dotted browse path; the HA sample mints the bare
browse name, but only for the sample folder and its direct children.

Two things are worth knowing for anyone writing such an override.

**It has to keep an identifier the caller already chose.**
`DefaultNodeIdFactory.New` returns `node.NodeId` unchanged when it is
non-null and in the manager's own namespace. Without that same guard,
`NodeState.Create(context, nodeId, …, assignNodeIds: true)` sets the
explicit id and then `AssignNodeIds` overwrites it from the factory —
which silently moved `…SupervisionProcessFluid.Cavitation.Alarm` to
`…SupervisionProcessFluid.CavitationAlarm` and broke five alarm tests
until the guard was added.

**It has to be narrow.** A browse name is unique only among its
siblings. The HA override originally named every node the manager
minted, and the historian hangs an `HA Configuration` object off each
node it historizes — so the Counter's and the HistoryEvents' one both
landed on `ns=2;s=HA Configuration` and the strong active/passive
replicas refused to start. It is now limited to the nodes the redundant
client actually spells out.

## Merge with master

Merged `origin/master` (`0e600c0f0`). The one conflict was in
`HaSampleNodeManager.CreateAddressSpaceAsync`, where #4445 added
`AddFactoryAssignedNodes` next to the `HistoryEvents` object this branch
had converted. Both are kept, and the factory-assigned subtree stays
hand-built and unstaged on purpose: it exists to show what an identity
minted straight through `NodeIdFactory` looks like beside the sample's
named ones. It is attached once the folder has been staged, so those
identifiers survive untouched and the subtree is still registered as
part of the folder's. Its parent's identifier is `s=HighAvailability` on
both sides, so the ids it mints are unchanged from master.

## Not candidates

**`src/Opc.Ua.PubSub.Server/PubSubNodeManager.cs`** is out for the
issue's own DI-device-builder reasoning. All three of its build paths
are reachable from runtime Method calls — `OnAddDataSetFolder` and
`OnRemoveDataSetFolder` → `RebuildConfigurationAddressSpaceAsync`,
`OnAddPushTarget` / `OnRemovePushTarget` →
`RebuildKeyPushTargetAddressSpaceAsync`, `OnGetSecurityGroup` /
`OnAddSecurityGroup` / `OnRemoveSecurityGroup` →
`RebuildSecurityGroupAddressSpaceAsync` — plus `ConfigurationChanged`.
Each removes its previous roots and rebuilds them, and `Add*` throws
`BadInvalidState` once `RegisterAuthoredNodesAsync` has run.
Independently of that, its parents (`PublishSubscribe`,
`PublishedDataSets`, `SecurityGroups`, `KeyPushTargets`) are owned by
the diagnostics node manager, so `AttachToParent` would reject them as
`BadNodeIdUnknown`.


**`src/Opc.Ua.OpenUsd.Server/Scene/UsdSceneMaterializer.Properties.cs`**
— the issue asked whether a builder is reachable at those call sites. It
is not. `MaterializeUsdStage` is a public, documented `ISystemContext`
extension (`docs/OpenUsd.md`, the package README) whose only input is
the context; it returns a detached subtree for the caller to register.
It already mints through `context.RequireNodeIdFactory()`, which is the
same factory the staged path uses, so there is nothing to gain and a
public signature to break.

## Verification

Run against the merged tree:

- `Opc.Ua.Di.Tests` — 425/425 pass (covers `PumpOpenUsdE2eTests`,
`PumpHostedReferenceTests`).
- `Opc.Ua.Redundancy.Samples.Tests` — 14/14 pass, including both
variants of master's new
`FactoryAssignedNodeIdsSurviveActiveReplicaFailureAsync` and
`StrongHistorianContinuationsSurviveActiveReplicaFailureAsync`.
- `Opc.Ua.WotCon.Samples.Tests` — 25 pass, 4 fail. The same 4 fail on a
clean baseline worktree
(`CompanionTypeDefinitionsMatchNativePumpServerAsync`,
`WotPumpInstanceMatchesNativePumpSubsetAsync`,
`RealSamplesAggregateSubscribeAndReplaceGenerationAsync`,
`RealSamplesRouteManagementAndConditionActionsToEachSourceAsync` — all
`BadConfigurationError` out of the aggregation client). Pre-existing,
unrelated to this change.
- All three sample projects build with 0 warnings and 0 errors.

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 12:11:55 +00:00
Marc Schier 4633aa7a34 Harden HA address-space hydration and replica-consistent NodeIds (#4445)
# Description

Follow-up to #4390 for distributed address-space ownership,
authoritative
hydration, and replica-consistent wire NodeIds. Tracks #4444.

Identity implementation: `a78d59067` (Server policy/lifecycle/adapter
seam) and
`07faf5cfd` (replica identity module, runtime wiring, examples and
proofs).

## Replica identity

- `UseReplicaNodeIdentity` / `ReplicaNodeIdFactory` reserve fixed shared
namespace
indexes before node-manager construction and reuse
`DefaultNodeIdFactory`.
  ApplicationUri and built-in diagnostics/configuration remain local.
- Replicated startup requires an explicit identity contract. Namespace
layout,
  deterministic mode, replica-set identity and writer policy must agree.
Standard factory views, replacement and registration preserve the
policy.
- Active/passive writer-assigned identities are replicated unchanged.
Independent
creation requires stable keys or explicit IDs. Live IDs and retained
tombstones
are reserved before writer allocation; no network/storage call runs in
`New`.
- Protected store descriptors use strong routing, including customized
hybrid
prefixes. Unknown legacy state and empty eventual scans cannot authorize
  automatic adoption. Verified-new hybrid provisioning is explicit.
- Active/active frames compare the descriptor before CRDT merge.
Rejected frames
are not reported as applied; incompatible admission remains at
ServiceLevel 0
  through the standard service-level wiring.
- Shared metadata validation covers type definitions, data types,
references,
permissions, NodeId/ExpandedNodeId/QualifiedName values,
arrays/matrices,
  DataValues and encodeable structures/arguments, without reflection.
- Runtime preparation hydrates retained IDs before replacement routes
publish.
Rebinding keeps the existing store/election or gossip transport/map and
attaches
to the new node-manager sources. Normal node-manager mutations,
including
  AddNodes, reach the cached local-address-space adapter.
- WoT materialization retains the stable configured factory. Samples
expose
genuinely factory-created nodes and a client `--identity` failover
workflow.

## Preserved HA repairs

The published `d99995003` and `79da74f14` fixes remain in place:

- Shared linearizable sequence/lease coordination; no pretend CAS on
CRDT and no
  private counter substituted for shared coordination.
- Strict authoritative record decoding, protected pending reservations,
safe
  snapshot publication and required delta retention.
- Accepted topology state controls authoritative membership; stale
deletion
  cannot remove a newer incarnation.
- Independent subtree attachment tracking and correct disposal of the
final
  snapshot task.
- Values wait for their topology, and parent tombstone horizons prevent
older
  descendant values crossing incarnations.

A new deterministic regression also covers writer hydration preparation:
temporary reconciliation removals must remain inbound application, not
be
recaptured as new shared deletion records.

## Validation

### Review feedback fixes

`3a682155b` addresses the active-node notification, WoT namespace-index
bounds,
and retired-manager XML documentation feedback. The address-space
adapter now
notifies listeners with the registered active node, and both WoT
managers reject
missing/out-of-range namespace indexes before ownership is established.

Targeted feedback validation passes on net10.0 and net48: 8
address-space adapter
cases and 74 WoT cases per framework, plus 17
hydration/gossip/live-client
integration cases on net10.0. The wrong-instance notification and
missing
namespace regressions were reproduced before the fixes.

### Identity implementation evidence

| Selected scope | net10.0 | net48 |
| --- | --- | --- |
| HA repair, identity, metadata, peer admission and real-client
identity/reload fixtures | 215 passed | 215 passed |
| Factory, fluent authoring/import, NodeManagement and address-space
adapter fixtures | 213 passed | 213 passed |
| WoT node-manager fixtures | 68 passed | 68 passed |

- Real UA-TCP clients compare full NodeIds/reference targets across
independent
servers in all four deterministic modes, with different manager/creation
order
and local allocations. Saved IDs work after endpoint failover and
restart.
- Real AddNodes-created IDs remain readable through successive live
manager
replacements in both replication modes, including nodes created after a
reload.
- Two process-level cases start three sample replicas, kill the serving
process,
verify a different serving ServerArray URI, and reuse cached
factory-generated
  IDs for Read and CreateMonitoredItems without remapping or rebrowsing.
- The identity/hydration scenario runs from the actual published win-x64
NativeAOT
  executable in the existing historian companion host.
- The focused writer-recapture regression was observed failing without
its fix,
  then passing. The real reload scenario was additionally repeated while
  diagnosing that race.
- Changed-file formatting/style/analyzers were applied. Existing CA1508,
CA1823,
CA1861 and CA1850 warnings in untouched upstream files were not
suppressed.

The selected checks above do not replace full-solution CI, coverage
gates or
maintainer review, and do not claim that the entire solution or every
native
platform was run. #4444 remains the tracking issue.

## Contract and consistency limits

Fixed identity does not make eventual payload reads linearizable. Hybrid
views
remain non-destructive, use explicit tombstones, and do not infer
empty-store
bootstrap or partition completion from absence. Compacted snapshots
require
authoritative storage. Failed/uncertain reservations require
reconciliation.

There is no automatic namespace-slot allocation, persistent logical-key
mapping
database, live renumbering, legacy-store wipe/adoption, or new hash
algorithm.
Opaque shared structures require a registered codec so their
namespace-bearing
fields can be validated. Application callbacks are still
application-owned.

## Related issues

- Tracks #4444.
- Follow-up to #4390.
- Builds on the merged NodeId factory (#4433), configure/seal (#4442)
and
  lifecycle/import (#4427) integration at `aedd3f738`.

## Checklist

- [ ] I have signed the CLA and read CONTRIBUTING.
- [x] Added targeted regression and integration tests.
- [x] Updated directly related API, HA, migration and sample
documentation.
- [x] Ran scoped formatting and the targeted framework checks listed
above.
- [ ] Full UA.slnx matrix and coverage gates completed.
- [ ] All CI and maintainer review completed.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
2026-09-09 13:26:45 +02:00
Marc Schier 7411654c8d Complete OPC UA Part 11 Historical Access (#4390)
# Description

Completes the OPC UA Part 11 Historical Access work requested by #4387
and
the historian framework integration requested by #4400.

The conformance/evidence catalog covers all 37 released UACore 1.05
Historical Access profiles: 15 Server facets and 22 Client facets,
together
with the standard aggregate functions and their conformance units.
Server
facets are advertised only when the resolved providers expose their
required
interfaces and capabilities.

## Server and provider support

- Completes raw, modified, at-time, processed, annotation, structured,
and event history.
- Dispatches best-effort, bulk, and transactional updates with aligned
statuses, old values, diagnostics, and rollback state.
- Supports mixed history-update detail types and batched
structured/annotation operations.
- Uses composite identities and exclusive cursors for same-timestamp
structured, modified, and event values.
- Captures live data and events, installs historical configuration
objects, and emits typed audit payloads.
- Derives aggregate monitoring filters from per-node historian
capabilities and rejects unsupported processed aggregates before
provider dispatch.
- Supports tri-state `Historizing`: set, clear, or preserve
provider-owned state.

## Client support

- Exposes modified values together with `ModificationInfo`.
- Adds event-history reads and event insert/replace/update/delete
operations.
- Adds structured and annotation batching with strict response
validation.
- Shares continuation handling and release-on-abandon across read
families.
- Accepts inherited standard event fields rooted at event subtypes.

## Review hardening

- Registers DI-selected historians before node-manager reconciliation
and capability publication, while preserving provider ownership.
- Uses committed Raft read barriers for linearizable `TryGetAsync` and
`ScanAsync`.
- Keeps claimed continuation state immutable until a fresh successor is
saved, restoring the original after transient failures.
- Uses exact-incarnation compare-and-delete so delayed cleanup cannot
remove a restored same-ID continuation.
- Cleans up indeterminate initial and replacement saves, and propagates
shutdown cancellation through cleanup resolution.
- Preserves legacy continuation envelopes/cursors and distinct
annotation parent/property identities.
- Deletes every structured entry at a requested timestamp and uses the
complete modified-history ordering tuple.
- Shares native/fallback at-time and AnnotationCount calculations,
including reverse endpoints and cross-framework fractional intervals.
- Primes modified aggregate items before buffered live delivery and
deduplicates history/live overlap using provider-specific identities.
- Keeps committed Modify results successful while reporting priming
failures
  through the monitored item's notification queue.
- Keeps protected-notification state live-only for queues larger than
one.
Queue size one retains the newest notification without pinning errors or
transferring priority during resizing. Durable restore retains ordinary
definitions, last values/errors, and raw queues without recreating
priority.
- Isolates aggregate calculation, modification preparation, and
historical/live
handoff in `AggregationFilterHandler`, retaining existing
provider/factory
wiring and supported past-start behavior. The larger-queue protection
policy
  is unchanged pending the open review discussion.
- Uses one `IMonitoringFilter` owner for protocol filters or the
aggregation
handler, eliminating parallel effective-filter storage and retaining the
current definition during preparation. The original client filter stays
distinct.
- Integrates current master, retaining custom monitored-item creation
decisions and per-node history callbacks.

## Redundant-server alignment and samples

- Adds a protected shared historian with immutable segments, manifest
CAS publication, writer fencing, and restart recovery.
- Persists portable continuation state before exposing it to clients and
transfers ownership during mirrored-session takeover.
- Rejects eventual-consistency and active/active multi-writer historian
configurations rather than silently degrading.
- ReferenceServer and ConsoleReferenceClient demonstrate discovery,
paging, raw/modified/at-time/processed reads, annotation/structured
CRUD, event capture, and event CRUD.
- RedundantServer and RedundantClient demonstrate active/passive
history, continuation recovery after replica termination, and
post-promotion writes.
- Updates Historical Access, High Availability, profile, migration, and
sample documentation.

## Compatibility

- New 2.0 collection boundaries use `ArrayOf<T>` and public byte
payloads use `ByteString`.
- Removes the legacy public virtual
`SamplingGroupManager.CreateMonitoredItem` /
`ModifyMonitoredItem` entry points and protected creation factory, as
agreed
  in review. Migration guidance points to the monitored-item manager and
  construction-decision APIs.
- Retains established persisted formats and current write layouts.
Retired interim
notification-state formats (sample v2 / shared v4) are rejected, as
agreed in review.
- Keeps the historian integration compatible with .NET Framework and
NativeAOT.

## Validation

### Latest CI source repair

`73c619435` forwards the existing cancellation token at both
predefined-node
creation calls in the PR-added History event fixture. The merged
lifecycle
overload exposed these two CA2016 errors, which blocked History jobs,
the
solution build matrix, and CodeQL on GitHub and Azure. The exact
compiler
failure was reproduced locally; the repaired fixture passes 4/4
integration
cases on net10.0 and 4/4 on net48 with clean scoped diagnostics. No
assertions,
tests, coverage thresholds, or CI configuration were weakened.

The repaired History jobs on Linux/macOS and all four Azure solution
builds
now pass. The Windows all-TFM GitHub job instead lost its hosted
runner's
connection; no build log was recoverable, and GitHub recorded a
runner-loss
annotation rather than a compiler/test diagnostic. One unchanged retry
was
requested. Remaining checks are monitored with
`gh pr checks --watch --fail-fast --interval 30`; CI is not yet reported
as green.

### Latest base integration

`bd52e5fea` merges upstream `master` at `da7412c42`. The lifecycle
conflict
retains upstream's shared cancellation-aware registration helper and the
historian behavior. The hosted-historian fixture forwards its existing
token
to the new lifecycle overload. GDS project renames and all three AOT
test
hosts are retained, with their project references resolving after the
merge.

| Integrated scope | net10.0 | net48 |
| --- | --- | --- |
| Server lifecycle, fluent authoring/import, historian, monitored-item
and hosting regressions | 732 passed | 732 passed |
| NodeState lifecycle regressions | 216 passed | 216 passed |
| Automatically merged Raft client fixtures | 28 passed | 28 passed |

No selected tests failed or were skipped. The manually resolved files
pass
scoped formatting/analyzer checks. One CA1861 warning remains in the
incoming
`NodeSetImportIntegrationTests.cs`; that file is byte-identical to
upstream
master and was not rewritten or suppressed during integration.

### Latest feedback follow-up

`102954eaa` replaces split filter references with the shared
`IMonitoringFilter`
contract implemented by protocol filters and `AggregationFilterHandler`.
Targeted server regressions pass **103/103 on net10.0 and 103/103 on
net48**;
scoped formatting/analyzers are clean, and the updated XML-documentation
inventory has zero gaps across 188 non-generated PR C# files.

`93c5b5db8` renames `AggregationFilterHandler`, makes it the owner of
the
server-revised aggregate filter while preserving the original client
filter,
and removes the retired interim notification-state readers. Server
regressions
pass **101/101** and shared-store regressions **64/64**, on both net10.0
and
net48, with no failures or skips. Runtime-file formatting/analyzers are
clean.

`5ccf45630` completes the public/internal XML-documentation sweep. The
Roslyn
inventory finds zero undocumented eligible declarations across all 186
non-generated PR C# files; the 110-file documentation slice changes only
XML
comments and spacing. `f17765475` applies the three requested Markdown
edits.

### Earlier feedback follow-ups

`a53950737` removes single-slot notification pinning and extracts
aggregate
implementation from MonitoredItem. Targeted sampling, aggregate
modification,
history/live handoff, queue/lifecycle, restore, and serialization cases
pass
**101/101 on net10.0 and 101/101 on net48**, without failures or skips.
Scoped formatting/analyzers are clean; the focused review found no
significant
issues. The final formatted monitored-item fixtures also pass 45/45 on
net48.

The preceding `b21555568` removes the legacy sampling API bridge and
durable
notification-priority state. Its targeted Server cases passed 97/97 and
shared
subscription-store cases passed 64/64 on each framework. The broader
evidence
below remains pinned to its original tested commit, not attributed to
these
follow-ups.

### Earlier integrated-implementation validation

Validated implementation commit: **`3efae597f`**. The following commit,
`f6dbefb1c`, only adds two explanatory comment lines for the final
standards
review; it changes no executable code.

| Scope | net10.0 | net48 |
| --- | --- | --- |
| Server | 5,187 passed; 56 skipped | Affected surface: 2,160 passed; 1
reproduced baseline failure; 56 skipped |
| Client | 2,202 passed; 2 skipped | 2,204 passed; 5 skipped |
| History integration | 519 passed; 21 skipped | 519 passed; 21 skipped
|
| Redundancy server | 641 passed | 641 passed |
| Redundancy client | 124 passed | 124 passed |
| Core shared-store regression suite | 23 passed | 23 passed |

All six affected samples passed non-incremental Release/net10.0 builds
with
zero warnings and errors: ConsoleReferenceClient,
ConsoleReferenceServer,
RedundantServer, RedundantClient, MinimalBoilerServer, and
PumpDeviceIntegrationServer.

The published self-contained win-x64 NativeAOT executable is 116,924,928
bytes and has no CLR header or managed runtime dependency. Direct
execution
passed **HistoryAotTests 3/3** and **RaftAotTests 2/2**.

Final standards and specification reviews found no confirmed outstanding
high/medium functional gaps or hard standards violations. The remaining
constructor-purpose clarification is addressed in `f6dbefb1c`.

### Reproduced baseline exceptions

The net48 hosted-startup timeout also reproduces in a pristine worktree
at
the previous PR head, `24cf1416d`, in

`RegisterPostStartRegistriesWiresHistorianAndAliasStoresOnPlainServerAsync`,
with the same subsequent certificate-leak assertion. The corresponding
renamed case fails before startup in the reviewed branch as well. This
is
reported separately from the net48 affected-surface results, not counted
as
a passing full Server suite. The net48 matrix selected the historian,
aggregate, monitored-item, fluent, and continuation surfaces and
excluded
the Hosting namespace.

`DurableDataValueQueueVerifyReferenceBatchingAsync` failed at item 1000
in
the net48 affected-surface run and passed its single isolated rerun. The
same
failure at the same assertion reproduces on pristine `849e16acc`; the
test,
sample queue, queue factory, and batch persistor are unchanged. The
original
failure remains in the table rather than being erased by the rerun.

### CI source repairs

- `4be7eb8f7` keeps diagnostics/configuration managers out of
distributed
address-space replication. Those managers inherit the source interface,
but their live server state and redundancy metadata must remain
replica-local.
  The ownership regression passes on net10.0 and net48, and all nine
  redundancy sample CI tests pass locally.
- `d17f467b3` completes the sample startup repair: core managers are
also
replica-local; the startup banner no longer advertises readiness before
initialization; the producer starts after distributed services; and
native
Raft startup no longer forces disruptive extra elections. The full
sample
suite passed 9/9, with initialized `RedundancySupport=Hot`, and focused
  Raft/address-space regressions passed 8/8 and 3/3.
- `81b0b72be` moves the unchanged distributed-historian NativeAOT
scenario
into a smaller, always-run companion executable to bound Apple's linker
input. All 137 AOT tests remain present; the moved test and helpers are
  unchanged. GitHub and Azure publish/run both native executables, with
separate result directories where they share a job. The companion passed
actual Windows NativeAOT execution. Linux and Intel macOS CI passed both
  executables, but this split alone did not fix ARM64 macOS linking.
The remaining historian graph is rooted by ReferenceServer startup, not
just the relocated test. No test exclusion or speculative linker setting
  was added to conceal it.
- `8a5842913` isolates the independent MCP test graph into an always-run
native host. The existing MCP source and assembly leak hooks are linked
  unchanged, and evaluated compiler inputs prove exactly one owner for
  every test source: 133 main + 3 MCP + 1 historian = 137 declarations.
All three hosts run on every AOT platform. The main Windows native
object
is 9.35% smaller; both changed publishes had zero diagnostics. MCP
passed
  3/3; the identical main executable passed 133/133 on one unfiltered
followup after two initial endpoint-discovery connection failures, which
  remain recorded in the local evidence.
- The previous Intel macOS job successfully published and executed its
native
tests, then failed on artifact-upload DNS (`ENOTFOUND`). No upload gate,
  shared environment setting, or cache configuration was disabled.

The first repair round also encountered the unrelated
`MetricsAreEmittedForChannelLifetimeAsync` timing failure on macOS Core.
Neither that test nor channel-manager code is changed by this PR, and
its
assertions remain intact. It passed unchanged on the final repair head.

On `d17f467b3`, the Linux and macOS redundancy sample jobs pass; the
remaining
failure was ARM64 Apple's `too many large addends` assertion. The
verified
upstream relocation-anchor fix (dotnet/runtime#124721, backport
dotnet/runtime#132171) is not present in the shipping ILCompiler
10.0.11.
An experimental multi-module probe failed and was rejected, not added to
CI.
On `8a5842913`,

[`aot-macos-latest`](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/34102158680/job/101678934057)
successfully publishes and executes all three native hosts on ARM64:
136 main runtime cases, 1 historian case, and 3 MCP cases, with no
skips.
The previous Apple linker blocker is resolved. Linux NativeAOT also
passes.
No assertion, test, shared environment setting, cache, or required gate
was
weakened.

The same CI run reports a separate macOS Sessions teardown failure: all
662 executed tests pass, but the global leak assertion detects one
undisposed certificate. The MCP partition changes no runtime, Sessions,
or
test-fixture code. A single unchanged diagnostic rerun is queued once
GitHub
permits rerunning that job; the failure is not suppressed or counted as
green.
Other CI checks are still pending.

## Related issues

- Fixes #4387
- Fixes #4400

## Checklist

- [ ] I have signed the CLA and read the contributing guide.
- [x] I have added regression tests and directly related documentation.
- [x] I have retained working reference and redundant-server sample
wiring.
- [x] Final validation and review evidence is complete, including the
baseline exceptions above.
- [ ] All required CI checks pass; the fail-fast rerun for `73c619435`
is pending.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9dbd9527-d818-47a0-984c-fbb1472d3d6b
2026-09-08 11:32:23 +02:00
Copilot d525bc3f28 Bound reconnect cleanup and organize samples by domain (#4276)
# Description

This PR fixes bounded ManagedSession reconnect cleanup and reorganizes
the sample applications into domain-focused folders.

## Reconnect cleanup

- Bounds reconnect integration cleanup with independent 30-second close
and dispose limits.
- Sends `CloseSession` through the awaited transport request path during
`Session.DisposeAsync`.
- Preserves explicit `Session.CloseAsync` behavior that waits for or
cancels outstanding publish requests.
- Covers the disposal request path with a regression test.

## Sample organization

- `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer`
- `samples/Redundancy`: `RedundantClient`, `RedundantServer`,
`RedundantPubSub`
- `samples/Isa95`: `MinimalIsa95Server`
- `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`,
`MinimalClient`
- `samples/DI`: `PumpDeviceIntegrationServer`
- `samples/Lds`: `ConsoleLdsServer`
- `samples/Reference`: `ConsoleReferenceClient`,
`ConsoleReferenceServer`
- `samples/PubSub`: `ConsoleReferencePubSubClient`

`UA.slnx` now exposes the same requested domain groups. Project
references, tests, runtime sample probes, CI workflows, Dockerfiles,
Compose files, helper scripts, and documentation were updated to the new
paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`.

## Validation

- `dotnet build UA.slnx`
- 3 Redundancy short-haul sample tests
- 70 DI sample tests
- 914 OpenUSD tests
- 1 WotCon model-materialization test
- 137 AOT tests
- `docker compose config` for all 6 moved Redundancy Compose files

## Related Issues

- Fixes #4273

## 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.
- [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 built the complete `UA.slnx` solution locally.
- [ ] I ran all tests locally against at least .NET Framework and .NET
10.
- [ ] I fixed all failing and flaky tests in the CI pipelines and all
CodeQL warnings.
- [x] I have addressed all PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <mschier@microsoft.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2026-08-21 04:35:47 +00:00