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); + } + } + } + } +}