Skip to content

[WotCon] Add the WoT protocol binding abstractions and planners - #4140

Open
marcschier wants to merge 19 commits into
marcschier/wot-10-wotcon-modelfrom
marcschier/wot-11-bindings-core
Open

[WotCon] Add the WoT protocol binding abstractions and planners#4140
marcschier wants to merge 19 commits into
marcschier/wot-10-wotcon-modelfrom
marcschier/wot-11-bindings-core

Conversation

@marcschier

Copy link
Copy Markdown
Collaborator

Summary

PR 11 of the stacked split for integration PR #4093. This adds the protocol-agnostic core of Opc.Ua.WotCon.Bindings: binder, planner, executor and channel-factory contracts; payload codec and credential provider seams; the binding plan model; polling subscriptions for forms without native observation; and DI/builder registration helpers.

This PR is stacked on #4139, which itself depends on #4132 and #4134.

Scope

The included planners validate forms and produce binding plans without transport I/O:

  • HTTP planner and validation only; the concrete HTTP executor follows in the next PR.
  • MQTT planner and validation only; the separate MQTT executor package follows in the next PR.
  • Modbus TCP planner and shared Modbus type/limit definitions only; the Modbus executor and TCP client follow in the next PR.
  • OPC UA planner and validation only; the concrete OPC UA executor follows in the next PR.
  • CoAP, BACnet, PROFINET and LoRaWAN are planner/validation-only in this split.

Keeping concrete transport executors out of this PR makes the abstractions and planning layer reviewable without a network stack.

Validation

  • dotnet build tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj -c Release -p:CustomTestTarget=net10.0 -v:m
  • dotnet test tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj -c Release -p:CustomTestTarget=net10.0 --no-build
  • dotnet build src\Opc.Ua.WotCon.Bindings\Opc.Ua.WotCon.Bindings.csproj -c Release -v:m

Introduces the protocol-agnostic half of the WoT binding runtime: the binder,
planner, executor and channel factory contracts, the payload codec and
credential provider seams, the binding plan model, and the polling subscription
that drives forms without native observation.

Eight protocol planners are included - HTTP, CoAP, MQTT, Modbus TCP, BACnet,
PROFINET, LoRaWAN and OPC UA. A planner validates a form and produces a binding
plan; it performs no transport I/O, so this layer can be reviewed and tested
without any network stack. The concrete executors follow separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
The fluent builder generator emits calls to INodeManagerBuilder.VariableFromDataTypeId
and to a NodeManagerBuilder constructor overload that carries the data-type lookup, but
the runtime side of that API was missing, so every generated node manager failed to
compile with CS1729 and CS1061.

Adds the VariableFromDataTypeId resolution to the builder interface and implementation,
along with NodeStateLookupExtensions.FindByDataType, which is the lookup the builder
delegates to and has no other consumer.

Resolution reports BadNodeIdInvalid for a null data type, BadNodeIdUnknown when nothing
matches, BadBrowseNameDuplicated when the match is ambiguous, and BadTypeMismatch when
the resolved node is not a variable. An optional browse name disambiguates a data type
that is carried by more than one variable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
@marcschier

Copy link
Copy Markdown
Collaborator Author

Fixed the net10.0 solution build failure on this PR.

This branch carries the FluentBuilderGenerator change, which emits calls to
INodeManagerBuilder.VariableFromDataTypeId and to a NodeManagerBuilder constructor
overload carrying the data-type lookup. The runtime side of that API was not present on
any branch in the stack - it had been grouped into a later server PR that does not exist
yet - so every generated node manager failed to compile:

BoilerNodeManager.NodeManager.g.cs(97,31): CS1729: 'NodeManagerBuilder' does not
  contain a constructor that takes 7 arguments
BoilerNodeManager.FluentBuilders.g.cs(140,24): CS1061: 'INodeManagerBuilder' does not
  contain a definition for 'VariableFromDataTypeId'

The fix lands in #4132 (where the generator change belongs) and is cherry-picked here so
this branch builds standalone. It adds VariableFromDataTypeId to the builder interface
and implementation, NodeStateLookupExtensions.FindByDataType, and eight tests covering
the resolution rules.

Verified locally: MinimalBoilerServer - one of the two projects failing in CI - builds
clean on this branch.

marcschier and others added 3 commits July 31, 2026 14:57
DiNodeManager constructed NodeManagerBuilder without the data-type resolver, so
VariableFromDataTypeId reported BadNodeIdUnknown ("no predefined variable has DataType")
for every DI node manager - a misleading error, since the lookup had simply never
been supplied rather than the variable being absent.

