WoT Binding and WoT Connectivity 1.1 - #4093
Draft
marcschier wants to merge 113 commits into
Draft
Conversation
…ode-manager-lifecycle # Conflicts: # src/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs
…ode-manager-lifecycle # Conflicts: # src/Opc.Ua.Server/NodeManager/MasterNodeManager.cs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 29378426-2a98-41ee-9a6d-050af5b1141f
Merged
8 tasks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 29378426-2a98-41ee-9a6d-050af5b1141f
…ver limits - Make uav:nodeSet.sha256 mandatory: a missing, malformed, or mismatched digest now produces a structured InvalidDigest/ DigestMismatch diagnostic and no NodeSet is returned, instead of silently skipping integrity verification when the field was absent. - Catch XML/deserialization failures from UANodeSet.Read and report them as a WotDiagnosticCode.MalformedNodeSet diagnostic instead of letting the exception escape the converter. - Create exactly one WotResolutionContext per top-level conversion in WotNodeSetConverter.ToNodeSetCore, seeded from the converter options (depth, max documents, cumulative bytes), and thread that single context through link/thing resolution. Removes the previous per-link context ??= new WotResolutionContext() fallback in TryResolveTargetNodeId, which reset cycle/limit tracking for every link instead of applying bounds across the whole conversion. - Seed WotNodeSetDocumentConverter's resolution context from its configured options instead of resolver defaults. - Add WotNodeSetConverterOptions.MaxResolverDocuments/ MaxResolverDocumentBytes/MaxResolverTotalBytes (aligned with WotResolverOptions defaults) plus ToResolverOptions() and validation, so aggregate resolver limits are configurable through the same options object used for the rest of the conversion. - Add tests: missing/malformed digest, digest mismatch, malformed NodeSet XML without throwing, unsupported vs. accepted (base64) encoding, multi-link aggregate document/byte limits sharing one context, and sibling-link cycle detection that doesn't block unrelated links. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After a ShadowReload the retired generation is removed from the routing table but still owns the monitored items created on it. Modify, Delete, SetMonitoringMode and Transfer previously iterated only the visible routing-table managers, so a same-namespace replacement generation would claim (and fail on) the retired items - returning BadMonitoredItemIdInvalid and leaving the retired generation's items undrained and its address space never disposed until another lifecycle operation or shutdown. Dispatch each data monitored item to its recorded owning NodeManager (grouped by owner, each owner offered only its own indices) so a retired generation services its own items and a same-namespace replacement cannot cross-claim them. Event/all-event handling and result indexing are unchanged. Trigger prompt retired-generation cleanup when the last item drains: the master notifies the lifecycle from the Delete path when an owner is no longer registered, and the lifecycle runs cleanup on a background task (request execution context suppressed, coordinated via the lifecycle semaphore) so nothing is disposed inside the request callback. Existing fail-closed Reload/Remove behaviour and the deferred-cleanup fallback are preserved. Add tests proving that after ShadowReload old data items can be modified, disabled/re-enabled, transferred and deleted on the retired generation, new reads use the replacement, old notifications keep flowing, and the retired generation is disposed after the final drain without another lifecycle operation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rects) MQTT: mqtts now enables TLS and defaults to port 8883, resolving username / password, TLS client certificate and trust anchors through the credential provider. A declared security scheme fails closed when unresolved, and username / password never downgrades to plaintext unless explicitly allowed. plain mqtt stays explicit (port 1883). Modbus: the planner enforces address 0-65535 and the address+quantity-1 range, maps function-only forms exactly onto codes 1,2,3,4,5,6,15,16 (mnemonic or numeric) and rejects op/function and entity/function mismatches. The executor re-validates the range before the ushort/byte casts. HTTP: the executor-owned HttpClient disables automatic redirects and applies a bounded, origin-aware redirect policy that strips custom header/query credentials across origins, refuses loops, non-http(s) schemes and https->http downgrades, and caps hops. A caller-supplied client with a credential-bearing form fails closed unless CallerClientHandlesRedirectSafety is set. Adds planner, executor and E2E regression tests and updates the binding docs and options. WotCredential gains runtime-only client certificate / trust material. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Registry mutations now persist through a transactional store commit before publishing the snapshot or raising Changed, and the generated WoT V2 event types are fully populated from the coordinator arguments. - IWotRegistryStore: replace the per-entity Upsert/Remove methods with a single atomic CommitAsync(snapshot). LoadAsync returns only the committed generation. - FileWotRegistryStore: stage content-addressed version blobs durably, then switch a single manifest.json pointer with an atomic replace, so a crash never exposes a half-written generation; Load reads only the committed manifest. - InMemoryWotRegistryStore: keep the committed snapshot behind an atomic reference switch (same failure semantics). - WotRegistryService: commit durably before publishing m_snapshot or raising Changed for every CRUD/default/enabled/labels and projection path. On failure Current stays previous, no event is raised, a retry re-attempts persistence and a restart never sees partial data. - WotRegistryNodeManager: populate every generated WoT event field (resource identity/kind, generation, phase/outcome, validation outcome, load state/failed node/reason, binding uri, refresh summary/request id) from WotMaterializationEventArgs via SetChildValue. - Tests: fault-injection transaction tests (atomicity, no-event, retry, restart) and real EventFilter integration tests that verify the typed fields are delivered through the running server notifier chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Regenerate the Opc.Ua.WotCon model from the single revised Opc.Ua.WoTCon NodeSet2, which incorporates the published OPC 10100-1 v1.02 model (NodeIds 1..172, deprecated) plus the additive registry nodes (64000+) in one namespace (http://opcfoundation.org/UA/WoT-Con/, version 1.1.0), replacing the separate 1.02 ModelDesign and the Opc.Ua.WotCon.V2 companion model. Model / build: - Pin Opc.Ua.WoTCon.NodeSet2.xml/.csv (+ xRegistry) and drop the V2-named artifacts; update Sync-WotConModels.ps1 mappings/docs. - Opc.Ua.WotCon.csproj: stop compiling WotConnection.xml/csv; compile the combined NodeSet once as Opc.Ua.WotCon (name WotCon, model URI WoT-Con/). Legacy 1.02 sources kept under Design/ for reference only. Server / tests / docs: - Replace all Opc.Ua.WotCon.V2 namespace/type references with the combined Opc.Ua.WotCon generated types (AddOpcUaWotCon, ObjectIds, BrowseNames, DataTypeIds, ...); remove user-visible V2 wording and rename the registry config section to OpcUa:WotConRegistry:Server. - Decide model-node ownership cleanly via WotConModelPartition: the registry NodeManager owns {WotCon, xRegistry} and loads the registry slice (>=64000); the legacy asset NodeManager loads the incorporated 1.02 slice (<64000). Neither claims the same static node twice. - Add CombinedModelPreservationTests proving exact 1.02 NodeIds, typed method state/results and the generated client API are preserved, and that the registry types now coexist in Opc.Ua.WotCon. Source generator (NodeSet2 -> ModelDesign) fixes exposed by generating the richer combined model with typed method arguments: - Map placeholder BrowseNames "<Name>" to "Name_Placeholder". - Reuse an existing explicit method-type declaration instead of synthesizing a colliding duplicate, and carry the concrete method's arguments onto it. - Register the standard encodeable types (Argument) on the value decoder so NodeSet2 method argument lists actually decode (was silently empty). - Disambiguate a by-ref output argument whose name matches an input. - Skip synthesizing method types for base-namespace (inherited) methods and for standalone method-type declarations. Deprecated 1.02 nodes remain usable and all 1.02 tests pass unchanged (only mechanical Ua.DataTypeIds qualification where the new combined DataTypeIds shadows Core). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
char.IsAsciiLetterOrDigit is .NET 7+, but Opc.Ua.WotCon.Server also targets net48
and netstandard2.0, so the file-name safety check is now an explicit ASCII range
test. char.IsLetterOrDigit would have been the wrong fix: it accepts non-ASCII
letters that are not safe in a file name.
Docs: a "Keeping the document bytes in a shared store" section in
docs/WoTConnectivity.md covering WotRegistryServerOptions.ResourceStore, the DI
precedence, why the historical {root}/{digest}.bin layout is preserved (no on-disk
migration), and why the registry keeps manifest atomicity while only the bytes
move. docs/XRegistry.md now cites WotBlobResourceStore as a worked example of
implementing the store contract for a domain registry.
Validation: net10.0 WotCon 902, XRegistry 171, WotCon.Bindings 505, Types 8390,
Server 3966 (only the known pre-existing local PKI flake); net48 WotCon 902.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…' into marcschier/wot-binding-xregistry
…4110) Addresses three gaps @barnstee found while evaluating whether UA Edge Translator can replace its hand-written WoT-Connectivity node manager with this library. #4110 polling and backoff PollingWotSubscription already gave poll-only bindings a shared loop, but it only reacted to thrown exceptions: a binding that maps a failure onto a bad StatusCode returned a failed WotReadResult without throwing, so nothing was notified (the variable silently kept its last good value) and the loop kept polling an offline device once per interval. The poll callback now reports health instead. HTTP and Modbus surface the mapped bad status and report the poll unhealthy, and consecutive unhealthy polls back off through IChannelReconnectPolicy - the abstraction the stack already uses for channel reconnects, rather than a second bespoke backoff. Default is ExponentialBackoffChannelReconnectPolicy (500 ms doubling to 30 s); RetryPolicy on Http/ModbusWotBindingOptions overrides it. Backing off never polls faster than the configured interval, the first healthy poll resets it, and a policy that reports "stop retrying" ends the loop rather than spinning. Per-affordance interval without inventing vocabulary The issue asked for a polling interval on the tag. Rather than add a uav: term - that vocabulary is OPC-owned, so extending it is a specification decision - this uses the standard term the protocol binding already defines: modv:pollingTime, milliseconds, per the W3C Modbus binding (distinct from modv:timeout, a request timeout). It compiles onto WotOperationDescriptor.PollInterval and wins over the executor's ObserveInterval. HTTP has no standard polling term and keeps using its option. #4109 custom DataTypes IWotNodeSetContributor runs once per resource after conversion and before any variable is created, and may add nodes to the converted NodeSet - the hook for StructureTypes that have no NodeSet to import because they belong to one controller program (Studio 5000 / TIA Portal / TwinCAT UDTs). That ordering is the point: a uav:mapByFieldPath mapping only resolves once its structured DataType exists. The declarative path already worked - the native projection carries DataType nodes with their DataTypeDefinition - so this adds only the programmatic one. IWotDocumentConverter is now also resolved from DI; it was a constructor parameter that could not actually be overridden through the builder. #4108 NodeSet resolution - abstraction only IWotNodeSetResolver is consulted for every namespace a converted document requires but neither declares nor finds on the server, recursing into whatever it returns. Resolved models are prepended to the projection sources so they materialize before the document that requires them, and unresolved namespaces are reported rather than silently dropped. NOT a full fix for #4108: no UA Cloud Library implementation ships. Resolving a namespace means reaching out to a catalogue, which is a new external HTTP dependency and a deployment decision, so the library takes no dependency on one. The uav:nodeSet envelope (embedding the NodeSet in the Thing Description) and the existing RuntimeNodeSet loading remain the paths that need no resolver. All three seams are optional; registering none leaves behaviour unchanged. Tests: backoff engages and resets, a policy that gives up stops the loop, modv:pollingTime is carried and validated, contributors run after conversion and reach the projection, a resolved dependency is projected first, and an unresolved namespace is reported. WotCon 908, WotCon.Bindings 511; solution builds warning-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
This was referenced Jul 29, 2026
Open
marcschier
commented
Jul 29, 2026
Fixes the 11 findings from the implementation review and the 12 open review threads on this PR. Review findings - FileWotRegistryStore/WotBlobResourceStore: content-addressed blobs are now verified and skipped instead of rewritten on every commit, and blob writes stage to a temp file and publish through an atomic replace. An interrupted write can no longer leave a partial blob that the unchanged manifest still references. - Added the IAtomicFileReplace capability to IFileSystem implementations rather than a new interface member, which would break external implementers and is not expressible as a default interface member on netstandard2.0. Blob storage now fails fast instead of silently degrading to a destructive write. - The WotCon registry client accessor borrows the container-wide ManagedSession instead of disposing it, so a failed ForServerAsync no longer closes the session other client facades are still using. - ModbusTcpClient reconnects a faulted connection on the next transaction; backoff stays in PollingWotSubscription so there is only one retry loop. - NodeSetComparer enforces MaxXmlDepth, which was validated but never read, so deeply nested XML reports a diagnostic instead of overflowing the stack. - WotMaterializationCoordinator enforces MaxResolverDocumentBytes and decides namespace knownness from the live materialized closure, because namespace table entries outlive the NodeManagers that registered them. - Bulk load validates the group kind on cache hits as well as misses. - GetOrCreateGroup normalizes names the way the registry service does, so a name that slugifies differently no longer reports Created on every call. - WotRegistryProjection reads the resource output map under its gate. - Removed System.Nullable wrappers around INullable types across the WoT and registry surface, including TrySelectProjectionRoot. - The WoT resolver contracts are async. The converter pre-resolves through a shared reference enumeration into WotThingCatalog and the synchronous core only performs lookups, so the Roslyn generator stays synchronous and no sync-over-async is introduced. Review threads - Grouped the WotCon samples under samples/WotCon and dropped the Wot prefix from folder, project, assembly and namespace names. - Moved NodeSetComparer next to the other NodeSet code in Opc.Ua.Export. - Completed ExtensionObject.TryGetValue to decode through the codecs and reduced the Variant structure helpers to delegation, since the context-taking API already existed. - Factored request admission and lifecycle waiter tracking out of RequestManager into an optional registerable extension. The AsyncLocal scope nesting stays, as it fixes scope propagation rather than adding a feature. - Documented and made StandardServer disposal idempotent. - Moved MemoryWotBinding out of the shipping bindings library into the aggregation sample and the test project. - Removed Sync-WotConModels.ps1, which belongs in the drafts repository. - Renamed the pinned NodeSet2 files to WotCon casing. - Reverted a blank-line-only change in Session.cs. CompareXml now takes ReadOnlySpan<byte> instead of byte[]. The Subscription/Session and server shutdown threads asked for investigation before changes, so those are analysed and answered on the threads rather than rewritten here. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Addresses the concrete defects found while reviewing the server lifecycle handling in StandardServer, per the review request for a thorough review of that code. This is deliberately not a structural refactor: the three proposals for restructuring the coordinator are on the review thread and are still open. - The shutdown task slot is now reserved under the coordination lock while the shutdown itself starts outside it. Previously every statement before the first await in ShutdownServerInternalAsync ran under m_shutdownCoordinationLock. Concurrent callers still join a single shutdown because ActiveTask is published inside the lock. - The deferred retry is bounded by a maximum retry count and an overall time budget. It previously retried indefinitely as long as cleanup progress kept increasing. Exceeding either bound takes the existing terminal path. - Terminal shutdown failures surface through ServerError and a source-generated log rather than only being recorded on the shutdown state. - The deferred observer routes its exceptions instead of leaving them unobserved. Final node-manager cleanup keeps using CancellationToken.None. That is intentional rather than an oversight: cancelling it would leak retired lifecycle state, node managers and the server semaphore after request admission has already been closed permanently. The call sites are now commented to say so. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
marcschier
commented
Jul 30, 2026
Fixes the review comments raised on the server lifecycle, publish queue and documentation surface of this PR. No sync over async - SubscriptionManager now implements IAsyncDisposable. DisposeAsync signals shutdown, cancels the publish delay, awaits the publish and condition-refresh workers and waits the semaphore asynchronously. The synchronous Dispose no longer blocks; it only signals and releases resources. Disposal is idempotent. The previous code awaited the worker tasks with GetAwaiter().GetResult() and blocked on the semaphore from inside Dispose, which this PR had introduced. - ServerInternalData disposes the subscription manager through the asynchronous path. - WotResourceFileManager.Close routes through OnCallAsync so the commit is awaited end to end instead of being blocked on. The two remaining blocking waits in SubscriptionManager and the blocking dispose in WotConnectivityNodeManager are unchanged, having been confirmed as pre-existing. The two adapter calls in SyncNodeManagerAdapter are also unchanged: that type exists to bridge asynchronous NodeManagers to the legacy synchronous INodeManager contract, and the surrounding transfer call already does the same thing. Publish queue locking - SessionPublishQueue probes the concurrent dictionary without a lock on the common paths of PublishCompleted and Requeue, and takes the lock only to mutate queue entry state or a transfer claim, where the multi-step invariant genuinely requires it. The locks around Add, Remove, ContainsSubscription and the current-entry snapshot are gone. Documentation - Internal types and the public and internal members of internal types across the PR now carry summary documentation, including the source-generated log message classes. - Call chains keep the method name on the line of the instance where that stays within the line length limit. - WotBindings.md drops the Part N section prefixes, gains a table of contents and describes the current project layout, including the moved samples, the binding that no longer ships in the library and the asynchronous resolver contracts. - Subscription.PublishTimerExpired explains, with a reference to the specification, why a transfer in progress suspends publishing. The new publish queue concurrency tests are added under the tests directory rather than a second directory differing only in case, and they avoid APIs that do not exist on the legacy target frameworks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The core server test project referenced Opc.Ua.WotCon.Server, which inverted the layering: Opc.Ua.Server itself knows nothing about WoT, but its tests pulled in the whole WoT stack. That prevents the server changes from being reviewed or merged independently of the WoT ones. The tests in question were two kinds tangled together. The genuine WoT integration tests move to Opc.Ua.WotCon.Tests, where the stack they exercise lives. The server lifecycle behaviour they also happened to cover is already covered against plain NodeSet payloads by RuntimeNodeSetLifecycleTests, which needs no WoT types at all. The project reference and the remaining WoT usings are gone from the server test project. No production code changed and the combined pass count is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
This was referenced Jul 30, 2026
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.
Status and dependencies
Draft proof implementation.
OPCFoundation/UA-.NETStandard:marcschier/wot-binding-xregistry.[Server] Add live NodeManager lifecycle support) has landed, so this PR now targetsmasterdirectly and the lifecycle dependency is resolved.What this proves
Lossless NodeSet2 ↔ WoT conversion
Opc.Ua.Types.Extensions.uav:nodesprojection only when readable-only reverse conversion does not reproduce the complete UANodeSet.WhenRequired,Always, andNeverpreservation modes.uav:nodeSetis emitted only for explicit byte archival or a demonstrated fallback, and completeness tests useNever.WoT source generation
.tm.json,.td.json,.tm.jsonld,.td.jsonldand explicitly opted-in.jsonldAdditionalFiles.UANodeSetand uses the existing direct NodeSet code-generation pipeline.MODELGEN030–MODELGEN034diagnostics for parsing, conversion and virtual-path collisions.Live NodeManager shadow reload
ShadowReloadAsync.ImmediateReloadAsyncas the alternative policy: it atomically routes new requests to the replacement, invalidates affected non-durable monitored items withBadNodeIdUnknown, detaches disposed owner graphs, and rejects durable subscriptions before commit.ReloadAsync/RemoveAsyncbehavior unchanged.Registry-first WoT Connectivity 1.1
WotProjectionRetirementPolicyso deployments select graceful or immediate replacement.Replaceable protocol binding code-behind
Adds a dependency-light planner/binder project for:
Concrete optional executor projects cover HTTP, MQTT, Modbus TCP and OPC UA. The OPC UA executor supports Read, Write, native data-change observation, Method invocation and Event subscription, including portable
nsu=NodeIds.BACnet, PROFINET, LoRaWAN and CoAP are planner/validation-only in this proof and report their non-executable capability explicitly.
Review remediation
The follow-up review fixes are included:
mqttsenables TLS, defaults to 8883, resolves credentials/trust and fails closed;uav:nodeSet.sha256is mandatory and malformed XML becomes structured diagnostics;uav:eventType, persisted identities use portablensu=ExpandedNodeIds, andHasComponentsubtypes round-trip exactly;uaand deterministicnsNprefixes; ReferenceType model names are emitted directly in linkrel, withuav:refIdas definitive identifier, and name/NodeId conflicts are rejected;mapToTypeName/congruentTypeNamehints remain paired with definitive identifiers, while arbitrary instance targets remain ExpandedNodeIds;uav:typedReference/refTypeNamedraft surface is removed;@contextprefixes (with URI-qualified QualifiedNames also accepted), authored IDs are retained during synthesis, and the proof omitsuav:nodeswhen the semantic mapping alone is equivalent;observable/observepropertyis documented as advertised binding support, not a restriction on core UA monitorability.Compatibility
IWotAssetProviderFactoryintegrations remain supported.Opc.Ua.WotCon.Server.Commit structure
Add shadow NodeManager reload supportAdd lossless WoT conversion and source generationAdd injectable WoT protocol bindingsAdd registry-first WoT Connectivity runtimeAlign WoT Connectivity proof to combined 1.1 spec modelAlign WoT converter with portable Binding identityMake WoT NodeSet conversion native-firstAddress WoT model concept review feedbackAddress WoT mapping review feedbackPrefer context prefixes for WoT QualifiedNamesImplement selectable WoT projection retirementConsolidate WoT bindings and add WoT-driven aggregation sampleApply code style and analyzer fixes to WoT bindings branchWire the DI NodeManager fluent builder DataType resolverAddress PR review feedback on WoT docs and sample layoutRaise PR patch coverage from 70% to 91% with unit and integration testsFix net48 test failures and remove dead WoT mapping codeMerge copilot/3993-live-node-manager-lifecycle into marcschier/wot-binding-xregistryAdopt the merged xRegistry implementation and packagesDerive WotRegistryClient from the shared XRegistryClientMake subscription transfer effects transactionalAdd a WoT blob store implementing the xRegistry resource store contractLet a WoT registry keep its document bytes in an injected resource storeAdd WoT materialization and polling extension points (#4108, #4109, #4110)Address code review findings and PR feedbackValidation
Opc.Ua.Types.Testsnet10.0 and net48: passedOpc.Ua.WotCon.TestsandOpc.Ua.WotCon.Bindings.Testsnet10.0: passeddotnet build UA.slnx -c Release: passedOpc.Ua.Server/Opc.Ua.WotCon.Serverbuilds on net10.0 and net472: passed with 0 warningsgit diff --check: cleanDocumentation layout
docs/WoTProtocolBindings.md+docs/WoTBindingDevelopment.mdare merged into a singledocs/WotBindings.md, ordered as Part 1 (the bindings that ship today) and Part 2 (adding your own binding).docs/WoTAggregationSample.mdmoved tosamples/WotCon/README.md, next to the WoT samples it documents.samples/WotCon/AggregationClient/Documents: the client is what uploads them into the server, and the aggregation server has no dependency on them.AutoRefresh = trueversus the sample'sAutoRefresh = false.Test coverage
Codecov reported 70.39 % patch coverage against the 80 %
codecov/patchgate. Tests were added acrossOpc.Ua.Types/Wot,Opc.Ua.WotCon.Server,Opc.Ua.WotCon.Bindings*,Opc.Ua.Serverand the WoT source generator, raising measured patch coverage to 91.2 % (10 750 of 11 782 changed lines). No production behaviour was changed to achieve it; the only production edit was removing the unreachableWotNodeSetConverter.ToCompactModelNamehelper.Suite totals on net10.0, all warning-free:
Opc.Ua.Types.Tests8 390,Opc.Ua.WotCon.Tests823,Opc.Ua.WotCon.Bindings.Tests464,Opc.Ua.SourceGeneration.Tests94,Opc.Ua.Server.Tests3 851. On net48:Opc.Ua.Types.Tests8 383 andOpc.Ua.WotCon.Tests823.Base branch merge
The live NodeManager lifecycle work (#4094) has landed in
masterand has been merged in; this PR is now based onmaster. The upstream NodeManager lifecycle interface consolidation was adopted (IMonitoredItemLifecycle→IDetachableMonitoredItem,INodeManagerMonitoredItemTrackerfolded intoISubscriptionMonitoredItemLifecycle,ExecuteMonitoredItemMutationAsyncmoved toIDynamicNodeManagerHost) while retaining this branch's shadow/immediate reload support;INodeManagerMonitoredItemRetirementTrackernow lives in its own file to match the new one-interface-per-file layout.Reported issues addressed
Raised by @barnstee while evaluating whether UA Edge Translator can replace its hand-written WoT-Connectivity node manager with this library. All four target the deprecated OPC 10100-1 v1.02 asset surface; the answers live in the registry-first materialization runtime and
Opc.Ua.WotCon.Bindingsthat this PR adds.uav:mapToNodeId/uav:mapToType/uav:mapByFieldPathper OPC 10101 §6.5.4, authored on the property affordance (never a form), withWotTargetMappingDescriptor,IWotTargetVariableResolverandWotStructuredFieldBinding.IWotNodeSetContributorruns per resource after conversion and before variable creation, forStructureTypes generated from a controller symbol table;IWotDocumentConverteris now also resolvable from DI.StatusCodepropagation,IChannelReconnectPolicybackoff on consecutive failures, and the standardmodv:pollingTimeper-affordance interval.IWotNodeSetResolver(recursive, ordered, unresolved namespaces reported) ships; no UA Cloud Library implementation, since that is a new external HTTP dependency and a deployment decision. That issue stays open for the resolver implementation itself.Review-round remediation (commit
3ea717089)An implementation review raised 11 findings (5 High, 6 Medium) and 12 review threads were open. All are addressed.
Correctness / robustness
IAtomicFileReplacecapability implemented byLocalFileSystemandVirtualFileSystem, rather than a newIFileSystemmember: that would break external implementers and is not expressible as a default interface member onnetstandard2.0. Blob storage now fails fast instead of silently degrading to a destructive write.ManagedSessioninstead of disposing it, so a failedForServerAsyncno longer closes the session other client facades are still using.ModbusTcpClientreconnects a faulted connection on the next transaction; backoff stays inPollingWotSubscriptionso there is only one retry loop.NodeSetComparerenforcesMaxXmlDepth, which was validated but never read, so deeply nested XML reports a diagnostic instead of overflowing the stack.WotMaterializationCoordinatorenforcesMaxResolverDocumentBytes, and decides namespace knownness from the live materialized closure because namespace-table entries outlive the NodeManagers that registered them.GetOrCreateGroupnormalizes names the way the registry service does; the resource output map is read under its gate.API
System.Nullablewrappers aroundINullabletypes across the WoT and registry surface, includingTrySelectProjectionRoot.WotThingCatalog, and the synchronous core only performs lookups — so the Roslyn source generator stays synchronous and no sync-over-async is introduced.ExtensionObject.TryGetValueto decode through the codecs and reduced theVariantstructure helpers to delegation, since the context-taking API already existed.NodeSetComparer.CompareXmltakesReadOnlySpan<byte>instead ofbyte[].Layout
samples/WotCon/with theWotprefix dropped from folder, project, assembly and namespace names.NodeSetComparermoved next to the other NodeSet code inOpc.Ua.Export.MemoryWotBindingmoved out of the shipping bindings library into the aggregation sample and the test project.Sync-WotConModels.ps1(belongs in the drafts repository); renamed the pinned NodeSet2 files toWotConcasing; reverted a blank-line-only change inSession.cs.Server lifecycle
RequestManagerinto an optional registerable extension. With no extension registered the behaviour is exactly master's. The AsyncLocal scope nesting stays inRequestManager: it fixes scope propagation for the pre-existing scopes rather than adding a feature.StandardServerdisposal is documented and idempotent.Subscription/Sessionand server-shutdown threads asked for investigation and proposals before changes, so those are analysed and answered on the threads rather than rewritten here.Validation: full
UA.slnxbuilds clean on net10.0 (0 warnings) and net48 (0 errors). Suite totals on net10.0:Opc.Ua.Types.Tests8 408,Opc.Ua.Server.Tests3 978,Opc.Ua.WotCon.Tests920,Opc.Ua.WotCon.Bindings.Tests512,Opc.Ua.SourceGeneration.Core.Tests3 743,Opc.Ua.SourceGeneration.Tests112,Opc.Ua.XRegistry.Tests171,Opc.Ua.Di.Tests310,Opc.Ua.ISA95.Tests137.Server shutdown hardening (commit
46e5e5f2d)Follow-up to the
StandardServer.csreview thread, which asked for a thorough review of the server lifecycle handling with proposals before merge. The structural proposals remain open on that thread and no refactor was done; what landed here are the concrete defects that are wrong under any of those proposals:m_shutdownCoordinationLockwhileShutdownServerInternalAsyncnow starts outside it. Previously every statement before its firstawaitexecuted under the lock.ActiveTaskis still published inside the lock, so concurrent callers keep joining a single shutdown.ServerErrorand a source-generated log instead of only being recorded on the shutdown state.Final node-manager cleanup deliberately keeps
CancellationToken.None: cancelling it would leak retired lifecycle state, node managers and the server semaphore after request admission has already been closed permanently. Those call sites are now commented to make the intent explicit.Opc.Ua.Server.Tests: 3 980 passing, 0 warnings.Open review threads
Three threads are intentionally left unresolved because they ask for a joint decision rather than a specific change, and each has a full written analysis posted:
StandardServer.cs— three ranked structural proposals for the shutdown coordinator, awaiting a direction.Subscription.cs— a four-capability inventory of what this PR adds versus whatmasteralready supports, with a keep/refactor/revert recommendation per capability.IMonitoredItem.cs— answered: immediate generation retirement is the one scenario master's detach/park/recover lifecycle cannot express.