From 9e48f3d04d80da3f8a7465f476f0991560de501c Mon Sep 17 00:00:00 2001 From: romanett Date: Mon, 26 Oct 2026 08:28:02 +0100 Subject: [PATCH] Client: apply TransportQuotas in ManagedSessionBuilder URL shortcuts (#4906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description `ManagedSessionBuilder.UseEndpoint(string url, ...)`, `UseWebApiEndpoint(...)` and `UseWssOpenApiEndpoint(...)` created their endpoint with `new ConfiguredEndpoint(null, description, null)`. The `null` endpoint configuration falls back to `EndpointConfiguration.Create()` defaults (`MaxStringLength = 65535`, `MaxArrayLength = 65535`, `MaxMessageSize = 2 MiB`, ...). The transport channels build their message context from the endpoint configuration, so the builder's `ApplicationConfiguration.TransportQuotas` were silently ignored and `session.MessageContext` kept the defaults. The three shortcuts now build the endpoint with `EndpointConfiguration.Create(configuration)`, the same as `AddDiscoveryAndConnect` since #4810. Reported in #4905. That report was about `AddDiscoveryAndConnect`, which is already fixed on master. While investigating it I found the same pattern in these builder shortcuts. ## Tests - `ManagedSessionBuilderExtendedTests.UrlShortcutUsesApplicationTransportQuotas`: unit test for all three shortcuts, checks the built endpoint configuration. - `ManagedSessionBuilderTransportQuotaTests.UseEndpointUrlSessionUsesApplicationTransportQuotasAsync`: connects to an in-process reference server through `UseEndpoint(url)` and checks `session.MessageContext`. All four new test cases fail without the fix (they get the 65535 / 2 MiB defaults). Local runs on net10.0: - Full `Opc.Ua.Client.Tests`: 3327 passed, 2 skipped, 1 failed. The failure was `ManagedSessionServerRedundancyTests.ConnectWithProbingSelectsMemberWithHigherServiceLevelAsync`, which doesn't use the changed shortcuts. That fixture passed 3/3 when rerun on its own. - `Opc.Ua.Sessions.Tests` `HttpsWebApiIntegrationTests` and `WssOpenApiIntegrationTests`, which use `UseWebApiEndpoint` / `UseWssOpenApiEndpoint`: 19/19 passed. `Opc.Ua.Client.Tests` also compiles for net48. ## Related Issues - Related to #4905 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../Fluent/ManagedSessionBuilder.cs | 20 +++- .../ManagedSessionBuilderExtendedTests.cs | 45 ++++++++ ...anagedSessionBuilderTransportQuotaTests.cs | 108 ++++++++++++++++++ 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderTransportQuotaTests.cs diff --git a/src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs b/src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs index 0c79aee134..e9430121d4 100644 --- a/src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs +++ b/src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs @@ -103,6 +103,9 @@ namespace Opc.Ua.Client /// /// Use a discovery endpoint URL with optional security mode/policy. + /// The endpoint's encoding and message limits are taken from the + /// of the + /// configuration this builder was created with. /// /// is null. public ManagedSessionBuilder UseEndpoint( @@ -120,7 +123,8 @@ namespace Opc.Ua.Client SecurityMode = securityMode, SecurityPolicyUri = securityPolicyUri }; - return UseEndpoint(new ConfiguredEndpoint(null, endpointDescription, null)); + return UseEndpoint(new ConfiguredEndpoint( + null, endpointDescription, EndpointConfiguration.Create(m_configuration))); } /// @@ -137,7 +141,9 @@ namespace Opc.Ua.Client /// Certificate + IssuedToken / JWT) so the client can present /// any identity supported by the server; the server-side /// DefaultSessionlessIdentityProvider still enforces what - /// it accepts. + /// it accepts. As with , + /// the endpoint's limits come from the builder configuration's + /// . /// /// /// The server's base URL, e.g. https://server:4843/. @@ -189,7 +195,8 @@ namespace Opc.Ua.Client }; m_webApiOptions ??= new WebApiClientOptions(); m_webApiOptions.Encoding = encoding; - return UseEndpoint(new ConfiguredEndpoint(null, endpointDescription, null)); + return UseEndpoint(new ConfiguredEndpoint( + null, endpointDescription, EndpointConfiguration.Create(m_configuration))); } /// @@ -198,7 +205,9 @@ namespace Opc.Ua.Client /// surfaced via , /// OPC Foundation profile/2339). Same envelope as /// but - /// multiplexed over a WebSocket text-frame transport. + /// multiplexed over a WebSocket text-frame transport, including + /// the limits taken from + /// . /// /// /// The server's WSS URL, e.g. wss://server:4843/. @@ -250,7 +259,8 @@ namespace Opc.Ua.Client }; m_webApiOptions ??= new WebApiClientOptions(); m_webApiOptions.Encoding = encoding; - return UseEndpoint(new ConfiguredEndpoint(null, endpointDescription, null)); + return UseEndpoint(new ConfiguredEndpoint( + null, endpointDescription, EndpointConfiguration.Create(m_configuration))); } /// diff --git a/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderExtendedTests.cs b/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderExtendedTests.cs index 11d063026c..12ee3bb883 100644 --- a/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderExtendedTests.cs +++ b/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderExtendedTests.cs @@ -143,6 +143,51 @@ namespace Opc.Ua.Client.Tests.ClientBuilder Assert.That(opts.Endpoint, Is.Not.Null); } + // ---------------------------------------------------------------- + // URL shortcuts and transport quotas + // ---------------------------------------------------------------- + + /// + /// The URL shortcuts build their endpoint from the application's + /// transport quotas instead of the built-in defaults (issue #4905). + /// + [TestCase("opc.tcp://server:4840")] + [TestCase("https://server:4843/")] + [TestCase("wss://server:4843/")] + public void UrlShortcutUsesApplicationTransportQuotas(string url) + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + ApplicationConfiguration configuration = CreateConfig(telemetry); + configuration.TransportQuotas = new TransportQuotas + { + OperationTimeout = 60_000, + MaxStringLength = 1024 * 1024, + MaxByteStringLength = 2 * 1024 * 1024, + MaxArrayLength = 300_000, + MaxMessageSize = 8 * 1024 * 1024, + MaxBufferSize = 128 * 1024 + }; + var builder = new ManagedSessionBuilder(configuration, telemetry); + builder = url.StartsWith("https", StringComparison.Ordinal) + ? builder.UseWebApiEndpoint(url) + : url.StartsWith("wss", StringComparison.Ordinal) + ? builder.UseWssOpenApiEndpoint(url) + : builder.UseEndpoint(url, MessageSecurityMode.None, SecurityPolicies.None); + + EndpointConfiguration? quotas = builder.Build().Endpoint?.Configuration; + + Assert.That(quotas, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(quotas!.OperationTimeout, Is.EqualTo(60_000)); + Assert.That(quotas.MaxStringLength, Is.EqualTo(1024 * 1024)); + Assert.That(quotas.MaxByteStringLength, Is.EqualTo(2 * 1024 * 1024)); + Assert.That(quotas.MaxArrayLength, Is.EqualTo(300_000)); + Assert.That(quotas.MaxMessageSize, Is.EqualTo(8 * 1024 * 1024)); + Assert.That(quotas.MaxBufferSize, Is.EqualTo(128 * 1024)); + }); + } + // ---------------------------------------------------------------- // WithWebApiAuthentication // ---------------------------------------------------------------- diff --git a/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderTransportQuotaTests.cs b/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderTransportQuotaTests.cs new file mode 100644 index 0000000000..e70e95993d --- /dev/null +++ b/tests/Opc.Ua.Client.Tests/ClientBuilder/ManagedSessionBuilderTransportQuotaTests.cs @@ -0,0 +1,108 @@ +/* ======================================================================== + * 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; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Opc.Ua.Client.TestFramework; +using Opc.Ua.Server.TestFramework; +using Opc.Ua.Tests; +using Quickstarts.ReferenceServer; + +namespace Opc.Ua.Client.Tests.ClientBuilder +{ + /// + /// Verifies against a running server that a session connected through + /// + /// decodes with the application's transport quotas (issue #4905). + /// + [TestFixture] + [NonParallelizable] + [Category("Client")] + [Category("Integration")] + public sealed class ManagedSessionBuilderTransportQuotaTests + { + /// + /// The session message context reflects the configured limits rather + /// than the built-in endpoint defaults. + /// + [Test] + public async Task UseEndpointUrlSessionUsesApplicationTransportQuotasAsync() + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + string root = Path.Combine(Path.GetTempPath(), "mtq-" + Guid.NewGuid().ToString("N")[..8]); + var fixture = new ServerFixture(context => new ReferenceServer(context)) + { + AutoAccept = true, + SecurityNone = true + }; + try + { + await fixture.StartAsync(root).ConfigureAwait(false); + await using var client = new ClientFixture(telemetry); + await client.LoadClientConfigurationAsync(root, "TransportQuotaClient").ConfigureAwait(false); + ApplicationConfiguration configuration = client.Config; + configuration.TransportQuotas = new TransportQuotas + { + MaxStringLength = 1024 * 1024, + MaxByteStringLength = 2 * 1024 * 1024, + MaxArrayLength = 300_000, + MaxMessageSize = 8 * 1024 * 1024 + }; + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + await using Client.ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry) + .UseEndpoint( + $"{Utils.UriSchemeOpcTcp}://localhost:{fixture.Port}", + MessageSecurityMode.None, + SecurityPolicies.None) + .ConnectAsync(timeout.Token) + .ConfigureAwait(false); + + Assert.Multiple(() => + { + Assert.That(session.MessageContext.MaxStringLength, Is.EqualTo(1024 * 1024)); + Assert.That(session.MessageContext.MaxByteStringLength, Is.EqualTo(2 * 1024 * 1024)); + Assert.That(session.MessageContext.MaxArrayLength, Is.EqualTo(300_000)); + Assert.That(session.MessageContext.MaxMessageSize, Is.EqualTo(8 * 1024 * 1024)); + }); + } + finally + { + await fixture.StopAsync().ConfigureAwait(false); + if (Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + } + } +}