From 18a30770df94ceaf1ec9fa6f9af0f03802da0164 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 12 Aug 2026 11:17:54 +0200 Subject: [PATCH] Make the message-extension seam internal to Opc.Ua.Core The seam is inline-only. Checking every one of its symbols against Opc.Ua.Bindings.Quic gives zero references to ISecureChannelMessageHost, ISecureChannelMessageExtension, SequenceNumberBudget, the send gate and UaSCSecureChannelRegistry alike - a QUIC data channel rides its own stream and never becomes a UASC chunk, so none of it applies. The seam therefore exists to keep the engine out of Core, not to invite other implementers: there is exactly one, and no prospect of a second. Publishing it would commit Core to an abstraction with a single implementer for ever. Core already grants InternalsVisibleTo to its sibling first-party assemblies - Opc.Ua.Client, Opc.Ua.Bindings.Quic, Opc.Ua.Bindings.Https, Opc.Ua.Core.Diagnostics - and Opc.Ua.Core.Channels had simply never been added to that list, which is the only reason the seam had to be public. Adding it lets the following become internal with no code moving at all: * ISecureChannelMessageHost and ISecureChannelMessageExtension * SequenceNumberBudget, and UaSCUaBinaryChannel.SequenceBudget * RegisterMessageExtension and TryGetMessageExtension * DataChannelExtension and TryGetDataChannelExtension, which are the inline adapter rather than API - a consumer reaches the channels through EnableDataChannels and GetDataChannels, which return the engine UaSCSecureChannelRegistry stays public deliberately. An application writing its own IServerDataChannelTransport for inline framing needs it to resolve the channel behind a request, which samples/ConsoleDataChannelStreaming demonstrates; making it internal would break a real extension point to tidy an API surface. The public surface this PR adds to Opc.Ua.Core is now UaSCSecureChannelRegistry and IUaSCSecureChannelBoundTransport, the latter being a transport extension point alongside the existing public IUaSCByteTransport. This captures the main benefit of folding the engine back into Core - not owing compatibility on a single-implementer abstraction - without moving 4866 lines of experimental engine into Core on every TFM from net472 upward, and without touching the send gate or the sequence budget, both of which are inherent to inline framing wherever its code lives. UA.slnx builds pinned to net10.0, 316 data channel tests pass, and Opc.Ua.Server.Tests is unchanged at 4072. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5d026b47-265d-4c97-8715-26e207dfa03c --- docs/DataChannels.md | 21 ++++++++++++++----- .../DataChannels/DataChannelExtension.cs | 2 +- .../UaSCDataChannelExtensions.cs | 8 ++++++- src/Opc.Ua.Core/Opc.Ua.Core.csproj | 1 + .../Tcp/ISecureChannelMessageExtension.cs | 4 ++-- .../Stack/Tcp/SequenceNumberBudget.cs | 2 +- .../UaSCBinaryChannel.MessageExtensions.cs | 6 +++--- 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/DataChannels.md b/docs/DataChannels.md index 6fd97c8b5d..2c0a321ae5 100644 --- a/docs/DataChannels.md +++ b/docs/DataChannels.md @@ -56,14 +56,25 @@ sequence-checks every chunk before the engine sees it. Core itself carries no da vocabulary — `ISecureChannelMessageExtension` and `ISecureChannelMessageHost` name only "a MessageType that is neither a Service call nor part of establishing the SecureChannel". +Both are `internal`, reached through the `InternalsVisibleTo` that Core already grants its +sibling first-party assemblies (`Opc.Ua.Client`, `Opc.Ua.Bindings.Quic`, +`Opc.Ua.Bindings.Https`). The seam exists to keep the engine out of Core, not to invite +other implementers — there is exactly one, and the `opc.quic` binding does not use any of +it, because a QUIC data channel rides its own stream and never becomes a UASC chunk. +Publishing it would commit Core to an abstraction with one implementer for ever. + Two things stay in Core because they are the SecureChannel's own concerns rather than the feature's: `SequenceNumberBudget`, which tracks the sequence space every MessageType draws on, and `UaSCSecureChannelRegistry`, which maps a SecureChannel identifier to the channel -that owns it. Because the budget is Core's, the channel claims the SequenceNumber itself -while it secures the chunk and refuses the send with `Bad_SecureChannelTokenUnknown` when -the space under the current token is exhausted — an extension stalls rather than reuse a -number, and never has to reason about the serialization to do so. The `opc.quic` transport -remains its own package, `OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic`. +that owns it. The budget is internal for the same reason as the seam; the registry is +public because an application writing its own `IServerDataChannelTransport` for inline +framing needs it to resolve the channel behind a request, which +`samples/ConsoleDataChannelStreaming` demonstrates. Because the budget is Core's, the +channel claims the SequenceNumber itself while it secures the chunk and refuses the send +with `Bad_SecureChannelTokenUnknown` when the space under the current token is exhausted — +an extension stalls rather than reuse a number, and never has to reason about the +serialization to do so. The `opc.quic` transport remains its own package, +`OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic`. ## Server side diff --git a/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs index f8da82d827..00b39b014d 100644 --- a/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs +++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs @@ -42,7 +42,7 @@ namespace Opc.Ua.Bindings /// This owns the STR MessageType on the channel and is the only thing that /// knows the frame format; the channel itself sees an opaque body. /// - public sealed class DataChannelExtension : ISecureChannelMessageExtension, IDataChannelTransport + internal sealed class DataChannelExtension : ISecureChannelMessageExtension, IDataChannelTransport { /// /// Attaches data channels to a SecureChannel. diff --git a/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs index 1a39843ed7..f648992301 100644 --- a/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs +++ b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs @@ -111,10 +111,16 @@ namespace Opc.Ua.Bindings /// Returns the extension that owns the STR MessageType on a /// SecureChannel, if one is registered. /// + /// + /// The extension is an implementation detail of the inline framing: + /// a consumer reaches the channels through + /// , which returns the engine rather + /// than the adapter that drives it. + /// /// The SecureChannel. /// The extension. /// is null. - public static bool TryGetDataChannelExtension( + internal static bool TryGetDataChannelExtension( this UaSCUaBinaryChannel channel, out DataChannelExtension? extension) { diff --git a/src/Opc.Ua.Core/Opc.Ua.Core.csproj b/src/Opc.Ua.Core/Opc.Ua.Core.csproj index 20b2c981e0..449293d2e3 100644 --- a/src/Opc.Ua.Core/Opc.Ua.Core.csproj +++ b/src/Opc.Ua.Core/Opc.Ua.Core.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs index f30316b422..05c972907a 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs @@ -43,7 +43,7 @@ namespace Opc.Ua.Bindings /// from the channel's single SequenceNumber space. This is the surface that /// makes that possible without publishing the channel's internals. /// - public interface ISecureChannelMessageHost + internal interface ISecureChannelMessageHost { /// /// The largest chunk the peer accepts. @@ -114,7 +114,7 @@ namespace Opc.Ua.Bindings /// faults, which is what OPC 10000-6 §6.7.2.2 requires of a receiver that /// does not implement it. /// - public interface ISecureChannelMessageExtension + internal interface ISecureChannelMessageExtension { /// /// The MessageType this extension owns, without the chunk type. diff --git a/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs index 249220b916..e2d1703716 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs @@ -64,7 +64,7 @@ namespace Opc.Ua.Bindings /// than emitting a chunk that would reuse a value. /// /// - public sealed class SequenceNumberBudget + internal sealed class SequenceNumberBudget { /// /// Creates a budget. diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs index e6d686435b..1711120965 100644 --- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs +++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs @@ -60,7 +60,7 @@ namespace Opc.Ua.Bindings /// under the current SecurityToken. Every MessageType the channel /// carries draws on the same space. /// - public SequenceNumberBudget SequenceBudget + internal SequenceNumberBudget SequenceBudget { get { @@ -89,7 +89,7 @@ namespace Opc.Ua.Bindings /// The registered extension, which is /// unless another was registered first. /// is null. - public ISecureChannelMessageExtension RegisterMessageExtension( + internal ISecureChannelMessageExtension RegisterMessageExtension( ISecureChannelMessageExtension extension) { if (extension == null) @@ -105,7 +105,7 @@ namespace Opc.Ua.Bindings /// /// The MessageType, without the chunk type. /// The extension. - public bool TryGetMessageExtension( + internal bool TryGetMessageExtension( uint messageType, out ISecureChannelMessageExtension? extension) {