Delegates to NodeStateLookupExtensions.FindByDataType rather than hand-rolling the
scan a fourth time in this file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…bindings-core

# Conflicts:
#	src/Opc.Ua.Di.Server/DiNodeManager.cs
Thing Descriptions are remote-supplied, so the host in a form's href was an
unvalidated outbound request target: the executors would connect to loopback,
link-local and private-range addresses, including the cloud instance metadata
service, and return the response body to the caller as a readable value.

Adds WotEndpointPolicy and WotEndpointValidator and enforces them in
WotProtocolBinderRegistry.OpenChannelAsync, the single point through which every
executor opens a channel. Loopback and private ranges are denied by default and
can be re-enabled per deployment. As with the asset endpoint validator, DNS is
deliberately not resolved during validation, because resolving at validation time
and again at connect time is itself a request-forgery vector.

Also rejects control characters in a form's declared content type, which could
otherwise be injected verbatim into outbound request headers, and MQTT topic
wildcards, which would subscribe the server to an entire broker namespace under
its own identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
@marcschier

Copy link
Copy Markdown
Collaborator Author

Security fixes pushed in 17209eda1:

  • Added WotEndpointPolicy / WotEndpointValidator in the bindings assembly and enforce them in WotProtocolBinderRegistry.OpenChannelAsync before executor activation. Loopback, localhost aliases, RFC1918, 169.254/16 (including IMDS), 100.64/10, IPv6 ULA/link-local, and IPv4-mapped IPv6 literals are denied by default; DNS is intentionally not resolved during validation.
  • Added DI registration for the endpoint policy so deployments can explicitly relax the defaults.
  • Reject unsafe WoT contentType values containing CR, LF, NUL, or non-ASCII at planning time.
  • Reject MQTT # / + topics at planning time unless WotBindingBounds.AllowMqttWildcardTopics is explicitly enabled.

Follow-up note: once Opc.Ua.WotCon.Server can reference the bindings assembly in a later PR, Opc.Ua.WotCon.Server.Assets.AssetEndpointValidator should delegate to this shared implementation to avoid drift.

Verification:

  • dotnet test tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj -p:CustomTestTarget=net10.0 --logger "console;verbosity=minimal" => 310 passed.
  • dotnet test tests\Opc.Ua.WotCon.Bindings.Tests\Opc.Ua.WotCon.Bindings.Tests.csproj -p:CustomTestTarget=net48 --logger "console;verbosity=minimal" => project skipped by its .NET 8+ guard for net48.

marcschier and others added 6 commits July 31, 2026 19:53
The Opc.Ua.WotCon.Bindings library and its test project were added without being
listed in UA.slnx, so the solution build never compiled them and their tests never
ran in CI. Registering both restores that coverage: 310 binding tests now execute.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
An empty collection expression binds to ImmutableDictionary<TKey, TValue> only on
the newest framework, so the binding plan, credential and plan-model types failed
to compile on net9.0 and below. Naming the Empty singleton explicitly works on
every target.

This was invisible until the project was added to the solution, because nothing
built it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Copilot AI review requested due to automatic review settings August 1, 2026 16:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the protocol-agnostic core of WoT Connectivity protocol bindings by adding a new Opc.Ua.WotCon.Bindings library (binder/planner/registry abstractions + planners/validators) and a matching test project, and wires both into the main solution.

Changes:

  • Add src/Opc.Ua.WotCon.Bindings with binder identity/capability models, plan model, diagnostics, endpoint policy/validator, registry, codecs, and polling subscription helper.
  • Add planner/validator implementations for multiple protocols (HTTP, CoAP, MQTT, Modbus TCP, BACnet, PROFINET, LoRaWAN, OPC UA).
  • Add tests/Opc.Ua.WotCon.Bindings.Tests and include both projects in UA.slnx.

Reviewed changes

