Client: apply TransportQuotas in ManagedSessionBuilder URL shortcuts (#4906)

# 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 <noreply@anthropic.com>
This commit is contained in:
romanett
2026-10-26 08:28:02 +01:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 3c5aa421d3
commit 9e48f3d04d
3 changed files with 168 additions and 5 deletions
@@ -103,6 +103,9 @@ namespace Opc.Ua.Client
/// <summary>
/// Use a discovery endpoint URL with optional security mode/policy.
/// The endpoint's encoding and message limits are taken from the
/// <see cref="ApplicationConfiguration.TransportQuotas"/> of the
/// configuration this builder was created with.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="url"/> is <c>null</c>.</exception>
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)));
}
/// <summary>
@@ -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
/// <c>DefaultSessionlessIdentityProvider</c> still enforces what
/// it accepts.
/// it accepts. As with <see cref="UseEndpoint(string, MessageSecurityMode, string)"/>,
/// the endpoint's limits come from the builder configuration's
/// <see cref="ApplicationConfiguration.TransportQuotas"/>.
/// </summary>
/// <param name="url">
/// The server's base URL, e.g. <c>https://server:4843/</c>.
@@ -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)));
}
/// <summary>
@@ -198,7 +205,9 @@ namespace Opc.Ua.Client
/// surfaced via <see cref="Profiles.WssOpenApiTransport"/>,
/// OPC Foundation profile/2339). Same envelope as
/// <see cref="UseWebApiEndpoint(string, WebApiEncoding)"/> but
/// multiplexed over a WebSocket text-frame transport.
/// multiplexed over a WebSocket text-frame transport, including
/// the limits taken from
/// <see cref="ApplicationConfiguration.TransportQuotas"/>.
/// </summary>
/// <param name="url">
/// The server's WSS URL, e.g. <c>wss://server:4843/</c>.
@@ -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)));
}
/// <summary>
@@ -143,6 +143,51 @@ namespace Opc.Ua.Client.Tests.ClientBuilder
Assert.That(opts.Endpoint, Is.Not.Null);
}
// ----------------------------------------------------------------
// URL shortcuts and transport quotas
// ----------------------------------------------------------------
/// <summary>
/// The URL shortcuts build their endpoint from the application's
/// transport quotas instead of the built-in defaults (issue #4905).
/// </summary>
[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
// ----------------------------------------------------------------
@@ -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
{
/// <summary>
/// Verifies against a running server that a session connected through
/// <see cref="ManagedSessionBuilder.UseEndpoint(string, MessageSecurityMode, string)"/>
/// decodes with the application's transport quotas (issue #4905).
/// </summary>
[TestFixture]
[NonParallelizable]
[Category("Client")]
[Category("Integration")]
public sealed class ManagedSessionBuilderTransportQuotaTests
{
/// <summary>
/// The session message context reflects the configured limits rather
/// than the built-in endpoint defaults.
/// </summary>
[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<ReferenceServer>(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);
}
}
}
}
}