Add generic xRegistry libraries (abstract registry base model) - #4097
Merged
Conversation
Adds three new packages implementing the generic, registry-agnostic xRegistry abstract base model (Annex B) so that any concrete registry can reuse the same content-addressed resource identity, registration lifecycle and federation: - Opc.Ua.XRegistry: the abstract base companion NodeSet (source generated), well-known provisional NodeIds and the IResourceContentIdProvider abstraction that maps a resource document + format to its content-derived identity. Depends only on Opc.Ua.Core. - Opc.Ua.XRegistry.Client: resolves a resource by its content-derived id via the Opaque NodeId fast path and registers a resource through the CreateResource/Write/Close lifecycle. - Opc.Ua.XRegistry.Server: server-side node managers for the content-addressed fast path, the registration lifecycle with auto-bootstrap on Close, and federated resource proxies. This code was previously developed inside the experimental data encodings work as the substrate underneath the PubSub Schema Registry. It has no dependency on Avro, Arrow or PubSub, so it is factored out here to be reviewed and shipped on its own. Two fixes were applied while extracting: - XRegistryRegistrationNodeManager now uses System.Threading.Lock instead of an object-typed sync root, per the repository locking rules. - Dropped the InternalsVisibleTo entries granting internals to Opc.Ua.PubSub.Server.Tests; that project only consumes public XRegistryWellKnown API, and Opc.Ua.XRegistry/.Client have no internals at all. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
…ucture Adds tests/Opc.Ua.XRegistry.Server.Tests covering the registration lifecycle node manager: the CreateResource/Write/Close happy path plus the negative paths for the concurrent-upload, per-resource-size and registered-resource bounds that guard against remote memory / address-space exhaustion. Also adds tests/Directory.Build.props and tests/Directory.Build.targets. The .NET 10 SDK prunes Microsoft.Extensions.Logging.Abstractions out of the test output, and coverlet's Mono.Cecil resolver only searches the output directory, so it silently fails to instrument every module that references ILogger and reports 0% coverage for them. These files force the package back into the restore graph and copy its implementation next to the test binary. The UA.slnx entry for the new test project is added without disturbing the existing Opc.Ua.Redundancy.Kubernetes.Tests entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
Adds docs/XRegistry.md covering the content-derived resource identity (IResourceContentIdProvider), the Opaque-NodeId fast path, the CreateResource/Write/Close registration lifecycle with auto-bootstrap on Close, federated resource proxies, the resource-exhaustion bounds enforced by the registration Methods, and server-side plus client-side usage examples. Links it from the documentation index. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
Pure style / analyzer pass over the new xRegistry files; no behaviour changes. Clears all 28 info-level diagnostics (IDE0005, IDE0090, IDE0270, IDE0305, RCS0027, RCS1085, RCS1140, RCS1181) so the projects build clean at the repository's "all"/preview analysis level. - Move private fields to the end of each class, ordered public -> private, per the repository member-ordering convention, and move the private static AddMethod helper below the internal Method handlers. - Expand every single-line /// <summary>...</summary> to the required multi-line form. - Drop unused usings; document the InvalidOperationException thrown by the two CreateAddressSpace overrides; convert the Method signature comments into XML doc comments. - XRegistryClient: replace the m_namespaceIndex field plus expression-bodied property with a get-only auto-property (public API unchanged), and put the binary operator at end of line per roslynator_binary_operator_new_line. The three node managers are intentionally left unsealed: they are the derivation points a concrete registry specializes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
marcschier
added a commit
that referenced
this pull request
Jul 25, 2026
…odings branch The generic xRegistry stack (Opc.Ua.XRegistry, Opc.Ua.XRegistry.Client and Opc.Ua.XRegistry.Server) was factored out of this branch into #4097 so it can be reviewed and shipped independently of the experimental Avro / Arrow encodings. This branch is now stacked on that PR and keeps only the encoding work and the PubSub Schema Registry specialization that derives from the generic node managers. Also brings in the 13 master commits #4097 is based on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4 # Conflicts: # docs/README.md # src/Opc.Ua.XRegistry.Client/Opc.Ua.XRegistry.Client.csproj # src/Opc.Ua.XRegistry.Client/XRegistryClient.cs # src/Opc.Ua.XRegistry.Server/Opc.Ua.XRegistry.Server.csproj # src/Opc.Ua.XRegistry.Server/XRegistryFastPathNodeManager.cs # src/Opc.Ua.XRegistry.Server/XRegistryFederationNodeManager.cs # src/Opc.Ua.XRegistry.Server/XRegistryRegistrationNodeManager.cs # src/Opc.Ua.XRegistry.Server/XRegistryServerNodeSets.cs # src/Opc.Ua.XRegistry.Server/XRegistryServerOptions.cs # src/Opc.Ua.XRegistry/IResourceContentIdProvider.cs # src/Opc.Ua.XRegistry/Opc.Ua.XRegistry.csproj # src/Opc.Ua.XRegistry/XRegistryNodeSets.cs # src/Opc.Ua.XRegistry/XRegistryWellKnown.cs # tests/Opc.Ua.XRegistry.Server.Tests/XRegistryRegistrationNodeManagerTests.cs
The NodeSet2 importer decoded Value elements with an empty encodeable factory, so a method's InputArguments/OutputArguments (a list of Argument ExtensionObjects) decoded to an empty list. HasArguments was still set, so generation silently emitted argument-less NodeState handlers and ObjectType proxies for every NodeSet2-sourced model. Decode with a factory that registers Argument, and cover it with a regression test. Also correct the xRegistry NodeSet: InputArguments/OutputArguments BrowseNames must be in namespace 0, and rename the CreateResource/GetOrCreateResource output VersionId to AssignedVersionId so it no longer collides with the input of the same name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
The NodeSet2 was both a source-generator input and an embedded resource that XRegistryNodeSets/XRegistryServerNodeSets re-parsed at runtime through the RuntimeNodeSetSource pipeline, so the model was materialized twice by two mechanisms. The generator is now the single source: the node managers return the generated model from LoadPredefinedNodes (the Opc.Ua.Di.Server pattern) and the embedded resource and both runtime loader types are gone. Generated code is emitted into the project's own Opc.Ua.XRegistry namespace, which also yields the typed RegistryTypeClient / GroupTypeClient / ResourceTypeClient (: FileTypeClient) ObjectType proxies. The hand-written instance identifiers in XRegistryWellKnown collided with the model - 63001-63004 are GroupType, ResourceType, AttributesType and RegistryCapabilitiesDataType - so they move above the range the model occupies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
XRegistryClient hand-rolled Session.CallAsync/ReadValueAsync against invented Write/Close methods. It is now an abstract base whose wire interactions go through the source-generated RegistryTypeClient / GroupTypeClient / ResourceTypeClient proxies and the model's real lifecycle: CreateResource opens the version for writing and the document streams through the FileType methods ResourceType inherits. GenericXRegistryClient is the sealed implementation for the base model; a domain registry client derives from the same abstract base. Because a domain model subtypes the base types, the generated proxy chain (SchemaFileTypeClient : ResourceTypeClient : FileTypeClient) lets domain proxies reuse the ResourceTypeClient extensions directly and lets a generic client drive a domain registry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Adds tests/Opc.Ua.XRegistry.Client.Tests (registered in UA.slnx) covering the proxy-based client: constructor validation and namespace resolution, the Opaque-NodeId fast path including the not-registered and propagating fault paths, chunked registration through the inherited FileType methods, and the document read-back loop. A test-only domain client derived from the abstract XRegistryClient proves the extensibility contract - a domain registry inherits the whole lifecycle - and the proxy chain assertions pin ResourceTypeClient to FileTypeClient. On the server side the mocked IServerInternal harness moves into XRegistryServerTestHarness (seeding the base types the compiled model derives from) and new fixtures cover the fast-path and federation managers. xRegistry source coverage is 97.1% (530/546), against the 80% patch gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #4097 +/- ##
==========================================
- Coverage 79.95% 79.68% -0.27%
==========================================
Files 1472 1481 +9
Lines 199303 200265 +962
Branches 34534 34724 +190
==========================================
+ Hits 159347 159584 +237
- Misses 27833 28543 +710
- Partials 12123 12138 +15
🚀 New features to boost your workflow:
|
marcschier
commented
Jul 26, 2026
Resource documents are about to move off the node manager's in-process buffers and behind a provider, so a registry can keep them in a shared store and survive a failover instead of pinning that state to one process. Mirrors the IFileSystemProvider / HistoricalAccess provider model; Opc.Ua.Server's FileObjectState is internal so the FileType plumbing cannot be reused directly. The provider is document-oriented rather than stream-oriented because a resource's identity is derived from the whole document, so an upload is committed in one piece once the content id has been computed. XRegistryServerOptions.ResourceStore defaults to the in-process implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
The registry root is now materialized through the source-generated RegistryType factory (SystemContext.CreateInstanceOfRegistryType), the same pattern DiNodeManager uses for DeviceType. A bare new RegistryState(parent) does not carry the type's children - the generated states have an empty constructor, no Initialize override and no InitializationString - so the optional children, including the lifecycle Methods, are materialized explicitly through the generated Add* builders. RegistryType.CreateGroup and GetOrCreateGroup are bound to async handlers and create GroupType instances at runtime, each carrying the model's own resource lifecycle Methods. CreateGroup is strict (BadNodeIdExists on a duplicate id) and GetOrCreateGroup is idempotent, reporting Created. Runtime instances are allocated above the range the compiled model occupies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
GroupType.CreateResource and GetOrCreateResource now materialize ResourceType instances from the generated factory and return the model's real outputs (ResourceNodeId, AssignedVersionId, FileHandle, Created). An empty VersionId makes the server assign the next one, a duplicate (ResourceId, VersionId) fails with BadNodeIdExists, and GetOrCreateResource is idempotent. Because ResourceType is a FileType, the document is transferred through the inherited Open/Write/Read/Close rather than a registry-specific mechanism. Closing a write handle computes the content-derived id, commits the document to IXRegistryResourceStore, bumps the epoch and publishes the Opaque content-id fast-path node. The handlers are async, so the store is awaited rather than blocked on. MaxConcurrentUploads, MaxResourceBytes and MaxRegisteredResources are enforced on the new lifecycle. xRegistry coverage: 96.3% (1144/1188). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Delete is bound on both GroupType and ResourceType with the model's optimistic-concurrency semantics: a stale ExpectedEpoch is rejected with BadInvalidState instead of deleting a newer version. Deleting a resource also removes its Opaque fast-path node and its document from the resource store; deleting a group removes every version it owns and frees its GroupId. With the model's own lifecycle in place, the hand-written CreateResource/Write/Close/Delete handlers, the ResourceGroup object they hung off and the well-known identifiers that addressed them are removed. Those methods were never part of the compiled model, and the PR is unmerged, so no [Obsolete] shim is warranted. The tests that drove them are replaced by tests against the real contracts - the DoS bounds are still covered. xRegistry coverage: 97.4% (1046/1074). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Adds CreateGroupAsync / GetOrCreateGroupAsync, an idempotent GetOrRegisterResourceAsync that only streams the document when it actually created the version, and DeleteResourceAsync / DeleteGroupAsync carrying the model's ExpectedEpoch. All of them go through the generated proxies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Every RegistryType, GroupType and ResourceType instance now materializes its optional Labels Object and binds the AttributesType Methods on it. Labels are published as addressable String Properties in the registry namespace and both mutations take the owning node's epoch as an optimistic-concurrency check, so a stale caller is rejected rather than silently overwriting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
The federated proxy is now a ResourceType instance created from the generated type factory instead of a hand-built BaseObjectState with invented Properties, so a generic xRegistry client drives a federated resource through exactly the same proxy as a local one. The last hand-written Federation* NodeIds are gone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Adds tests for the paths the patch left uncovered: both MaxConcurrentUploads gates (CreateResource and Open), Read with an unknown handle, Read on a write handle, Read at EOF and with zero length, Close with an unknown handle, Close of a read handle, and the de-duplication guard where two resources carrying identical bytes resolve to one fast-path node. These are the claims the hardening table and the content-derived identity design make, so they are worth asserting rather than inferring. Patch coverage goes from 97.59% to 99.84%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
marcschier
commented
Jul 26, 2026
marcschier
commented
Jul 26, 2026
marcschier
added a commit
that referenced
this pull request
Jul 26, 2026
marcschier
added a commit
that referenced
this pull request
Jul 26, 2026
) The xRegistry base branch replaced the invented registration lifecycle with the companion model's own: a registry root owns groups, groups own resources, and a resource is a FileType whose document transfers over the inherited file Methods. The base companion model is now compiled into Opc.Ua.XRegistry by the model source generator, so XRegistryNodeSets, XRegistryServerNodeSets and the CreateResource/Write/Close/Delete NodeIds on XRegistryWellKnown are all gone. Client side: - SchemaRegistryClient follows XRegistryClient, which is now abstract and takes an ITelemetryContext for the generated ObjectType proxies. - RegisterSchemaAsync now registers into a SchemaGroup by schema resource id and returns the resource NodeId plus the version the server assigned. Added GetOrCreateSchemaGroupAsync and the idempotent GetOrRegisterSchemaAsync. - SchemaRegistrySink registers each DataSet's schema as one resource keyed by the DataSet identity and carries the MetaData MajorVersion as the resource version, so a schema growth versions the same resource instead of creating an unrelated entry. It uses the idempotent call, so re-announcing a known schema succeeds. - SchemaRegistrySinkOptions collapses to the SchemaGroup NodeId and chunk size, and no longer models NodeId as System.Nullable, per the INullable rule. Server side: - SchemaRegistryOptions maps the new XRegistryServerOptions surface: the injectable resource store plus the registry root BrowseName and identity. - The Schema Registry NodeSet declares a RequiredModel on the xRegistry base, and the runtime importer must resolve those supertypes. Since Opc.Ua.XRegistry no longer publishes the base as a runtime NodeSet, Opc.Ua.PubSub embeds the base document and SchemaRegistryNodeSets exposes it, keeping the import path self-contained. Tests keep their original coverage - namespace registration, the type model in the address space, fast-path resolution, the register/resolve round trip, deletion and federation - expressed through the new model. No test was dropped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
marcschier
added a commit
that referenced
this pull request
Jul 26, 2026
Test-only change on the base branch; no API impact on the PubSub Schema Registry specialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
Resource store (thread on IXRegistryResourceStore): - Reshaped the store to be offset and length based so it maps the FileType access model, and reads now stream the requested slice out of the store instead of materializing the whole document. Documented random-access writes, the read clamping rules and how error conditions are reported. - Added FileSystemXRegistryResourceStore built on the IFileSystem abstraction from Opc.Ua.Types. - Added a store contract test suite that runs against both implementations. Client: - ResolveResourceAsync now goes through the ReadBytesAsync session extension, so a document larger than MaxByteStringLength is fetched with range based reads rather than failing. Security: - Registry writes always require a SignAndEncrypt channel; added RequireEncryptionForReads to extend that to reads. Tests: - Merged Opc.Ua.XRegistry.Client.Tests and Opc.Ua.XRegistry.Server.Tests into a single Opc.Ua.XRegistry.Tests project. Docs: - Dropped the Annex B reference, generalized the schema document wording, reworded the extensibility example, and added sections on implementing a resource store and on transport security. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
marcschier
marked this pull request as ready for review
July 26, 2026 16:51
marcschier
commented
Jul 27, 2026
- Renamed InMemoryXRegistryResourceStore to InMemoryResourceStore and FileSystemXRegistryResourceStore to FileSystemResourceStore. The XRegistry prefix was redundant inside the Opc.Ua.XRegistry.Server namespace. - Replaced the value tuples returned by the client convenience API with ResourceRegistrationResult and GroupRegistrationResult readonly record structs, matching the result type convention used across the stack. All three tuple returning methods were converted rather than only the one flagged, so the convenience layer stays consistent. RegisterResourceAsync now reports Created as well; it drives the strict CreateResource, so it only returns when it created the version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
The base branch replaced the client's value tuples with named record structs, added a chunk-size parameter to the resolve path, renamed the resource store implementations, and moved the seed/federation documents to ByteString. - SchemaRegistryClient returns GroupRegistrationResult and ResourceRegistrationResult instead of tuples, and forwards the new maxByteStringLength argument on ResolveSchemaAsync. - SchemaRegistryOptions constructs InMemoryResourceStore under its new name and holds the seed and federated schema documents as ByteString, matching the ByteString-over-byte[] preference the base now follows. - The Schema Registry integration tests read those documents through .Span. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
koepalex
reviewed
Jul 27, 2026
romanett
reviewed
Jul 27, 2026
koepalex
reviewed
Jul 27, 2026
koepalex
reviewed
Jul 27, 2026
koepalex
reviewed
Jul 27, 2026
Four defects found in review, all around the shared content-addressed fast-path node and the write handle: - The fast-path node is shared by every resource whose document has the same bytes, but its lifetime was tied to whichever resource happened to be deleted first, which silently broke resolution for the others. Its lifetime is now reference counted: publishing takes a reference and the node is only unpublished once the last resource that resolves to those bytes lets it go. - Re-writing a resource with different content left the superseded content id published forever. Closing a write handle now releases the previous content id before taking a reference on the new one, and skips the churn entirely when the bytes did not change. - Committing a document wrote at offset 0 without truncating, so replacing a version with a shorter one left the tail of the previous version behind. The stored document is now removed before the new one is written, which is what the store contract prescribes for a wholesale replacement. - GetOrCreateResource handed out a write handle on the existing resource path without checking MaxConcurrentUploads, and the client never closed that handle when it had not created the version, so every idempotent re-registration leaked one handle. The bound now covers every path that hands out a handle and the client always releases it. Also widened the source generator value decoding factory to register every encodeable type exported by the built-in assembly rather than only Argument, so a NodeSet carrying any other standard structure decodes too. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
OnFileReadAsync took the handle's Position outside the lock and only advanced it after awaiting the store, so two overlapping Reads on one handle both started at the same offset: both returned the same slice and the cursor then skipped one. StoreKey and Writing are immutable for the life of the handle, but Position is not. The cursor is now taken and the range reserved inside the same lock that validates the handle, and the optimistic reservation is corrected down afterwards so a short read at the end of the document leaves the cursor at the real end rather than past it. Added a gated resource store that holds both readers inside ReadAsync until each has taken its offset, which makes the overlap deterministic instead of timing dependent. The test fails against the old code with both reads starting at offset 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
The spec defines ExpectedEpoch as optimistic concurrency where a non-zero value that does not match the entity's epoch fails Bad_InvalidState and 0 disables the check. The implementation compared for equality unconditionally, and since Epoch starts at 1 and only increments, passing 0 could never succeed: the documented force path was unreachable on Delete for resources and groups and on AddAttribute and RemoveAttribute. Also syncs the compiled NodeSet2 byte for byte with the regenerated spec model so the spec stays the single source of truth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Review found that the idempotent-registration fix landed earlier was destructive. GetOrCreateResource hands out a write handle even when it returned an existing version, and the client releases it without writing; Close treated every write handle as a completed upload, so it erased the stored document, re-fingerprinted the resource as empty and dropped the fast-path node. A handle now tracks whether anything was written and Close only commits when it was. File handles were also server-wide, sequential and unvalidated: a caller could drive another resource's document through its own resource's Methods, and Close would store one resource's bytes while marking a different one. A handle now records the resource it was opened on and is rejected elsewhere. Close was gated unconditionally on the write policy, so a read handle opened on a channel the read policy allows could never be closed and leaked, permanently consuming the upload budget. Close is now gated on what the handle actually does. Handles open on a deleted resource were never released, and a repeated or racing Delete double-released the shared fast-path reference and drifted the registration count. Removal is now idempotent and drops the resource's handles. Close could also publish a fast-path node for a resource deleted while the store call was in flight; it now re-validates before touching fast-path state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
Open reduced the mode to a single bool. Per OPC 10000-5 the combinations with neither Read nor Write, with both, and with EraseExisting or Append but no Write are now rejected with BadInvalidArgument. More importantly EraseExisting and Append are honoured: a write-open that does not erase seeds the buffer from the stored document and overwrites at the cursor, so a partial rewrite no longer silently truncates the rest of it. The inherited Size and OpenCount Properties are kept current. Auto-assigned VersionId could collide with a version the caller created explicitly, which failed the create outright or made GetOrCreateResource return an unrelated version. The counter is now scoped to the owning group, advances past any existing version, and is pruned when a resource's last version goes so the map stays bounded. File handles are now bound to the session that opened them (a null session id means an in-process call) and released when that session closes, so an abandoned session no longer holds the upload budget forever. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Repo guidance mandates ByteString over ReadOnlyMemory in public API, and the store surface was inconsistent with itself since ReadAsync already returned ByteString. Switched IXRegistryResourceStore.WriteAsync, both stores, WriteDocumentAsync and the two client registration methods. ByteString wraps ReadOnlyMemory with no copy, so the per-chunk allocation removed earlier stays removed. XRegistryServerOptions is now sealed. The three node managers stay unsealed on purpose: subclassing is the server-side extension seam a domain registry uses, mirroring how a domain client derives from XRegistryClient, and that is now stated in their docs. Added XRegistryServiceCollectionExtensions so the server pieces wire into DI, with direct construction still supported. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
Adds the coverage the plan called for but the previous commits omitted: version assignment (skipping an explicitly created version, per-group scoping, counter pruning on delete) and session ownership (a handle rejected from another session, and a closing session releasing its handles). Both suites were verified to fail against the pre-fix behaviour. Two existing security tests were opening a handle in-process and then operating on it from a real session, which the session check now correctly rejects. They are restructured to hold the session constant and vary only the channel, so they test the security policy rather than two rules at once. Also covers the new service collection extensions, taking the patch from 95.8 to 98.5 percent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
marcschier
added a commit
that referenced
this pull request
Jul 27, 2026
The base branch moved the client's document parameter from ReadOnlyMemory<byte> to ByteString, matching the repository's preference for ByteString over byte[] / ReadOnlyMemory<byte> in public API. SchemaRegistryClient takes ByteString on RegisterSchemaAsync and GetOrRegisterSchemaAsync, and SchemaRegistrySink now hands the notification's schema document straight to the client instead of copying it into a new array on every publish. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4
koepalex
reviewed
Jul 27, 2026
OnFileCloseAsync writes the document to the resource store outside the lock and only then re-checks that the resource still exists. A concurrent Delete can complete its own store cleanup inside that window, so the bytes the commit wrote survive it. Store keys are the resource NodeId and instance ids only ever increase, so the key is never reused and nothing would ever collect them: a permanent entry in the in-memory store, or a permanent file on disk for the file-backed one. The post-write block now returns the abort decision instead of returning from inside the lock, and deletes the just-written document before failing the call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cb13c4-ea7c-4f1c-b6a6-ab037e418c6b
koepalex
approved these changes
Jul 28, 2026
marcschier
added a commit
that referenced
this pull request
Jul 28, 2026
#4097 was squash-merged into master, so master now carries the xRegistry libraries as a single commit with no shared history with the copies this branch carried while it was stacked on that PR. Resolve every xRegistry path in favour of master, which is now authoritative and also newer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 000aa394-d085-4bf5-b605-a7adbe7d27f4 # Conflicts: # docs/README.md # src/Opc.Ua.XRegistry.Server/XRegistryRegistrationNodeManager.cs
marcschier
added a commit
that referenced
this pull request
Jul 28, 2026
Merges origin/master, which introduced the generic xRegistry libraries
(src/Opc.Ua.XRegistry{,.Client,.Server}, #4097), and drops this branch's
private copy of that model in favour of the shared one.
xRegistry consolidation
- Opc.Ua.WotCon no longer vendors src/Opc.Ua.WotCon/Design/Opc.Ua.XRegistry.NodeSet2.xml.
It now takes a ProjectReference on Opc.Ua.XRegistry and links master's NodeSet as a
dependency-only AdditionalFile (ModelSourceGeneratorIgnore=true), so the model resolves
for source generation but the types come from the shared assembly.
The vendored copy was also subtly wrong: it namespace-qualified the standard
namespace-0 properties InputArguments/OutputArguments as 1:InputArguments.
- Verified there are now zero duplicate xRegistry types in Opc.Ua.WotCon.dll and that
the generated WoT proxies derive from the shared ones
(WoTDocumentTypeClient : Opc.Ua.XRegistry.ResourceTypeClient).
- Call sites moved from the generated XRegistry.Namespaces.XRegistry constant to the
package's canonical XRegistryWellKnown.XRegistryNamespaceUri.
Fix a request-drain self-wait deadlock
The base-branch merge replaced AsyncLocal<uint?> m_currentRequestId with
AsyncLocal<bool> m_inServiceDispatch, losing the request *id*. WaitForCurrentRequestsAsync
therefore waited on the very request that triggered the lifecycle operation. The WoT
registry hits this legitimately: LifecycleWotProjectionHost sets
AllowLifecycleFromRequestCallback = true and Refresh is an OPC UA Method that triggers a
shadow reload, so Refresh timed out with BadRequestTimeout after the 2 minute drain
timeout (7 WotCon tests).
Current-request tracking is restored via AsyncLocal<StrongBox<uint?>>. The indirection is
required: an AsyncLocal *assignment* made inside an async method is not visible to the
awaiting caller, and EnterRequestScope is reached through ValidateRequestAsync. The slot
is established synchronously by EnterServiceDispatchScope (before any await, so it does
flow), and EnterRequestScope mutates the box rather than reassigning the AsyncLocal.
EnterRequestScope also creates a slot when none exists, for callers that use it directly
without going through the dispatcher.
Validation (all builds warning-free)
- net10.0: Server 3862 passed, WotCon 823, WotCon.Bindings 464, Types 8390,
SourceGeneration 94. The single Server failure is the known pre-existing local PKI
flake (ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync).
- net48: Types 8383 and WotCon 823 pass; all four test projects build clean.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
xRegistry is the abstract registry base model: a registry-agnostic, content-addressed
resource registry. It knows nothing about what a resource contains — a concrete registry supplies its
own companion namespace and a fingerprinting strategy and reuses everything else. The PubSub Schema
Registry in #4007 is the first specialization (its resources are schema documents),
and #4093 (WoT Binding) carries a byte-identical copy of the same base NodeSet, so a
shared canonical home is worth having.
Packages
OPCFoundation.NetStandard.Opc.Ua.XRegistryOpc.Ua.CoreXRegistryWellKnown,IResourceContentIdProviderOPCFoundation.NetStandard.Opc.Ua.XRegistry.ClientOpc.Ua.ClientXRegistryClient(abstract) /GenericXRegistryClient(sealed), built on the generated ObjectType proxiesOPCFoundation.NetStandard.Opc.Ua.XRegistry.ServerOpc.Ua.ServerXRegistryServerOptions,IXRegistryResourceStoreOpc.Ua.XRegistrydeliberately depends on neither SDK, so a codec or a shared contracts assembly canreference the identity abstraction without pulling in the client or the server.
The model is compiled, not loaded
The companion model is compiled into the assemblies by the OPC UA model source generator.
Opc.Ua.XRegistry.NodeSet2.xmlis anAdditionalFilesgenerator input only — there is noEmbeddedResource, no runtime NodeSet parsing and no hand-written NodeId tables for anything themodel declares. All three node managers load it the same way:
XRegistryWellKnownis reduced to the namespace URI plus the identifiers of the instances aregistry materializes at runtime, which live above the model's own 63000-63999 range so they can
never collide with it.
What it does
IResourceContentIdProvidermaps a document + format to afingerprint and names the (canonicalization, hash) algorithm. Identity is derived from the bytes,
not assigned by the server, so it is stable across registries — which is what makes de-duplication
and federation work.
NodeIdwhoseIdentifier is the raw content-id bytes, so a consumer that received the id on the wire resolves the
document in a single
Read: no Browse, no fingerprint recomputation. The client reads it throughthe
ReadBytesAsyncsession extension, so a document larger thanMaxByteStringLengthis fetchedwith range-based reads rather than failing. Optionally pre-publishes a seed resource so a fresh
server can resolve one resource before any registration.
RegistryTypeinstance servingCreateGroup/GetOrCreateGroup; each group servesCreateResource/GetOrCreateResource; andbecause
ResourceType : FileType, the document is the file — content is streamed through theinherited
Open/Read/Write/Close.Closefingerprints the accumulated bytes andpublishes the Opaque fast-path node at runtime. Identical bytes reuse the existing node, whose
lifetime is reference counted so it survives until the last resource resolving to those bytes is
gone and is retired when a resource is re-written with different content.
Delete(ExpectedEpoch)on both resources and groups, and the sameepoch check on label mutations, so a caller working from a stale read is rejected rather than
clobbering a concurrent change.
RegistryType,GroupTypeandResourceTypeeach expose anAttributesTypeObjectwith
AddAttribute/RemoveAttribute; labels are published as addressableStringProperties.ResourceTypeinstance whose
ExternalReference(ExpandedNodeId+ServerIndexinto the localServerArray)and
ResourceUrlcarry the link and whoseXidcarries the content-id. Because it is an ordinaryResourceType, a generic client drives a federated resource through exactly the same proxy as alocal one.
Extensibility
A domain registry subtypes the base model — the PubSub Schema Registry declares
SchemaFileType : ResourceType— and the generator mirrors that hierarchy across assemblies, so theproxy chain follows automatically (
SchemaFileTypeClient : ResourceTypeClient : FileTypeClient).XRegistryClientand inherits the whole lifecycle. Helperswritten as extension methods over a base proxy (such as
WriteDocumentAsynconResourceTypeClient) are callable on the domain proxy with no inheritance in the client layer.the base type. This is covered by a test that defines a domain subclass over the base.
State
Document bytes live behind an injectable
IXRegistryResourceStore. Because a resource is aResourceType, which is aFileType, the store mirrors the file access model: reads and writes areoffset and length based, so a document never has to be materialized as a whole and a ranged read
touches only the bytes it needs.
The document is transferred with the inherited
FileTypeMethods, whose mode bits are honoured: awrite that does not set
EraseExistingstarts from the stored bytes and replaces only the range itwrites rather than truncating the rest. A handle is valid only on the resource and the session that
opened it, and a session's handles are released when it closes.
Two implementations ship:
InMemoryResourceStore(the default, in-process) andFileSystemResourceStore, built on theIFileSystemabstraction so documents outlive theprocess and a shared volume can back a cluster.
XRegistryResourceStoreContractTestsis a reusablefixture that validates any implementation against the contract.
Transport security
Registry writes always require a
SignAndEncryptsecure channel — a document and itscontent-derived identity are integrity-critical, so every mutation is rejected with
BadSecurityModeInsufficienton a channel that is only signed or unprotected. This is notconfigurable. Reads are allowed on any secure channel by default;
RequireEncryptionForReadsextendsthe requirement to them when the documents themselves are confidential. An in-process call carries no
channel and is always allowed.
Hardening
The lifecycle Methods are remotely callable, so every unbounded dimension is bounded via
XRegistryServerOptions; exceeding a bound fails the call rather than the server:MaxConcurrentUploadsCreateResource,OpenBadTooManyOperationsMaxResourceBytesWriteBadRequestTooLargeMaxRegisteredResourcesCreateResourceBadTooManyOperationsA source generator bug fixed along the way
Building on the compiled model surfaced a defect in shared generator infrastructure.
NodeSetToModelDesign.CreateDecoderdecoded values with an emptyEncodeableFactory, so a NodeSet2'smethod
InputArguments/OutputArguments— encoded as a list ofArgumentExtensionObjects — neverdecoded, while
HasArgumentswas still set. Every NodeSet2-sourced model in the stack wassilently generating argument-less
MethodStates and ObjectType proxies. ModelDesign-sourced models(the core stack) were unaffected, which is why this went unnoticed. Fixed in
b87726afwith aregression test that fails without it.
Two defects in the xRegistry NodeSet itself were fixed at the same time:
BrowseName="1:InputArguments"→ ns-0
InputArguments(published NodeSets never namespace-qualify these), and theCreateResource/GetOrCreateResourceoutputVersionIdrenamed toAssignedVersionId, which collided with theinput of the same name and produced a duplicate C# parameter.
Other fixes applied while extracting
XRegistryRegistrationNodeManagerusesSystem.Threading.Lockinstead of anobject-typed syncroot, per the repository locking rules.
InternalsVisibleToentries granting internals toOpc.Ua.PubSub.Server.Tests. Thatproject only consumes public
XRegistryWellKnownAPI.UA.slnxedit in DRAFT Add experimental data encodings #4007 accidentally replaced thetests/Opc.Ua.Redundancy.Kubernetes.Testsentry instead of adding a line, silently dropping thatproject from the solution. The entry is preserved here and the change is purely additive.
Tests
tests/Opc.Ua.XRegistry.Tests(168) covers the group and resource lifecycles, theFileTypetransfer, delete and epoch conflicts, labels, federation, both resource stores, the fast path,
transport security and the extensibility contract — positive and negative paths. Passing on
net10.0and
net48. Patch line coverage is 98.5%.Also adds
tests/Directory.Build.props/.targets: the .NET 10 SDK prunesMicrosoft.Extensions.Logging.Abstractionsout of the test output, and coverlet's Mono.Cecilresolver only searches the output directory, so it silently fails to instrument every module that
references
ILoggerand reports 0% coverage for them. These force the package back into the restoregraph and copy its implementation next to the test binary.
Documentation
New
docs/XRegistry.md, linked from the documentation index.Follow-ups (deliberately not in this PR)
[LoggerMessage]logging is wortha follow-up.
Opc.Ua.XRegistry.NodeSet2.xmlundersrc/Opc.Ua.WotCon/Design/; de-duplicating it against this package is a natural follow-up.XRegistryWellKnownare provisional — final identifiers are assigned by the OPCFoundation.
Related
PubSub Schema Registry specialization.