Copilot reviewed 53 out of 53 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
UA.slnx Adds the new bindings library and its test project to the solution.
src/Opc.Ua.WotCon.Bindings/Opc.Ua.WotCon.Bindings.csproj New bindings/planners package project definition and TFM-conditional compilation.
src/Opc.Ua.WotCon.Bindings/NugetREADME.md Package readme describing scope, TFMs, and executor/planner split.
src/Opc.Ua.WotCon.Bindings/Properties/AssemblyInfo.cs Assembly-level attributes for the new bindings library.
src/Opc.Ua.WotCon.Bindings/OpcUaWotBindingBuilderExtensions.cs DI/builder registration helpers for binders/executors/credentials/policy.
src/Opc.Ua.WotCon.Bindings/IWotBinderRegistry.cs Registry seam + Null registry baseline implementation.
src/Opc.Ua.WotCon.Bindings/IWotBindingChannelFactory.cs Narrow runtime seam for opening channels for executable compiled forms.
src/Opc.Ua.WotCon.Bindings/IWotBindingExecutor.cs Executor/channel/subscription contracts + operation result models.
src/Opc.Ua.WotCon.Bindings/IWotBindingPlanner.cs Planning context + compiled form model + compilation result model.
src/Opc.Ua.WotCon.Bindings/IWotProtocolBinder.cs Core binder interface + shared binder base with common validation helpers.
src/Opc.Ua.WotCon.Bindings/IWotCredentialProvider.cs Credential reference/provider seam (secret-free references).
src/Opc.Ua.WotCon.Bindings/IWotPayloadCodec.cs Payload codec + codec registry and built-in codecs (JSON/text/octet-stream).
src/Opc.Ua.WotCon.Bindings/WotAffordanceForm.cs Immutable extracted form model with pointer helpers and typed term accessors.
src/Opc.Ua.WotCon.Bindings/WotFormExtractor.cs Extracts forms/security/ops defaults from TD/TM JSON without transport I/O.
src/Opc.Ua.WotCon.Bindings/WotBindingPlan.cs Plan request builder from document + immutable plan result container.
src/Opc.Ua.WotCon.Bindings/WotBindingPlanModel.cs Transport-neutral descriptors (endpoint/addressing/operation/payload) and op mapping helpers.
src/Opc.Ua.WotCon.Bindings/WotBindingBounds.cs Central safety bounds applied during planning/execution.
src/Opc.Ua.WotCon.Bindings/WotBindingIdentity.cs Binder identity model with stable id@version key semantics.
src/Opc.Ua.WotCon.Bindings/WotBindingIdentification.cs Matching model for deterministic binder selection + selection context pins.
src/Opc.Ua.WotCon.Bindings/WotBindingCapability.cs Browseable capability snapshot model projected to generated UA datatype.
src/Opc.Ua.WotCon.Bindings/WotBindingDiagnostic.cs Structured diagnostics with stable codes + JSON pointers.
src/Opc.Ua.WotCon.Bindings/WotBindingMaturity.cs Spec maturity enum + pinned spec source descriptor.
src/Opc.Ua.WotCon.Bindings/WotEndpointPolicy.cs Operator-configurable endpoint allow/deny policy.
src/Opc.Ua.WotCon.Bindings/WotEndpointValidator.cs Fail-closed endpoint validation (scheme/host/IP-literal checks) without DNS resolution.
src/Opc.Ua.WotCon.Bindings/WotProtocolBinderRegistry.cs Aggregating registry: selection, planning, target-mapping validation, executability gating, activation tracking, channel opening.
src/Opc.Ua.WotCon.Bindings/PollingWotSubscription.cs Polling-based observe/event subscription helper with retry/backoff integration.
src/Opc.Ua.WotCon.Bindings/ModbusDataTypes.cs Modbus scalar type normalization and register-width mapping.
src/Opc.Ua.WotCon.Bindings/ModbusProtocolLimits.cs Shared Modbus protocol limit constants.
src/Opc.Ua.WotCon.Bindings/Planners/WotBuiltInBinders.cs Factory for the eight built-in planner binders.
src/Opc.Ua.WotCon.Bindings/Planners/WotBindingSources.cs Pinned source catalog for each shipped binding/planner.
src/Opc.Ua.WotCon.Bindings/Planners/HttpBindingPlanner.cs HTTP planner/validator and compiled-form production.
src/Opc.Ua.WotCon.Bindings/Planners/CoapBindingPlanner.cs CoAP planner/validator (non-executable in this split).
src/Opc.Ua.WotCon.Bindings/Planners/MqttBindingPlanner.cs MQTT planner/validator and addressing compilation.
src/Opc.Ua.WotCon.Bindings/Planners/ModbusBindingPlanner.cs Modbus planner/validator and addressing/operation compilation.
src/Opc.Ua.WotCon.Bindings/Planners/BacnetBindingPlanner.cs BACnet planner/validator (non-executable in this split).
src/Opc.Ua.WotCon.Bindings/Planners/ProfinetBindingPlanner.cs PROFINET planner/validator (non-executable in this split).
src/Opc.Ua.WotCon.Bindings/Planners/LoRaWanBindingPlanner.cs LoRaWAN planner/validator (non-executable in this split).
src/Opc.Ua.WotCon.Bindings/Planners/OpcUaBindingPlanner.cs OPC UA planner/validator and NodeId/endpoint addressing compilation.
tests/Opc.Ua.WotCon.Bindings.Tests/Opc.Ua.WotCon.Bindings.Tests.csproj New test project for bindings/planners/registry.
tests/Opc.Ua.WotCon.Bindings.Tests/Properties/AssemblyInfo.cs Assembly-level attributes for the new test assembly.
tests/Opc.Ua.WotCon.Bindings.Tests/WotProtocolBinderBaseAdditionalTests.cs Additional binder-base helper coverage (ops/security/href bounds).
tests/Opc.Ua.WotCon.Bindings.Tests/WotEndpointValidatorTests.cs Endpoint policy/validator coverage (SSRF fail-closed behavior).
tests/Opc.Ua.WotCon.Bindings.Tests/WotBinderRegistryDiTests.cs DI wiring tests for EnsureWotBinderRegistry semantics.
tests/Opc.Ua.WotCon.Bindings.Tests/PollingWotSubscriptionTests.cs Polling subscription retry/backoff + disposal semantics tests.
tests/Opc.Ua.WotCon.Bindings.Tests/OpcUaWotBindingBuilderExtensionsTests.cs Builder extension registration tests.
tests/Opc.Ua.WotCon.Bindings.Tests/ModbusBindingPlannerAdditionalTests.cs Additional Modbus planner coverage (pollingTime, function codes, unit-id parsing).
Suppressed comments (2)

