diff --git a/.azurepipelines/expected-packages.txt b/.azurepipelines/expected-packages.txt
index 78da758690..ef913cc1b3 100644
--- a/.azurepipelines/expected-packages.txt
+++ b/.azurepipelines/expected-packages.txt
@@ -15,10 +15,12 @@
# package ids with this file in the same pull request that changes them.
OPCFoundation.NetStandard.Opc.Ua.Bindings.Https
+OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic
OPCFoundation.NetStandard.Opc.Ua.Client
OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes
OPCFoundation.NetStandard.Opc.Ua.Configuration
OPCFoundation.NetStandard.Opc.Ua.Core
+OPCFoundation.NetStandard.Opc.Ua.Core.Channels
OPCFoundation.NetStandard.Opc.Ua.Core.Diagnostics
OPCFoundation.NetStandard.Opc.Ua.Core.Schema
OPCFoundation.NetStandard.Opc.Ua.Core.Types
diff --git a/docs/DataChannels.md b/docs/DataChannels.md
index 95addfd2fb..d09baba4f8 100644
--- a/docs/DataChannels.md
+++ b/docs/DataChannels.md
@@ -36,14 +36,32 @@ it, and a peer that does use it never speaks first: it may not transmit a frame
because an unrecognized `MessageType` is a protocol error that closes the SecureChannel,
taking every Session, Subscription and Service call with it.
-Consumers opt in by suppressing the experimental diagnostic:
+Consumers opt in by referencing the package and suppressing the experimental diagnostic:
```xml
+
+
+
$(NoWarn);DataChannels
```
+## Where the code lives
+
+The engine ships in `OPCFoundation.NetStandard.Opc.Ua.Core.Channels` rather than in
+`Opc.Ua.Core`. It is a consumer of the message-extension seam in Core: it registers as the
+owner of the `STR` MessageType, and the SecureChannel decrypts, verifies and
+sequence-checks every chunk before the engine sees it. Core itself carries no data channel
+vocabulary โ `ISecureChannelMessageExtension` and `ISecureChannelMessageHost` name only "a
+MessageType that is neither a Service call nor part of establishing the SecureChannel".
+
+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. The `opc.quic` transport remains its own package,
+`OPCFoundation.NetStandard.Opc.Ua.Bindings.Quic`.
+
## Server side
```csharp
@@ -253,8 +271,8 @@ second. The sample measured 0.5 Mbit/s before the fix and 1.3 Gbit/s after it, a
The suite is 284 tests on `net10.0` and 212 tests on `net48` over
`tests/Opc.Ua.Core.DataChannels.Tests`, covering
-`Stack/DataChannels/**`, `Stack/Tcp/UaSCBinaryChannel.DataChannels.cs`,
-`Server/InlineServerDataChannelTransport.cs` and `Opc.Ua.Bindings.Quic`. Coverage was
+`Opc.Ua.Core.Channels`, `Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs`
+and `Opc.Ua.Bindings.Quic`. Coverage was
**80.1%** at 255 tests, having fallen from 87.6% while the test count rose, because
wiring the previously-uncalled obligations added a good deal of
production code โ the server-side Service dispatch, the stream mapping, the
diff --git a/docs/README.md b/docs/README.md
index 16a4cb90d1..2ba0ebedf3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -27,7 +27,7 @@ Here is a list of available documentation for different topics:
* Pluggable [Identity Providers](IdentityProviders.md) โ interfaces (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`) plus the OPC 10000-6 ยง6.5.2.2 `IssuerEndpointUrl` JSON parser for OAuth2 / OIDC / Entra / JWT flows.
* Support for [ECC Certificates](EccProfiles.md).
* Working with [ComplexTypes](ComplexTypes.md) - Custom structures and enumerations.
-* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Identifiers are provisional and the errata is not endorsed by the OPC Foundation.
+* [Data Channels](DataChannels.md) - **Experimental.** Inline streaming of opaque bytes over a SecureChannel: the `STR` frame, per-channel flow control and scheduling, and the `OpenDataChannel` / `ModifyDataChannel` / `CloseDataChannel` Service Set. Ships as `OPCFoundation.NetStandard.Opc.Ua.Core.Channels`, attached to the SecureChannel through the message-extension seam in `Opc.Ua.Core`. Identifiers are provisional and the errata is not endorsed by the OPC Foundation.
* Client-based [NodeSet Export](NodeSetExport.md) - Export server address space to NodeSet2 XML.
* Source generated [DataTypes] - How to annotate POCO classes and let the source generator generate the `IEncodeable` implementation.
* Runtime [Schema Generation](SchemaGeneration.md) - Produce XSD, OPC Binary (BSD) and JSON Schema (Part 6 Annex C, compact + verbose) for generated encodeable types and dynamically added complex types via the injectable `ISchemaProvider`; schemas are built as object models in code (trimmable, NativeAOT compatible).
diff --git a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj
index 435078bee1..2f4c4ff181 100644
--- a/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj
+++ b/samples/ConsoleDataChannelStreaming/ConsoleDataChannelStreaming.csproj
@@ -20,6 +20,7 @@
+
diff --git a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs
index aadbc8a43f..4dea07f9fc 100644
--- a/samples/ConsoleDataChannelStreaming/StreamingHarness.cs
+++ b/samples/ConsoleDataChannelStreaming/StreamingHarness.cs
@@ -365,7 +365,7 @@ namespace ConsoleDataChannelStreaming
}
else
{
- clientManager = clientChannel.EnableDataChannels(
+ clientManager = clientChannel.SecureChannel!.EnableDataChannels(
isServer: false,
telemetry,
maxDataChannels: 16,
@@ -789,7 +789,7 @@ namespace ConsoleDataChannelStreaming
out uint maxFrameSize,
out bool isReliable)
{
- if (!UaSCDataChannelSecureChannelRegistry.TryGet(
+ if (!UaSCSecureChannelRegistry.TryGet(
secureChannelContext.SecureChannelId,
out UaSCUaBinaryChannel? channel) ||
channel == null)
diff --git a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj
index 0c21ca8919..f48ce9fd6e 100644
--- a/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj
+++ b/src/Opc.Ua.Bindings.Quic/Opc.Ua.Bindings.Quic.csproj
@@ -23,6 +23,7 @@
+
diff --git a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs
index fcd47a3e29..8ee1bd9325 100644
--- a/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs
+++ b/src/Opc.Ua.Bindings.Quic/QuicMultiplexedTransport.cs
@@ -129,7 +129,7 @@ namespace Opc.Ua.Bindings
public TransportChannelFeatures Features
=> TransportChannelFeatures.Reconnect |
TransportChannelFeatures.ReverseConnect |
- TransportChannelFeatures.DataChannels |
+ TransportChannelFeatures.MessageExtensions |
TransportChannelFeatures.MultiplexedStreams;
///
diff --git a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs
index 2c4dbd69e6..8de69e38fe 100644
--- a/src/Opc.Ua.Client/Session/ManagedSession.Services.cs
+++ b/src/Opc.Ua.Client/Session/ManagedSession.Services.cs
@@ -610,8 +610,8 @@ namespace Opc.Ua.Client
///
#if NET8_0_OR_GREATER
- [Experimental(DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = DataChannelConstants.ExperimentalUrlFormat)]
+ [Experimental(DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask OpenDataChannelAsync(
RequestHeader? requestHeader,
@@ -635,8 +635,8 @@ namespace Opc.Ua.Client
///
#if NET8_0_OR_GREATER
- [Experimental(DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = DataChannelConstants.ExperimentalUrlFormat)]
+ [Experimental(DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask ModifyDataChannelAsync(
RequestHeader? requestHeader,
@@ -656,8 +656,8 @@ namespace Opc.Ua.Client
///
#if NET8_0_OR_GREATER
- [Experimental(DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = DataChannelConstants.ExperimentalUrlFormat)]
+ [Experimental(DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask CloseDataChannelAsync(
RequestHeader? requestHeader,
diff --git a/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs
new file mode 100644
index 0000000000..2013f8112d
--- /dev/null
+++ b/src/Opc.Ua.Core.Channels/ChannelsEventIds.cs
@@ -0,0 +1,46 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// The base EventIds each source-generated logging class in this assembly
+ /// counts from.
+ ///
+ ///
+ /// The values continue the block these messages used while the engine
+ /// lived in Opc.Ua.Core, so an operator's existing log filters keep
+ /// working across the move.
+ ///
+ internal static class ChannelsEventIds
+ {
+ public const int DataChannel = 600;
+ public const int DataChannelManager = 620;
+ }
+}
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannel.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannel.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs
similarity index 97%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs
index 3e18de533f..7fb71cedc9 100644
--- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelConstants.cs
+++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelConstants.cs
@@ -45,14 +45,13 @@ namespace Opc.Ua.Bindings
/// The diagnostic id suppressed by a consumer that opts in to the
/// experimental data channel surface.
///
- public const string ExperimentalDiagnosticId = "DataChannels";
+ public const string ExperimentalDiagnosticId = DataChannelFeature.ExperimentalDiagnosticId;
///
/// The documentation url reported alongside
/// .
///
- public const string ExperimentalUrlFormat =
- "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md";
+ public const string ExperimentalUrlFormat = DataChannelFeature.ExperimentalUrlFormat;
///
/// The connection control channel (Part 6 errata 5.6). Never
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs
similarity index 94%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs
index 19d6e52eeb..9fae0a7301 100644
--- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelExtension.cs
+++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelExtension.cs
@@ -65,7 +65,6 @@ namespace Opc.Ua.Bindings
{
m_host = host ?? throw new ArgumentNullException(nameof(host));
m_isSource = isServer;
- m_budget = new DataChannelSequenceBudget(host.TimeProvider);
// Cached so a frame does not allocate a closure on the send path.
m_onSecuring = ReserveSequenceNumber;
@@ -88,14 +87,7 @@ namespace Opc.Ua.Bindings
/// under the current token. STR, MSG, OPN and CLO chunks all draw on
/// the same space.
///
- public DataChannelSequenceBudget SequenceBudget
- {
- get
- {
- m_budget.ObserveConsumed(m_host.SequenceNumbersIssued);
- return m_budget;
- }
- }
+ public SequenceNumberBudget SequenceBudget => m_host.SequenceBudget;
///
/// Raised when a frame violated the framing rules badly enough to cost
@@ -145,7 +137,7 @@ namespace Opc.Ua.Bindings
ArraySegment body = message.Body;
if (!DataChannelFrameCodec.TryDecode(
- new ReadOnlyMemory(body.GetArray(), body.Offset, body.Count),
+ new ReadOnlyMemory(body.Array!, body.Offset, body.Count),
m_host.ReceiveBufferSize,
out DataChannelFrame frame,
out DataChannelFrameError error))
@@ -179,7 +171,8 @@ namespace Opc.Ua.Bindings
///
public void OnSecurityTokenActivated()
{
- m_budget.OnTokenActivated();
+ // The channel rebases the budget itself, because every MessageType
+ // it carries draws on the same space.
}
///
@@ -243,9 +236,7 @@ namespace Opc.Ua.Bindings
///
private void ReserveSequenceNumber()
{
- m_budget.ObserveConsumed(m_host.SequenceNumbersIssued);
-
- if (!m_budget.TryConsume())
+ if (!m_host.SequenceBudget.TryConsume())
{
throw ServiceResultException.Create(
StatusCodes.BadSecureChannelTokenUnknown,
@@ -254,7 +245,6 @@ namespace Opc.Ua.Bindings
}
private readonly ISecureChannelMessageHost m_host;
- private readonly DataChannelSequenceBudget m_budget;
private readonly Action m_onSecuring;
private readonly bool m_isSource;
}
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs
similarity index 84%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs
index a456d2b0de..9424cb24e0 100644
--- a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelLog.cs
+++ b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelLog.cs
@@ -37,23 +37,23 @@ namespace Opc.Ua.Bindings
///
internal static partial class DataChannelLog
{
- [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 0, Level = LogLevel.Warning,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 0, Level = LogLevel.Warning,
Message = "DataChannel: A frame named ChannelId {ChannelId}, which is not open on this SecureChannel.")]
public static partial void DataChannelManagerUnknownChannel(
this ILogger logger,
uint channelId);
- [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 1, Level = LogLevel.Error,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 1, Level = LogLevel.Error,
Message = "DataChannel: A connection level credit grant would overflow the window.")]
public static partial void DataChannelManagerCreditOverflow(this ILogger logger);
- [LoggerMessage(EventId = CoreEventIds.DataChannelManager + 2, Level = LogLevel.Error,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannelManager + 2, Level = LogLevel.Error,
Message = "DataChannel: The scheduler round faulted.")]
public static partial void DataChannelManagerSchedulerFault(
this ILogger logger,
Exception exception);
- [LoggerMessage(EventId = CoreEventIds.DataChannel + 0, Level = LogLevel.Debug,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 0, Level = LogLevel.Debug,
Message = "DataChannel {ChannelId}: entered {State} with {Status}.")]
public static partial void DataChannelStateChanged(
this ILogger logger,
@@ -61,14 +61,14 @@ namespace Opc.Ua.Bindings
DataChannelState state,
StatusCode status);
- [LoggerMessage(EventId = CoreEventIds.DataChannel + 1, Level = LogLevel.Warning,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 1, Level = LogLevel.Warning,
Message = "DataChannel {ChannelId}: rejected a frame, resetting with {Status}.")]
public static partial void DataChannelFrameRejected(
this ILogger logger,
uint channelId,
StatusCode status);
- [LoggerMessage(EventId = CoreEventIds.DataChannel + 2, Level = LogLevel.Warning,
+ [LoggerMessage(EventId = ChannelsEventIds.DataChannel + 2, Level = LogLevel.Warning,
Message = "DataChannel: a STR frame arrived but the data channel feature is not enabled.")]
public static partial void DataChannelFeatureDisabled(this ILogger logger);
}
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelManager.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelManager.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelMessage.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelMessage.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs b/src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/DataChannelSettings.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/DataChannelSettings.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs b/src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/FlowControl/DataChannelCredit.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/FlowControl/DataChannelCredit.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrame.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrame.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameCodec.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameCodec.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameError.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameError.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs b/src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Framing/DataChannelFrameType.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Framing/DataChannelFrameType.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/IDataChannelTransport.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/IDataChannelTransport.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs b/src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Model/DataChannelModel.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Model/DataChannelModel.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs b/src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Scheduling/DataChannelSendQueue.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Scheduling/DataChannelSendQueue.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelReceiveWindow.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelReceiveWindow.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs b/src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequence.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Sequencing/DataChannelSequence.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelNegotiator.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelNegotiator.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelServiceHandler.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelServiceHandler.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Services/DataChannelSourceRegistry.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Services/DataChannelSourceRegistry.cs
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs b/src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs
similarity index 100%
rename from src/Opc.Ua.Core/Stack/DataChannels/Services/QuicPeerBinding.cs
rename to src/Opc.Ua.Core.Channels/DataChannels/Services/QuicPeerBinding.cs
diff --git a/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs
new file mode 100644
index 0000000000..68cdefa0bf
--- /dev/null
+++ b/src/Opc.Ua.Core.Channels/IServerDataChannelTransport.cs
@@ -0,0 +1,109 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// The transport a Server binds an accepted OpenDataChannel to.
+ ///
+ ///
+ /// A data channel rides a transport that already exists, so the Service
+ /// Set needs a way to reach it from the SecureChannel a request arrived
+ /// on. Inline framing and the outer-protocol transports answer this
+ /// differently: inline multiplexes on the ChannelId in the frame header,
+ /// while opc.quic binds each channel to its own stream.
+ ///
+ public interface IServerDataChannelTransport
+ {
+ ///
+ /// Resolves the data channel engine for a SecureChannel, if this
+ /// transport carries it.
+ ///
+ /// The SecureChannel the request
+ /// arrived on.
+ /// What the Server advertises.
+ /// Telemetry context.
+ /// The engine that carries the channels.
+ /// The largest payload a frame may carry
+ /// on this transport.
+ /// False when frames can be genuinely lost
+ /// in flight.
+ bool TryGetManager(
+ SecureChannelContext secureChannelContext,
+ DataChannelServerCapabilities capabilities,
+ ITelemetryContext telemetry,
+ out DataChannelManager manager,
+ out uint maxFrameSize,
+ out bool isReliable);
+
+ ///
+ /// Opens the transport stream for a direction this peer initiates, and
+ /// returns the identifier to carry in revisedTransportChannelId.
+ ///
+ /// The SecureChannel.
+ /// The data channel.
+ /// The negotiated direction.
+ /// Cancellation token.
+ ValueTask AllocateServerStreamAsync(
+ SecureChannelContext secureChannelContext,
+ uint channelId,
+ DataChannelDirection direction,
+ CancellationToken ct);
+
+ ///
+ /// Binds a data channel to the transport stream the Client named.
+ ///
+ ///
+ /// The identifier is taken straight from the requester, so an
+ /// implementation validates it before it binds anything and refuses
+ /// rather than echoing a value it has not checked.
+ ///
+ /// The SecureChannel.
+ /// The data channel.
+ /// The identifier from transportChannelId.
+ /// The negotiated direction.
+ /// Cancellation token.
+ ValueTask BindClientStreamAsync(
+ SecureChannelContext secureChannelContext,
+ uint channelId,
+ ulong streamId,
+ DataChannelDirection direction,
+ CancellationToken ct);
+
+ ///
+ /// Tears down every data channel on a SecureChannel.
+ ///
+ /// The SecureChannel.
+ /// Why the channels cannot continue.
+ void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason);
+ }
+}
diff --git a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs
similarity index 95%
rename from src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs
rename to src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs
index 4e0bc67f69..b5d069c54d 100644
--- a/src/Opc.Ua.Server/Server/InlineServerDataChannelTransport.cs
+++ b/src/Opc.Ua.Core.Channels/InlineServerDataChannelTransport.cs
@@ -30,9 +30,9 @@
using System;
using System.Threading;
using System.Threading.Tasks;
-using Opc.Ua.Bindings;
-namespace Opc.Ua.Server
+
+namespace Opc.Ua.Bindings
{
///
/// Carries data channels over the SecureChannel that already exists,
@@ -81,7 +81,7 @@ namespace Opc.Ua.Server
return false;
}
- if (!UaSCDataChannelSecureChannelRegistry.TryGet(
+ if (!UaSCSecureChannelRegistry.TryGet(
secureChannelContext.SecureChannelId,
out UaSCUaBinaryChannel? channel) ||
channel == null)
@@ -95,7 +95,7 @@ namespace Opc.Ua.Server
capabilities.MaxDataChannels,
capabilities.MaxCreditPerChannel);
- int body = channel.MaxDataChannelBodySize;
+ int body = channel.GetMaxDataChannelBodySize();
maxFrameSize = body <= 0 ? 0 : (uint)body;
// Inline framing rides the SecureChannel, which is ordered and
@@ -132,12 +132,12 @@ namespace Opc.Ua.Server
public void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason)
{
if (secureChannelContext != null &&
- UaSCDataChannelSecureChannelRegistry.TryGet(
+ UaSCSecureChannelRegistry.TryGet(
secureChannelContext.SecureChannelId,
out UaSCUaBinaryChannel? channel) &&
channel != null)
{
- channel.DataChannels?.AbortAll(reason);
+ channel.GetDataChannels()?.AbortAll(reason);
}
}
diff --git a/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7798c9bd57
--- /dev/null
+++ b/src/Opc.Ua.Core.Channels/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System;
+
+[assembly: CLSCompliant(false)]
diff --git a/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs
new file mode 100644
index 0000000000..1a39843ed7
--- /dev/null
+++ b/src/Opc.Ua.Core.Channels/UaSCDataChannelExtensions.cs
@@ -0,0 +1,161 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System;
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// Attaches inline data channel framing to a UASC binary channel.
+ ///
+ ///
+ /// A frame is one STR MessageChunk, written to the connection exactly as a
+ /// MSG chunk is. The channel itself knows nothing of the frame format; it
+ /// carries the MessageType on behalf of the extension registered here.
+ ///
+ public static class UaSCDataChannelExtensions
+ {
+ ///
+ /// Enables the data channel feature on a SecureChannel.
+ ///
+ ///
+ /// Experimental. Until this is called the STR dispatch is inert and an
+ /// incoming frame closes the SecureChannel, which is what the
+ /// interoperability rule of the Part 6 errata 5.16 requires of a peer
+ /// that does not implement this specification.
+ ///
+ /// The SecureChannel that carries the frames.
+ /// True on the server side, which allocates
+ /// ChannelIds.
+ /// Telemetry context.
+ /// The most channels kept open at once
+ /// on this SecureChannel.
+ /// The largest window granted to one
+ /// channel.
+ /// is null.
+ public static DataChannelManager EnableDataChannels(
+ this UaSCUaBinaryChannel channel,
+ bool isServer,
+ ITelemetryContext telemetry,
+ ushort maxDataChannels = 16,
+ uint maxCreditPerChannel = 1024 * 1024)
+ {
+ if (channel == null)
+ {
+ throw new ArgumentNullException(nameof(channel));
+ }
+
+ if (channel.TryGetDataChannelExtension(out DataChannelExtension? existing) &&
+ existing != null)
+ {
+ return existing.Manager;
+ }
+
+ var extension = new DataChannelExtension(
+ channel,
+ isServer,
+ telemetry,
+ maxDataChannels,
+ maxCreditPerChannel);
+
+ var registered = (DataChannelExtension)channel.RegisterMessageExtension(extension);
+
+ if (!ReferenceEquals(registered, extension))
+ {
+ _ = extension.Manager.DisposeAsync().AsTask();
+ }
+
+ return registered.Manager;
+ }
+
+ ///
+ /// Returns the data channels multiplexed onto a SecureChannel, or
+ /// null when the feature has not been enabled on it.
+ ///
+ /// The SecureChannel.
+ /// is null.
+ public static DataChannelManager? GetDataChannels(this UaSCUaBinaryChannel channel)
+ {
+ return channel.TryGetDataChannelExtension(out DataChannelExtension? extension)
+ ? extension?.Manager
+ : null;
+ }
+
+ ///
+ /// Returns the extension that owns the STR MessageType on a
+ /// SecureChannel, if one is registered.
+ ///
+ /// The SecureChannel.
+ /// The extension.
+ /// is null.
+ public static bool TryGetDataChannelExtension(
+ this UaSCUaBinaryChannel channel,
+ out DataChannelExtension? extension)
+ {
+ if (channel == null)
+ {
+ throw new ArgumentNullException(nameof(channel));
+ }
+
+ if (channel.TryGetMessageExtension(
+ TcpMessageType.Stream,
+ out ISecureChannelMessageExtension? registered) &&
+ registered is DataChannelExtension dataChannels)
+ {
+ extension = dataChannels;
+ return true;
+ }
+
+ extension = null;
+ return false;
+ }
+
+ ///
+ /// The largest secured body a data channel frame may occupy on a
+ /// SecureChannel.
+ ///
+ /// The SecureChannel.
+ /// is null.
+ public static int GetMaxDataChannelBodySize(this UaSCUaBinaryChannel channel)
+ {
+ if (channel == null)
+ {
+ throw new ArgumentNullException(nameof(channel));
+ }
+
+ ISecureChannelMessageHost host = channel;
+
+ return DataChannelFrameCodec.MaxPayload(
+ DataChannelFramingMode.Inline,
+ host.SendBufferSize,
+ host.SymmetricSignatureSize + 2,
+ withDeadline: true);
+ }
+ }
+}
diff --git a/src/Opc.Ua.Core/EventIds.cs b/src/Opc.Ua.Core/EventIds.cs
index d5b57a1e1d..ea4eeb052e 100644
--- a/src/Opc.Ua.Core/EventIds.cs
+++ b/src/Opc.Ua.Core/EventIds.cs
@@ -103,7 +103,5 @@ namespace Opc.Ua
public const int X509CertificateStore = 570;
public const int X509CrlHelper = 580;
public const int CryptoProvider = 590;
- public const int DataChannel = 600;
- public const int DataChannelManager = 620;
}
}
diff --git a/src/Opc.Ua.Core/Stack/DataChannelFeature.cs b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs
new file mode 100644
index 0000000000..eb180c3908
--- /dev/null
+++ b/src/Opc.Ua.Core/Stack/DataChannelFeature.cs
@@ -0,0 +1,58 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+namespace Opc.Ua
+{
+ ///
+ /// Marks the DataChannel Service Set as experimental.
+ ///
+ ///
+ /// The Services themselves are generated into the standard type surface
+ /// and are reachable from any Client, while the engine that carries the
+ /// frames ships separately in
+ /// OPCFoundation.NetStandard.Opc.Ua.Core.Channels. The marker
+ /// therefore lives here, where both the Services and their callers can see
+ /// it: every identifier the feature uses is provisional and will change if
+ /// and when the OPC Foundation assigns final values.
+ ///
+ public static class DataChannelFeature
+ {
+ ///
+ /// The diagnostic id reported for the experimental DataChannel API.
+ ///
+ public const string ExperimentalDiagnosticId = "DataChannels";
+
+ ///
+ /// The documentation url reported alongside
+ /// .
+ ///
+ public const string ExperimentalUrlFormat =
+ "https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DataChannels.md";
+ }
+}
diff --git a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs
index efb90aa0a4..70913dd14f 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/ISecureChannelMessageExtension.cs
@@ -159,17 +159,17 @@ namespace Opc.Ua.Bindings
TimeProvider TimeProvider { get; }
///
- /// How many SequenceNumbers this channel has issued under the
+ /// How much of the SequenceNumber space remains under the
/// SecurityToken currently in force.
///
///
- /// The SequenceNumber space is per token and is shared by every
- /// MessageType the channel carries, so an extension that emits a large
- /// number of messages has to watch it. Read this inside the callback
- /// passed to to observe it consistently
- /// with the message being sent.
+ /// The space is per token and is shared by every MessageType the
+ /// channel carries, so an extension that emits a large number of
+ /// messages has to draw on this rather than assume it is alone.
+ /// Consume it inside the callback passed to ,
+ /// which is where the reservation and the send are atomic.
///
- long SequenceNumbersIssued { get; }
+ SequenceNumberBudget SequenceBudget { get; }
///
/// Reports a fault whose blast radius is the whole SecureChannel.
diff --git a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs b/src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs
similarity index 91%
rename from src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.cs
rename to src/Opc.Ua.Core/Stack/Tcp/SequenceNumberBudget.cs
index 75d6723a99..249220b916 100644
--- a/src/Opc.Ua.Core/Stack/DataChannels/Sequencing/DataChannelSequenceBudget.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 DataChannelSequenceBudget
+ public sealed class SequenceNumberBudget
{
///
/// Creates a budget.
@@ -73,7 +73,7 @@ namespace Opc.Ua.Bindings
/// emission rate.
/// The number of SequenceNumbers usable
/// under one token. Defaults to the 32 bit space.
- public DataChannelSequenceBudget(
+ public SequenceNumberBudget(
TimeProvider? timeProvider = null,
uint capacity = uint.MaxValue)
{
@@ -142,14 +142,13 @@ namespace Opc.Ua.Bindings
{
get
{
- double perMinute = ChunksPerSecond *
- DataChannelConstants.SequenceNumberRenewalSeconds;
+ double perMinute = ChunksPerSecond * SequenceNumberRenewalSeconds;
long rateBased = perMinute >= long.MaxValue
? long.MaxValue
: (long)perMinute;
- return Math.Min(DataChannelConstants.SequenceNumberRenewalHeadroom, rateBased);
+ return Math.Min(SequenceNumberRenewalHeadroom, rateBased);
}
}
@@ -230,5 +229,18 @@ namespace Opc.Ua.Bindings
private readonly TimeProvider m_timeProvider;
private long m_consumed;
private long m_tokenStarted;
+
+ ///
+ /// The SequenceNumber headroom below which a channel initiates token
+ /// renewal ahead of the normal lifetime schedule.
+ ///
+ private const uint SequenceNumberRenewalHeadroom = 1u << 30;
+
+ ///
+ /// The window, in seconds, of expected chunk emission that also
+ /// triggers renewal when it exceeds the remaining SequenceNumber
+ /// space.
+ ///
+ private const int SequenceNumberRenewalSeconds = 60;
}
}
diff --git a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs
index c82c93575b..eaa6cabf25 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/TcpListenerChannel.cs
@@ -150,7 +150,7 @@ namespace Opc.Ua.Bindings
///
protected override void Dispose(bool disposing)
{
- UaSCDataChannelSecureChannelRegistry.Unbind(GlobalChannelId, this);
+ UaSCSecureChannelRegistry.Unbind(GlobalChannelId, this);
base.Dispose(disposing);
}
@@ -268,7 +268,7 @@ namespace Opc.Ua.Bindings
boundTransport.OnSecureChannelAttached(GlobalChannelId);
}
- UaSCDataChannelSecureChannelRegistry.Bind(GlobalChannelId, this);
+ UaSCSecureChannelRegistry.Bind(GlobalChannelId, this);
m_logger.TcpListenChannelLog0(
ChannelName,
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs
deleted file mode 100644
index 0c26a0e17d..0000000000
--- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.DataChannels.cs
+++ /dev/null
@@ -1,180 +0,0 @@
-/* ========================================================================
- * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
- *
- * OPC Foundation MIT License 1.00
- *
- * Permission is hereby granted, free of charge, to any person
- * obtaining a copy of this software and associated documentation
- * files (the "Software"), to deal in the Software without
- * restriction, including without limitation the rights to use,
- * copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following
- * conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE.
- *
- * The complete license agreement can be found here:
- * http://opcfoundation.org/License/MIT/1.00/
- * ======================================================================*/
-
-using System.Collections.Concurrent;
-
-namespace Opc.Ua.Bindings
-{
- ///
- /// Inline data channel framing over the UASC binary channel, expressed as
- /// an extension that owns the STR MessageType.
- ///
- public partial class UaSCUaBinaryChannel
- {
- ///
- /// The data channels multiplexed onto this SecureChannel, or null
- /// when the feature is not enabled.
- ///
- ///
- /// Experimental. Until is called the
- /// STR dispatch is inert and an incoming frame closes the
- /// SecureChannel, which is what the interoperability rule of the
- /// Part 6 errata 5.16 requires of a peer that does not implement this
- /// specification.
- ///
- public DataChannelManager? DataChannels => m_dataChannels?.Manager;
-
- ///
- /// Enables the data channel feature on this SecureChannel.
- ///
- /// True on the server side, which
- /// allocates ChannelIds.
- /// Telemetry context.
- /// The most channels kept open at
- /// once on this SecureChannel.
- /// The largest window granted
- /// to one channel.
- public DataChannelManager EnableDataChannels(
- bool isServer,
- ITelemetryContext telemetry,
- ushort maxDataChannels = 16,
- uint maxCreditPerChannel = 1024 * 1024)
- {
- DataChannelExtension? existing = m_dataChannels;
-
- if (existing != null)
- {
- return existing.Manager;
- }
-
- var extension = new DataChannelExtension(
- this,
- isServer,
- telemetry,
- maxDataChannels,
- maxCreditPerChannel);
-
- var registered = (DataChannelExtension)RegisterMessageExtension(extension);
-
- if (ReferenceEquals(registered, extension))
- {
- m_isDataChannelSource = isServer;
- }
- else
- {
- _ = extension.Manager.DisposeAsync().AsTask();
- }
-
- m_dataChannels = registered;
- return registered.Manager;
- }
-
- ///
- /// Tracks how much of the SecureChannel SequenceNumber space
- /// remains under the current SecurityToken. STR, MSG, OPN and CLO
- /// chunks all consume the same sender SequenceNumber space.
- ///
- public DataChannelSequenceBudget SequenceBudget
- {
- get
- {
- m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken);
- return m_sequenceBudget;
- }
- }
-
- ///
- /// True when the SequenceNumber space remaining under the current
- /// token has fallen below the renewal threshold, so the owning
- /// channel should initiate OpenSecureChannel with
- /// RenewalRequest ahead of the normal lifetime based renewal.
- ///
- public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew;
-
- ///
- /// The largest secured body a data channel frame may occupy on
- /// this SecureChannel.
- ///
- ///
- /// Public because a Server has to advertise this ceiling when it
- /// negotiates a channel, and the negotiation happens in the Server
- /// assembly rather than here.
- ///
- public int MaxDataChannelBodySize
- => DataChannelFrameCodec.MaxPayload(
- DataChannelFramingMode.Inline,
- SendBufferSize,
- SymmetricSignatureSize + 2,
- withDeadline: true);
-
- private readonly DataChannelSequenceBudget m_sequenceBudget = new();
- private DataChannelExtension? m_dataChannels;
- private bool m_isDataChannelSource;
- }
-
- ///
- /// Maps server-side SecureChannel identifiers to their UASC channels so
- /// the DataChannel Service Set can bind an accepted OpenDataChannel to the
- /// transport that will carry its STR frames.
- ///
- public static class UaSCDataChannelSecureChannelRegistry
- {
- ///
- /// Finds the server-side UASC channel that owns a SecureChannel.
- ///
- /// The SecureChannel identifier.
- /// The channel that owns it.
- public static bool TryGet(
- string secureChannelId,
- out UaSCUaBinaryChannel? channel)
- {
- return s_channels.TryGetValue(secureChannelId, out channel);
- }
-
- internal static void Bind(string secureChannelId, UaSCUaBinaryChannel channel)
- {
- if (!string.IsNullOrEmpty(secureChannelId))
- {
- s_channels[secureChannelId] = channel;
- }
- }
-
- internal static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel)
- {
- if (!string.IsNullOrEmpty(secureChannelId) &&
- s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) &&
- ReferenceEquals(current, channel))
- {
- s_channels.TryRemove(secureChannelId, out _);
- }
- }
-
- private static readonly ConcurrentDictionary s_channels = new();
- }
-}
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs
index 2ae3e1b808..95cd05c753 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.MessageExtensions.cs
@@ -56,7 +56,29 @@ namespace Opc.Ua.Bindings
TimeProvider ISecureChannelMessageHost.TimeProvider => TimeProvider;
///
- long ISecureChannelMessageHost.SequenceNumbersIssued => SequenceNumbersIssuedUnderCurrentToken;
+ SequenceNumberBudget ISecureChannelMessageHost.SequenceBudget => SequenceBudget;
+
+ ///
+ /// Tracks how much of the SecureChannel SequenceNumber space remains
+ /// under the current SecurityToken. Every MessageType the channel
+ /// carries draws on the same space.
+ ///
+ public SequenceNumberBudget SequenceBudget
+ {
+ get
+ {
+ m_sequenceBudget.ObserveConsumed(SequenceNumbersIssuedUnderCurrentToken);
+ return m_sequenceBudget;
+ }
+ }
+
+ ///
+ /// True when the SequenceNumber space remaining under the current
+ /// token has fallen below the renewal threshold, so the owning channel
+ /// should initiate OpenSecureChannel with RenewalRequest ahead of the
+ /// normal lifetime based renewal.
+ ///
+ public bool IsSequenceRenewalDue => SequenceBudget.ShouldRenew;
///
/// Registers the extension that owns a MessageType on this channel.
@@ -244,6 +266,8 @@ namespace Opc.Ua.Bindings
ref m_sequenceNumberBaseline,
Interlocked.Read(ref m_sequenceNumber));
+ m_sequenceBudget.OnTokenActivated();
+
if (m_messageExtensions.IsEmpty)
{
return;
@@ -305,6 +329,7 @@ namespace Opc.Ua.Bindings
private readonly ConcurrentDictionary m_messageExtensions
= new();
+ private readonly SequenceNumberBudget m_sequenceBudget = new();
private long m_sequenceNumberBaseline;
}
}
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
index 8efec94d93..aaa7305632 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
@@ -199,11 +199,10 @@ namespace Opc.Ua.Bindings
CurrentToken = token;
RenewedToken = null;
- // The SequenceNumber space is per token, so a new token
- // restores the budget an extension stalls against
+ // The SequenceNumber space is per token, so a new token restores
+ // the budget every MessageType on the channel draws on
// (Part 6 errata 5.1.1).
NotifySecurityTokenActivated();
- m_sequenceBudget.OnTokenActivated();
TokenActivatedCallback?.Invoke(token, PreviousToken);
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs
index ec99595cfc..0951d57b6d 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs
@@ -1483,7 +1483,7 @@ namespace Opc.Ua.Bindings
if (m_logger.IsEnabled(LogLevel.Information))
{
- DataChannelSequenceBudget budget = SequenceBudget;
+ SequenceNumberBudget budget = SequenceBudget;
m_logger.UaSCClientLog37(
ChannelId,
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs
index a8f55983f6..d6e1174b1d 100644
--- a/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs
+++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs
@@ -150,29 +150,18 @@ namespace Opc.Ua.Bindings
///
public TransportChannelFeatures SupportedFeatures =>
(m_channel?.Transport?.Features ?? TransportChannelFeatures.None) |
- TransportChannelFeatures.DataChannels;
+ TransportChannelFeatures.MessageExtensions;
///
- /// Enables OPC UA data channels on the current SecureChannel.
+ /// The SecureChannel behind this transport channel, or null
+ /// while it is not connected.
///
- public DataChannelManager EnableDataChannels(
- bool isServer,
- ITelemetryContext telemetry,
- ushort maxDataChannels = 16,
- uint maxCreditPerChannel = 1024 * 1024)
- {
- UaSCUaBinaryClientChannel channel = m_channel ?? throw BadNotConnected();
- return channel.EnableDataChannels(
- isServer,
- telemetry,
- maxDataChannels,
- maxCreditPerChannel);
- }
-
- ///
- /// The data channels multiplexed onto the current SecureChannel, if enabled.
- ///
- public DataChannelManager? DataChannels => m_channel?.DataChannels;
+ ///
+ /// Exposed so a message extension can attach itself to the channel a
+ /// Client is already using. It is the channel, not this wrapper, that
+ /// carries the MessageType an extension owns.
+ ///
+ public UaSCUaBinaryClientChannel? SecureChannel => m_channel;
///
public EndpointDescription EndpointDescription
diff --git a/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs
new file mode 100644
index 0000000000..7088dafc3c
--- /dev/null
+++ b/src/Opc.Ua.Core/Stack/Tcp/UaSCSecureChannelRegistry.cs
@@ -0,0 +1,95 @@
+/* ========================================================================
+ * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System.Collections.Concurrent;
+
+namespace Opc.Ua.Bindings
+{
+ ///
+ /// Maps server-side SecureChannel identifiers to the UASC channels that
+ /// own them.
+ ///
+ ///
+ /// A Service request carries a SecureChannel identifier rather than the
+ /// channel object, so anything that has to reach the transport behind a
+ /// request - a message extension binding itself to the connection the
+ /// request arrived on, for example - needs this lookup.
+ ///
+ /// TODO: This is process-global rather than scoped to the listener that
+ /// owns the channels, which means two servers in one process share it.
+ /// Scoping it to the listener requires threading a registry instance
+ /// through the transport bindings and is left as separate work.
+ ///
+ ///
+ public static class UaSCSecureChannelRegistry
+ {
+ ///
+ /// Finds the server-side UASC channel that owns a SecureChannel.
+ ///
+ /// The SecureChannel identifier.
+ /// The channel that owns it.
+ public static bool TryGet(
+ string secureChannelId,
+ out UaSCUaBinaryChannel? channel)
+ {
+ return s_channels.TryGetValue(secureChannelId, out channel);
+ }
+
+ ///
+ /// Records the channel that owns a SecureChannel.
+ ///
+ /// The SecureChannel identifier.
+ /// The channel that owns it.
+ public static void Bind(string secureChannelId, UaSCUaBinaryChannel channel)
+ {
+ if (!string.IsNullOrEmpty(secureChannelId))
+ {
+ s_channels[secureChannelId] = channel;
+ }
+ }
+
+ ///
+ /// Releases the record for a SecureChannel, if the channel given still
+ /// owns it.
+ ///
+ /// The SecureChannel identifier.
+ /// The channel that owned it.
+ public static void Unbind(string secureChannelId, UaSCUaBinaryChannel channel)
+ {
+ if (!string.IsNullOrEmpty(secureChannelId) &&
+ s_channels.TryGetValue(secureChannelId, out UaSCUaBinaryChannel? current) &&
+ ReferenceEquals(current, channel))
+ {
+ s_channels.TryRemove(secureChannelId, out _);
+ }
+ }
+
+ private static readonly ConcurrentDictionary s_channels = new();
+ }
+}
diff --git a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs
index 360d5b4989..ce689e6a6c 100644
--- a/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs
+++ b/src/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs
@@ -201,10 +201,10 @@ namespace Opc.Ua
ReverseConnect = 0x0040,
///
- /// The channel can multiplex OPC UA data channels onto the
- /// SecureChannel. Experimental.
+ /// The channel can carry MessageTypes owned by a registered
+ /// extension, for example OPC UA data channels. Experimental.
///
- DataChannels = 0x0080,
+ MessageExtensions = 0x0080,
///
/// The transport under the channel provides independently
diff --git a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs
index fafd7906d6..dce50c7e66 100644
--- a/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs
+++ b/src/Opc.Ua.Redundancy.Client/RedundantClientSession.cs
@@ -1237,8 +1237,8 @@ namespace Opc.Ua.Redundancy.Client
///
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(
- Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)]
+ Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask OpenDataChannelAsync(
RequestHeader? requestHeader,
@@ -1262,8 +1262,8 @@ namespace Opc.Ua.Redundancy.Client
///
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(
- Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)]
+ Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask ModifyDataChannelAsync(
RequestHeader? requestHeader,
@@ -1283,8 +1283,8 @@ namespace Opc.Ua.Redundancy.Client
///
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(
- Opc.Ua.Bindings.DataChannelConstants.ExperimentalDiagnosticId,
- UrlFormat = Opc.Ua.Bindings.DataChannelConstants.ExperimentalUrlFormat)]
+ Opc.Ua.DataChannelFeature.ExperimentalDiagnosticId,
+ UrlFormat = Opc.Ua.DataChannelFeature.ExperimentalUrlFormat)]
#endif
public async ValueTask CloseDataChannelAsync(
RequestHeader? requestHeader,
diff --git a/src/Opc.Ua.Server/Opc.Ua.Server.csproj b/src/Opc.Ua.Server/Opc.Ua.Server.csproj
index 785fa43496..53486b6272 100644
--- a/src/Opc.Ua.Server/Opc.Ua.Server.csproj
+++ b/src/Opc.Ua.Server/Opc.Ua.Server.csproj
@@ -35,6 +35,7 @@
+
diff --git a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs
index cb3333a28d..161a839893 100644
--- a/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs
+++ b/src/Opc.Ua.Server/Server/StandardServer.DataChannels.cs
@@ -16,35 +16,6 @@ using Opc.Ua.Bindings;
namespace Opc.Ua.Server
{
- ///
- /// Optional transport-side binding for the DataChannel Service Set.
- ///
- public interface IServerDataChannelTransport
- {
- bool TryGetManager(
- SecureChannelContext secureChannelContext,
- DataChannelServerCapabilities capabilities,
- ITelemetryContext telemetry,
- out DataChannelManager manager,
- out uint maxFrameSize,
- out bool isReliable);
-
- ValueTask AllocateServerStreamAsync(
- SecureChannelContext secureChannelContext,
- uint channelId,
- DataChannelDirection direction,
- CancellationToken ct);
-
- ValueTask BindClientStreamAsync(
- SecureChannelContext secureChannelContext,
- uint channelId,
- ulong streamId,
- DataChannelDirection direction,
- CancellationToken ct);
-
- void AbortSecureChannel(SecureChannelContext secureChannelContext, StatusCode reason);
- }
-
public partial class StandardServer
{
public DataChannelSourceRegistry DataChannelSources { get; } = new();
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs
index e539563b5f..e345eb2b0c 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelConformanceTests.cs
@@ -126,14 +126,14 @@ namespace Opc.Ua.Core.DataChannels.Tests
channel.AttachTransport(transport);
channel.Activate(SecureChannelId, TokenId);
DataChannel sink = OpenSink(channel);
- await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false);
+ await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false);
transport.ClearChunks();
byte[] chunk = SpecVectors.Load("inline_data_first");
chunk[SpecVectors.InlinePrefix + 4] = frameType;
Assert.That(channel.DispatchStream(chunk), Is.False);
- await PumpSchedulerRoundAsync(channel.DataChannels!).ConfigureAwait(false);
+ await PumpSchedulerRoundAsync(channel.GetDataChannels()!).ConfigureAwait(false);
DataChannelFrame reset = await WaitForOutboundFrameAsync(transport, DataChannelFrameType.Reset)
.ConfigureAwait(false);
@@ -151,7 +151,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
Assert.That(expectedDecodeError.ToStatusCode(), Is.EqualTo(expectedStatus));
});
- await channel.DataChannels!.DisposeAsync().ConfigureAwait(false);
+ await channel.GetDataChannels()!.DisposeAsync().ConfigureAwait(false);
}
[Test]
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs
index d215cae66e..c29add8eea 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequenceBudgetTests.cs
@@ -13,13 +13,13 @@ namespace Opc.Ua.Core.DataChannels.Tests
{
[TestFixture]
[Category("DataChannels")]
- public sealed class DataChannelSequenceBudgetTests
+ public sealed class SequenceNumberBudgetTests
{
[Test]
public void ObservedServiceChunksContributeToRenewalAndStallBudget()
{
var clock = new FakeTimeProvider(DateTimeOffset.UtcNow);
- var budget = new DataChannelSequenceBudget(clock, capacity: 5);
+ var budget = new SequenceNumberBudget(clock, capacity: 5);
budget.ObserveConsumed(4);
clock.Advance(TimeSpan.FromSeconds(1));
@@ -45,7 +45,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
[Test]
public void TokenActivationResetsObservedAndConsumedBudget()
{
- var budget = new DataChannelSequenceBudget(capacity: 3);
+ var budget = new SequenceNumberBudget(capacity: 3);
budget.ObserveSequenceNumber(2);
Assert.That(budget.MustStall, Is.True);
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs
index 908f2598fc..a916978d14 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/DataChannelSequencingTests.cs
@@ -261,7 +261,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
[Test]
public void DcF034RenewalIsDueBeforeTheSpaceIsExhausted()
{
- var budget = new DataChannelSequenceBudget(capacity: 1000);
+ var budget = new SequenceNumberBudget(capacity: 1000);
Assert.That(budget.ShouldRenew, Is.False, "a fresh token has the whole space");
@@ -281,7 +281,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
[Test]
public void DcF034ASenderStallsRatherThanReusingASequenceNumber()
{
- var budget = new DataChannelSequenceBudget(capacity: 4);
+ var budget = new SequenceNumberBudget(capacity: 4);
for (int ii = 0; ii < 4; ii++)
{
@@ -304,7 +304,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
[Test]
public void ANewTokenRestoresTheWholeSpace()
{
- var budget = new DataChannelSequenceBudget(capacity: 4);
+ var budget = new SequenceNumberBudget(capacity: 4);
for (int ii = 0; ii < 4; ii++)
{
@@ -329,7 +329,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider(
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
- var budget = new DataChannelSequenceBudget(clock);
+ var budget = new SequenceNumberBudget(clock);
// Ten chunks over ten seconds is one per second, so one
// minute of traffic is sixty values: far below the fixed
@@ -359,7 +359,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
var clock = new Microsoft.Extensions.Time.Testing.FakeTimeProvider(
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
- var budget = new DataChannelSequenceBudget(clock);
+ var budget = new SequenceNumberBudget(clock);
// A hundred million chunks in one second is a rate whose
// minute of traffic exceeds the 32 bit space entirely, so the
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs
index 07e6442ad1..4cddbcded2 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/InlineServerDataChannelTransportTests.cs
@@ -62,7 +62,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
channel.Activate(0x0000A17C, 7);
const string secureChannelId = "inline-transport-1";
- UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel);
+ UaSCSecureChannelRegistry.Bind(secureChannelId, channel);
try
{
@@ -84,7 +84,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
{
Assert.That(maxFrameSize, Is.GreaterThan(0u));
Assert.That(isReliable, Is.True);
- Assert.That(manager, Is.SameAs(channel.DataChannels));
+ Assert.That(manager, Is.SameAs(channel.GetDataChannels()));
});
DataChannel source = manager.Register(
@@ -112,7 +112,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
}
finally
{
- UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel);
+ UaSCSecureChannelRegistry.Unbind(secureChannelId, channel);
}
}
@@ -140,7 +140,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
using InlineTestChannel channel = InlineTestChannel.Create("inline-profile", telemetry);
const string secureChannelId = "inline-transport-2";
- UaSCDataChannelSecureChannelRegistry.Bind(secureChannelId, channel);
+ UaSCSecureChannelRegistry.Bind(secureChannelId, channel);
try
{
@@ -159,7 +159,7 @@ namespace Opc.Ua.Core.DataChannels.Tests
}
finally
{
- UaSCDataChannelSecureChannelRegistry.Unbind(secureChannelId, channel);
+ UaSCSecureChannelRegistry.Unbind(secureChannelId, channel);
}
}
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj
index 3da3c4dae0..6643096cc6 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/Opc.Ua.Core.DataChannels.Tests.csproj
@@ -42,6 +42,7 @@
+
diff --git a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs
index 9391954b82..d2a25b7fde 100644
--- a/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs
+++ b/tests/Opc.Ua.Core.DataChannels.Tests/UaSCDataChannelDispatchTests.cs
@@ -432,9 +432,11 @@ namespace Opc.Ua.Core.DataChannels.Tests
private static async Task DisposeDataChannelsAsync(TestChannel channel)
{
- if (channel.DataChannels != null)
+ DataChannelManager? manager = channel.GetDataChannels();
+
+ if (manager != null)
{
- await channel.DataChannels.DisposeAsync().ConfigureAwait(false);
+ await manager.DisposeAsync().ConfigureAwait(false);
}
}