src/Opc.Ua.WotCon.Bindings/OpcUaWotBindingBuilderExtensions.cs:89

  • ServiceDescriptor.Singleton(binder) registers only the concrete binder type, not IWotProtocolBinder. This prevents WotProtocolBinderRegistry (constructed from GetServices()) from seeing custom binders added via AddWotBinder.
            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton(binder));
            EnsureRegistry(builder.Services);
            return builder;

src/Opc.Ua.WotCon.Bindings/OpcUaWotBindingBuilderExtensions.cs:129

  • AddWotCredentialProvider uses AddSingleton(provider), which registers the provider under its concrete type only. EnsureWotBinderRegistry resolves IWotCredentialProvider, so a custom provider added here won't be used (registry falls back to NullWotCredentialProvider).
        public static IOpcUaBuilder AddWotCredentialProvider(
            this IOpcUaBuilder builder, IWotCredentialProvider provider)
        {
            if (builder is null)
            {
                throw new ArgumentNullException(nameof(builder));
            }
            if (provider is null)
            {
                throw new ArgumentNullException(nameof(provider));
            }
            builder.Services.AddSingleton(provider);
            return builder;
        }

Comment thread src/Opc.Ua.WotCon.Bindings/OpcUaWotBindingBuilderExtensions.cs
Comment thread src/Opc.Ua.WotCon.Bindings/OpcUaWotBindingBuilderExtensions.cs Outdated
Comment thread src/Opc.Ua.WotCon.Bindings/WotEndpointPolicy.cs
Comment thread src/Opc.Ua.WotCon.Bindings/PollingWotSubscription.cs
marcschier and others added 2 commits August 1, 2026 18:35
The generator now emits a distinct handler delegate and result type per declaring
type, so a method that appears on more than one type no longer shares a single
generic name. CloseAndUpdate on the WoT file and the five WoTAssetConnectionManagement
methods each have their own type now, and the server still referenced the generic
names, so it no longer compiled.

Points the asset file manager at the WoT file's CloseAndUpdate handler and the
connectivity node manager at the management type's result types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
…bindings-core

# Conflicts:
#	src/Opc.Ua.WotCon.Server/Assets/WotAssetFileManager.cs
Register WoT binders and executors explicitly under their interfaces so registry resolution does not depend on overload inference. Correct the endpoint allow-list documentation to reflect that scheme and address-range checks still apply, and make polling subscription disposal best-effort for residual non-OOM loop faults while logging them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Replace the public PollingWotSubscription logger parameter with optional ITelemetryContext so disposal diagnostics follow the repository observability convention. Carry telemetry through WotExecutorContext and the DI-created binder registry so executors can pass the runtime context into polling subscriptions without introducing a bare ILogger API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Reading TelemetryExtensions.InternalOnly__TelemetryHook from a binding assembly
reaches for an API whose name reserves it for the core to install a process-wide
default. Fall back to AmbientMessageContext.Telemetry instead, matching how
Subscription resolves an optional telemetry context, and let the null-tolerant
telemetry extension methods supply the default logger factory when no context is
in scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants