Allow passing an existing config XML to the client DI AddClient (#4328)

# Description

> **Stacked PR**: based on `romanett/server-di-config-xml-e30c4f`
(#4325) so the diff shows only the client changes. Once #4325 merges
this will be rebased onto `master`.

Gives the **client** DI surface the same migration path for existing
configuration XML files that #4325 gave the server: applications that
already own a classic `MyClient.Config.xml` can pass it to `AddClient`
and keep **all** of their settings.

New API surface:

- `AddClient(string configurationFile, Action<OpcUaClientOptions>?
configure = null)` loads the `ApplicationConfiguration` from the file;
the optional callback configures the remaining client options (session
endpoint, identity, reverse connect, ...).
- `AddClient(Stream configurationStream, ...)` for documents that are
not files on disk, e.g. embedded resources. The stream is read once and
disposed after loading.
- `OpcUaClientOptions.ConfigurationFile` / `ConfigurationStream` back
the overloads, so the options-callback route works too and the file path
can be bound from configuration (`OpcUa:Client:ConfigurationFile`).
- `OpcUaClientOptions.ConfigureLoadedConfiguration` runs after the
document is loaded and validated but before the application-instance
certificate check, for code-side overrides of individual file settings.

Mechanism and behavior:

- A new internal `ClientSuppliedConfigurationProvider` implements
`IOpcUaApplicationConfigurationProvider` and plugs into the client's
existing configuration-provider seam. The document loads **lazily on
first use** — the first session connect, reverse-connect startup, or an
explicit `GetAsync` — through
`ApplicationInstance.LoadApplicationConfigurationAsync`, so every
setting (security configuration, certificate stores, transport quotas,
client configuration) applies exactly as on the classic path, including
localhost fix-up.
- The load **ensures the application-instance certificate** (mirroring
the shared `ConfigureApplication(...)` provider), which the pre-existing
manual workaround — pre-loading and assigning
`OpcUaClientOptions.Configuration` — never received. DI-registered
`ICertificateManager` / `ICertificatePasswordProvider` are honored.
Single-flight loading; `IAsyncDisposable` like the shared provider.
- The supplied document is authoritative and takes precedence over a
shared application registered with `ConfigureApplication(...)`.
Ambiguous combinations are rejected eagerly at `AddClient` with clear
`InvalidOperationException`s: file + stream, document + explicit
`Configuration`, document + application identity properties, and a
white-space path arriving through configuration binding. The start-time
`IValidateOptions` validator accepts a supplied document as a
configuration source.
- `ConnectManagedSessionAsync` publishes the lazily loaded configuration
onto the resolved options after `GetAsync`, so the channel manager and
session builder see it exactly as on the other configuration paths;
reverse connect consumes the provider through its existing async-startup
path unchanged.

Coverage: new `ClientConfigurationFileTests` fixture (8 tests) — loading
from file and stream with assertions on the document settings, the
override callback, the certificate bootstrap, provider caching and lazy
semantics, precedence over `ConfigureApplication`, load failure for a
missing file, argument validation, ambiguous-combination rejection,
configuration-section binding, and validator acceptance. All 323
existing ClientBuilder tests and all 180 server hosting tests (including
the combined client+server fixtures) pass; `Opc.Ua.Client` builds
warning-free on net472, net8.0, and net10.0 (AOT analyzers).

Docs: new "Client: using an existing configuration XML file" section in
`docs/DependencyInjection.md` with cross-links from the server section,
an updated pointer in the 2.0 migration guide, and a NuGet README
mention.

## Related Issues

- Fixes #4327
- Stacked on #4325 (server-side counterpart)

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [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 Fable 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
This commit is contained in:
romanett
2026-08-29 13:33:40 +00:00
committed by GitHub
co-authored by Claude Fable 5 copilot-swe-agent[bot] marcschier
parent e61ad4b306
commit 70c8ce4649
7 changed files with 1024 additions and 11 deletions
+48 -2
View File
@@ -473,7 +473,9 @@ builder.Services
The certificate manager and certificate password provider registered in
dependency injection are honored the same way as on the options path,
and the unmatched user-token-policy startup warning is derived from the
policies advertised by the file.
policies advertised by the file. The client offers the same migration
path — see
[Client: using an existing configuration XML file](#client-using-an-existing-configuration-xml-file).
### Server security and resource controls
@@ -894,7 +896,51 @@ var managedSessions = sp.GetRequiredService<IManagedSessionFactory>();
ManagedSession dynamicSession = await managedSessions.ConnectAsync(endpoint, ct);
```
Misconfiguration is validated through `IValidateOptions<OpcUaClientOptions>` when the host starts and again before a DI-created session connects. Supply either an explicit `Configuration`, the application identity fields directly on `OpcUaClientOptions` (as shown above), or the shared `ConfigureApplication(...)` options for a combined client/server host (see [Shared application configuration](#shared-application-configuration)). `Session.Endpoint` is required by the cached fixed-endpoint delegate; `IManagedSessionFactory.ConnectAsync(endpoint, ...)` supplies it at runtime.
Misconfiguration is validated through `IValidateOptions<OpcUaClientOptions>` when the host starts and again before a DI-created session connects. Supply either an explicit `Configuration`, the application identity fields directly on `OpcUaClientOptions` (as shown above), an existing configuration XML document (`ConfigurationFile` / `ConfigurationStream`, see below), or the shared `ConfigureApplication(...)` options for a combined client/server host (see [Shared application configuration](#shared-application-configuration)). `Session.Endpoint` is required by the cached fixed-endpoint delegate; `IManagedSessionFactory.ConnectAsync(endpoint, ...)` supplies it at runtime.
### Client: using an existing configuration XML file
The client offers the same migration path as
[the server](#migrating-with-an-existing-configuration-xml-file):
applications that already own a classic client configuration file
(`*.Config.xml`) can pass it to `AddClient` and keep every setting in it
— security configuration, certificate stores, transport quotas, client
configuration:
```csharp
services
.AddOpcUa()
.AddClient("MyClient.Config.xml", opt =>
{
opt.Session = new ManagedSessionOptions { Endpoint = endpoint };
});
```
The same file can be referenced from configuration
(`OpcUa:Client:ConfigurationFile`), and a stream works for documents
that are not files on disk, e.g. embedded resources
(`OpcUaClientOptions.ConfigurationStream`; read once and disposed after
loading):
```csharp
Stream stream = typeof(Program).Assembly
.GetManifestResourceStream("MyApp.MyClient.Config.xml")!;
services.AddOpcUa().AddClient(stream);
```
The document loads lazily through
`ApplicationInstance.LoadApplicationConfigurationAsync` on first use —
the first session connect, reverse-connect startup, or an explicit
`GetAsync` on `OpcUaClientOptions.ConfigurationProvider` — which also
runs the optional `ConfigureLoadedConfiguration` override callback and
ensures the application-instance certificate, exactly like the shared
`ConfigureApplication(...)` path. The supplied document is
authoritative: it takes precedence over a shared application registered
with `ConfigureApplication(...)`, and combining it with an explicit
`Configuration`, with the application identity properties, or setting
both file and stream is rejected with a clear
`InvalidOperationException` at registration. DI-registered
`ICertificateManager` / `ICertificatePasswordProvider` are honored.
### Fluent shortcuts
+1 -1
View File
@@ -13,7 +13,7 @@ Because **Data Contract serialization** is not AOT compliant and does not suppor
All configuration DTO classes (`ApplicationConfiguration`, `ServerConfiguration`, `TraceConfiguration`, `TransportConfiguration`, `ServerSecurityPolicy`, `OAuth2ServerSettings`, `OAuth2Credential`, `GlobalDiscoveryServerConfiguration`, `CertificateGroupConfiguration`, `BrowserOptions`, etc.) migrated from `[DataContract]`/`[DataMember]` to source-generated `[DataType]`/`[DataTypeField]` attributes and are now `partial` classes.
- `ApplicationConfiguration.LoadWithNoValidation` uses `XmlParser`/`IEncodeable.Decode()`. Existing XML config files should remain loadable.
Server applications moving to the hosted dependency-injection surface can pass such a file directly to `services.AddOpcUa().AddServer("MyServer.Config.xml")` and keep every setting from it — see [Migrating with an existing configuration XML file](../../DependencyInjection.md#migrating-with-an-existing-configuration-xml-file).
Applications moving to the dependency-injection surface can pass such a file directly to `services.AddOpcUa().AddServer("MyServer.Config.xml")` or `.AddClient("MyClient.Config.xml")` and keep every setting from it — see [Migrating with an existing configuration XML file](../../DependencyInjection.md#migrating-with-an-existing-configuration-xml-file) and [Client: using an existing configuration XML file](../../DependencyInjection.md#client-using-an-existing-configuration-xml-file).
- Browser and session state persistence switched from XML to OPC UA Binary encoding. **Old persisted files cannot be loaded** — delete and re-save.
- `SecuredApplication` uses `SecuredApplicationEncoding` helpers instead of `DataContractSerializer`.
@@ -0,0 +1,243 @@
/* ========================================================================
* 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 Opc.Ua.Configuration;
using Opc.Ua.Security.Certificates;
namespace Opc.Ua.Client
{
/// <summary>
/// <see cref="IOpcUaApplicationConfigurationProvider"/> backed by an
/// existing OPC UA application configuration XML document supplied via
/// <see cref="OpcUaClientOptions.ConfigurationFile"/> or
/// <see cref="OpcUaClientOptions.ConfigurationStream"/>. The document
/// is loaded lazily on the first <see cref="GetAsync"/>: it is read
/// through <c>ApplicationInstance.LoadApplicationConfigurationAsync</c>
/// so every setting applies exactly as on the classic path, the
/// optional <see cref="OpcUaClientOptions.ConfigureLoadedConfiguration"/>
/// override callback runs, and the application-instance certificate is
/// ensured — mirroring how the shared
/// <c>OpcUaApplicationConfigurationProvider</c> completes client
/// configurations. A supplied stream is read once and disposed.
/// </summary>
internal sealed class ClientSuppliedConfigurationProvider :
IOpcUaApplicationConfigurationProvider
{
public ClientSuppliedConfigurationProvider(
string? configurationFile,
Stream? configurationStream,
Action<ApplicationConfiguration>? configureLoadedConfiguration,
IApplicationInstanceFactory applicationFactory,
ITelemetryContext telemetry,
ICertificateManager? certificateManager = null,
ICertificatePasswordProvider? certificatePasswordProvider = null)
{
if (applicationFactory is null)
{
throw new ArgumentNullException(nameof(applicationFactory));
}
if (telemetry is null)
{
throw new ArgumentNullException(nameof(telemetry));
}
if (string.IsNullOrEmpty(configurationFile) && configurationStream == null)
{
throw new ArgumentException(
"Either a configuration file path or a configuration stream is required.",
nameof(configurationFile));
}
m_configurationFile = configurationFile;
m_configurationStream = configurationStream;
m_configureLoadedConfiguration = configureLoadedConfiguration;
m_certificateManager = certificateManager;
Application = applicationFactory.Create(telemetry);
Application.ApplicationType = ApplicationType.Client;
Application.CertificatePasswordProvider = certificatePasswordProvider;
}
/// <inheritdoc/>
public IApplicationInstance Application { get; }
/// <inheritdoc/>
/// <remarks>
/// The supplied document loads lazily, so the configuration is only
/// available after the first <see cref="GetAsync"/> has completed.
/// </remarks>
public ApplicationConfiguration Configuration =>
Volatile.Read(ref m_configuration) ??
throw new InvalidOperationException(
"The supplied configuration document has not been loaded yet. " +
"Await GetAsync (e.g. by connecting a session) first.");
/// <inheritdoc/>
public async Task<ApplicationConfiguration> GetAsync(CancellationToken ct = default)
{
if (Volatile.Read(ref m_disposed) != 0)
{
throw new ObjectDisposedException(nameof(ClientSuppliedConfigurationProvider));
}
Task<ApplicationConfiguration> loadTask;
lock (m_lock)
{
if (m_disposed != 0)
{
throw new ObjectDisposedException(
nameof(ClientSuppliedConfigurationProvider));
}
m_loadTask ??= LoadAsync();
loadTask = m_loadTask;
}
await ((Task)loadTask).WaitAsync(ct).ConfigureAwait(false);
return await loadTask.ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
if (Interlocked.Exchange(ref m_disposed, 1) != 0)
{
return;
}
Task<ApplicationConfiguration>? loadTask;
lock (m_lock)
{
loadTask = m_loadTask;
}
try
{
if (loadTask != null)
{
// Wait for a load in flight so the application is not
// disposed under it; a failed load has already surfaced
// to the GetAsync caller and must not fail disposal too.
await loadTask.ConfigureAwait(false);
}
}
catch (Exception)
{
}
finally
{
if (loadTask == null)
{
// Never consumed: the supplied stream is owned by this
// provider and would otherwise leak.
m_configurationStream?.Dispose();
}
await Application.DisposeAsync().ConfigureAwait(false);
}
}
private async Task<ApplicationConfiguration> LoadAsync()
{
ApplicationConfiguration configuration;
if (m_configurationStream is { } configurationStream)
{
using (configurationStream)
{
configuration = await Application
.LoadApplicationConfigurationAsync(
configurationStream,
silent: false,
CancellationToken.None)
.ConfigureAwait(false);
}
}
else
{
configuration = await Application
.LoadApplicationConfigurationAsync(
m_configurationFile!,
silent: false,
CancellationToken.None)
.ConfigureAwait(false);
}
Application.ApplicationName = configuration.ApplicationName;
if (m_certificateManager != null)
{
configuration.CertificateManager = m_certificateManager;
}
m_configureLoadedConfiguration?.Invoke(configuration);
bool haveCertificate = m_certificateManager is null or CertificateManager
? await Application
.CheckApplicationInstanceCertificatesAsync(
silent: true,
CertificateFactory.DefaultLifeTime,
CancellationToken.None)
.ConfigureAwait(false)
: await HasApplicationCertificateAsync(
m_certificateManager,
configuration,
CancellationToken.None).ConfigureAwait(false);
if (!haveCertificate)
{
throw new InvalidOperationException(
"Application instance certificate invalid.");
}
Volatile.Write(ref m_configuration, configuration);
return configuration;
}
private static async Task<bool> HasApplicationCertificateAsync(
ICertificateManager certificateManager,
ApplicationConfiguration configuration,
CancellationToken ct)
{
await certificateManager.UpdateAsync(
configuration.SecurityConfiguration,
configuration.ApplicationUri,
ct).ConfigureAwait(false);
using CertificateEntryCollection certificates =
certificateManager.SnapshotApplicationCertificates();
return certificates.Count > 0;
}
private readonly string? m_configurationFile;
private readonly Stream? m_configurationStream;
private readonly Action<ApplicationConfiguration>? m_configureLoadedConfiguration;
private readonly ICertificateManager? m_certificateManager;
private readonly Lock m_lock = new();
private Task<ApplicationConfiguration>? m_loadTask;
private ApplicationConfiguration? m_configuration;
private int m_disposed;
}
}
@@ -30,6 +30,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
@@ -117,6 +118,126 @@ namespace Microsoft.Extensions.DependencyInjection
return new OpcUaClientBuilder(builder.Services);
}
/// <summary>
/// Registers OPC UA client services whose
/// <see cref="ApplicationConfiguration"/> is loaded from an existing
/// OPC UA XML configuration file (e.g. <c>MyClient.Config.xml</c>).
/// </summary>
/// <remarks>
/// <para>
/// This is the migration path for applications that already own a
/// configuration file: every setting in the file (security
/// configuration, certificate stores, transport quotas, client
/// configuration, ...) is applied as-is, exactly as when the file is
/// loaded through
/// <c>ApplicationInstance.LoadApplicationConfigurationAsync</c>. The
/// document is loaded and the application-instance certificate is
/// ensured on first use (first session connect or reverse-connect
/// startup). Identity providers, reverse connect, and the other
/// <see cref="IOpcUaClientBuilder"/> registrations compose with the
/// loaded file the same way they compose with built configurations.
/// </para>
/// <para>
/// Equivalent to
/// <see cref="AddClient(IOpcUaBuilder, Action{OpcUaClientOptions})"/>
/// with <see cref="OpcUaClientOptions.ConfigurationFile"/> set. Use
/// <paramref name="configure"/> to set the session endpoint and the
/// other client options, and
/// <see cref="OpcUaClientOptions.ConfigureLoadedConfiguration"/>
/// within it to override individual settings of the loaded file from
/// code.
/// </para>
/// </remarks>
/// <param name="builder">The OPC UA builder.</param>
/// <param name="configurationFile">Path to the application
/// configuration XML file. A relative path is resolved against the
/// current working directory.</param>
/// <param name="configure">Optional configuration delegate for the
/// remaining <see cref="OpcUaClientOptions"/> (session, identity,
/// reverse connect, ...). The application identity properties and an
/// explicit <see cref="OpcUaClientOptions.Configuration"/> must not
/// be combined with the configuration file.</param>
/// <returns>An <see cref="IOpcUaClientBuilder"/> for chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/>
/// or <paramref name="configurationFile"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="configurationFile"/>
/// is empty or white space.</exception>
/// <exception cref="InvalidOperationException">The configuration file
/// is combined with an explicit
/// <see cref="OpcUaClientOptions.Configuration"/>, a
/// <see cref="OpcUaClientOptions.ConfigurationStream"/>, or client
/// application identity options.</exception>
public static IOpcUaClientBuilder AddClient(
this IOpcUaBuilder builder,
string configurationFile,
Action<OpcUaClientOptions>? configure = null)
{
if (configurationFile is null)
{
throw new ArgumentNullException(nameof(configurationFile));
}
if (string.IsNullOrWhiteSpace(configurationFile))
{
throw new ArgumentException(
"The configuration file path must not be empty.",
nameof(configurationFile));
}
return builder.AddClient(options =>
{
options.ConfigurationFile = configurationFile;
configure?.Invoke(options);
});
}
/// <summary>
/// Registers OPC UA client services whose
/// <see cref="ApplicationConfiguration"/> is loaded from a stream
/// containing an OPC UA XML configuration document, e.g. an embedded
/// resource.
/// </summary>
/// <remarks>
/// <para>
/// See <see cref="AddClient(IOpcUaBuilder, string, Action{OpcUaClientOptions})"/>
/// for how the loaded configuration is applied. Equivalent to
/// <see cref="AddClient(IOpcUaBuilder, Action{OpcUaClientOptions})"/>
/// with <see cref="OpcUaClientOptions.ConfigurationStream"/> set.
/// </para>
/// <para>
/// The stream must remain open until the configuration is first
/// used; it is read once and disposed after loading.
/// </para>
/// </remarks>
/// <param name="builder">The OPC UA builder.</param>
/// <param name="configurationStream">Stream containing the
/// application configuration XML document.</param>
/// <param name="configure">Optional configuration delegate for the
/// remaining <see cref="OpcUaClientOptions"/>.</param>
/// <returns>An <see cref="IOpcUaClientBuilder"/> for chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/>
/// or <paramref name="configurationStream"/> is <c>null</c>.</exception>
/// <exception cref="InvalidOperationException">The configuration
/// stream is combined with an explicit
/// <see cref="OpcUaClientOptions.Configuration"/>, a
/// <see cref="OpcUaClientOptions.ConfigurationFile"/>, or client
/// application identity options.</exception>
public static IOpcUaClientBuilder AddClient(
this IOpcUaBuilder builder,
Stream configurationStream,
Action<OpcUaClientOptions>? configure = null)
{
if (configurationStream is null)
{
throw new ArgumentNullException(nameof(configurationStream));
}
return builder.AddClient(options =>
{
options.ConfigurationStream = configurationStream;
configure?.Invoke(options);
});
}
/// <summary>
/// Registers OPC UA client services with options bound from the
/// supplied <paramref name="configuration"/> section
@@ -874,7 +995,13 @@ namespace Microsoft.Extensions.DependencyInjection
OpcUaClientOptions options = sp.GetRequiredService<OpcUaClientOptions>();
if (options.ConfigurationProvider != null)
{
await options.ConfigurationProvider.GetAsync(ct).ConfigureAwait(false);
// Completes validation and application-instance certificate
// setup; a lazily loaded supplied configuration document
// (ConfigurationFile / ConfigurationStream) also becomes
// available here, so publish it on the resolved options.
ApplicationConfiguration providedConfiguration = await options
.ConfigurationProvider.GetAsync(ct).ConfigureAwait(false);
options.Configuration ??= providedConfiguration;
}
ValidateClientOptions(options, sessionOptions);
ITelemetryContext telemetry = sp.GetRequiredService<ITelemetryContext>();
@@ -899,7 +1026,7 @@ namespace Microsoft.Extensions.DependencyInjection
IServiceCollection services,
OpcUaClientOptions options)
{
if (options.Configuration == null)
if (options.Configuration == null && !options.HasSuppliedConfigurationDocument)
{
services.TryAddEnumerable(
ServiceDescriptor.Singleton<
@@ -907,16 +1034,48 @@ namespace Microsoft.Extensions.DependencyInjection
OpcUaClientApplicationConfigurationFeature>());
}
if (options.HasSuppliedConfigurationDocument)
{
// Capture the document source eagerly: the provider factory
// must not resolve OpcUaClientOptions, whose own factory
// resolves this provider.
string? configurationFile = options.ConfigurationFile;
Stream? configurationStream = options.ConfigurationStream;
Action<ApplicationConfiguration>? configureLoadedConfiguration =
options.ConfigureLoadedConfiguration;
services.TryAddSingleton(sp => new ClientSuppliedConfigurationProvider(
configurationFile,
configurationStream,
configureLoadedConfiguration,
sp.GetRequiredService<IApplicationInstanceFactory>(),
sp.GetRequiredService<ITelemetryContext>(),
sp.GetService<ICertificateManager>(),
sp.GetService<ICertificatePasswordProvider>()));
}
services.TryAddSingleton<OpcUaClientOptions>(sp =>
{
var resolvedOptions = new OpcUaClientOptions();
CopyClientOptions(options, resolvedOptions);
if (resolvedOptions.Configuration == null)
{
resolvedOptions.ConfigurationProvider =
sp.GetService<IOpcUaApplicationConfigurationProvider>();
resolvedOptions.Configuration =
resolvedOptions.ConfigurationProvider?.Configuration;
if (sp.GetService<ClientSuppliedConfigurationProvider>() is
ClientSuppliedConfigurationProvider suppliedProvider)
{
// An explicitly supplied configuration document is the
// most specific intent and therefore wins over a shared
// application registered via ConfigureApplication(...).
// It loads lazily: Configuration is filled in after the
// first GetAsync completes.
resolvedOptions.ConfigurationProvider = suppliedProvider;
}
else
{
resolvedOptions.ConfigurationProvider =
sp.GetService<IOpcUaApplicationConfigurationProvider>();
resolvedOptions.Configuration =
resolvedOptions.ConfigurationProvider?.Configuration;
}
}
return resolvedOptions;
});
@@ -937,6 +1096,10 @@ namespace Microsoft.Extensions.DependencyInjection
/// <summary>
/// Copies every publicly settable field of <paramref name="source"/>
/// (application identity/security options, <see cref="OpcUaClientOptions.Configuration"/>,
/// the supplied configuration document
/// (<see cref="OpcUaClientOptions.ConfigurationFile"/> /
/// <see cref="OpcUaClientOptions.ConfigurationStream"/> /
/// <see cref="OpcUaClientOptions.ConfigureLoadedConfiguration"/>),
/// <see cref="OpcUaClientOptions.Session"/>, <see cref="OpcUaClientOptions.Identity"/>
/// and <see cref="OpcUaClientOptions.ReverseConnect"/>) into
/// <paramref name="target"/>.
@@ -944,6 +1107,9 @@ namespace Microsoft.Extensions.DependencyInjection
private static void CopyClientOptions(OpcUaClientOptions source, OpcUaClientOptions target)
{
target.Configuration = source.Configuration;
target.ConfigurationFile = source.ConfigurationFile;
target.ConfigurationStream = source.ConfigurationStream;
target.ConfigureLoadedConfiguration = source.ConfigureLoadedConfiguration;
target.ApplicationName = source.ApplicationName;
target.ApplicationUri = source.ApplicationUri;
target.ProductUri = source.ProductUri;
@@ -978,6 +1144,17 @@ namespace Microsoft.Extensions.DependencyInjection
IOpcUaBuilder builder,
OpcUaClientOptions options)
{
ValidateSuppliedConfigurationDocument(options);
if (options.HasSuppliedConfigurationDocument)
{
// The supplied document is loaded through an
// IApplicationInstance created by the shared factory; ensure
// the factory is registered even when no ConfigureApplication
// call is made.
builder.AddApplicationInstance();
return;
}
if (!options.HasApplicationOptions)
{
return;
@@ -999,6 +1176,54 @@ namespace Microsoft.Extensions.DependencyInjection
builder.ConfigureApplication(_ => { });
}
/// <summary>
/// Rejects ambiguous combinations of an existing configuration XML
/// document (<see cref="OpcUaClientOptions.ConfigurationFile"/> /
/// <see cref="OpcUaClientOptions.ConfigurationStream"/>) with other
/// configuration sources. Unlike the argument validation on the
/// <c>AddClient(string, ...)</c> overload, this also covers values
/// that arrive through configuration binding or the options
/// callback.
/// </summary>
/// <exception cref="InvalidOperationException">An invalid
/// combination was configured.</exception>
private static void ValidateSuppliedConfigurationDocument(OpcUaClientOptions options)
{
bool hasConfigurationFile = !string.IsNullOrEmpty(options.ConfigurationFile);
if (hasConfigurationFile && string.IsNullOrWhiteSpace(options.ConfigurationFile))
{
throw new InvalidOperationException(
"OpcUaClientOptions.ConfigurationFile must not be a white-space path.");
}
if (hasConfigurationFile && options.ConfigurationStream != null)
{
throw new InvalidOperationException(
"Set only one of OpcUaClientOptions.ConfigurationFile and " +
"OpcUaClientOptions.ConfigurationStream.");
}
if (!options.HasSuppliedConfigurationDocument)
{
return;
}
if (options.Configuration != null)
{
throw new InvalidOperationException(
"OpcUaClientOptions.ConfigurationFile / ConfigurationStream cannot " +
"be combined with an explicit OpcUaClientOptions.Configuration.");
}
if (options.HasApplicationOptions)
{
throw new InvalidOperationException(
"OpcUaClientOptions.ConfigurationFile / ConfigurationStream cannot " +
"be combined with client application identity options " +
"(ApplicationName, ApplicationUri, ProductUri, SubjectName, " +
"PkiRoot, AutoAcceptUntrustedCertificates, " +
"RejectSHA1SignedCertificates, MinimumCertificateKeySize). The " +
"supplied document is authoritative; use " +
"ConfigureLoadedConfiguration for programmatic overrides.");
}
}
private static void ValidateClientOptions(
OpcUaClientOptions options,
ManagedSessionOptions sessionOptions)
@@ -1369,7 +1594,9 @@ namespace Microsoft.Extensions.DependencyInjection
bool hasConfigurationProvider = false)
{
var failures = new List<string>();
if (options.Configuration == null && !hasConfigurationProvider)
if (options.Configuration == null &&
!hasConfigurationProvider &&
!options.HasSuppliedConfigurationDocument)
{
failures.Add("OpcUaClientOptions.Configuration is required.");
}
+72 -1
View File
@@ -27,6 +27,8 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.IO;
using Opc.Ua.Configuration;
namespace Opc.Ua.Client
@@ -41,10 +43,71 @@ namespace Opc.Ua.Client
/// The application configuration. When omitted,
/// <c>ConfigureApplication(...)</c> must be registered on the root
/// OPC UA builder, or the application identity properties below
/// must be set instead.
/// must be set instead, or the configuration must be supplied as an
/// existing XML document via <see cref="ConfigurationFile"/> /
/// <see cref="ConfigurationStream"/>.
/// </summary>
public ApplicationConfiguration? Configuration { get; set; }
/// <summary>
/// Optional path to a classic OPC UA application configuration XML
/// file (e.g. <c>MyClient.Config.xml</c>). When set, the client
/// loads the <see cref="ApplicationConfiguration"/> from this file
/// instead of building one, so existing applications can adopt
/// dependency injection and keep every setting from their
/// configuration file (security configuration, certificate stores,
/// transport quotas, client configuration, ...).
/// </summary>
/// <remarks>
/// <para>
/// A relative path is resolved against the current working
/// directory. The file must validate as a client (or
/// client-and-server) configuration. The document is loaded and the
/// application-instance certificate is ensured on first use (first
/// session connect, reverse-connect startup, or an explicit
/// <c>GetAsync</c> on the resolved configuration provider),
/// mirroring how a shared <c>ConfigureApplication(...)</c>
/// application completes asynchronously.
/// </para>
/// <para>
/// Because the file is authoritative, it must not be combined with
/// an explicit <see cref="Configuration"/> or with the application
/// identity properties (<see cref="ApplicationName"/>, ...); use
/// <see cref="ConfigureLoadedConfiguration"/> for programmatic
/// overrides of the loaded file. This path also takes precedence
/// over a shared application registered with
/// <c>ConfigureApplication(...)</c>. Mutually exclusive with
/// <see cref="ConfigurationStream"/>.
/// </para>
/// </remarks>
public string? ConfigurationFile { get; set; }
/// <summary>
/// Optional stream containing a classic OPC UA application
/// configuration XML document, e.g. an embedded resource. When set,
/// the client loads the <see cref="ApplicationConfiguration"/> from
/// this stream with the same semantics as
/// <see cref="ConfigurationFile"/>.
/// </summary>
/// <remarks>
/// The stream must remain open until the configuration is first
/// used; it is read once and disposed after loading. Mutually
/// exclusive with <see cref="ConfigurationFile"/>.
/// </remarks>
public Stream? ConfigurationStream { get; set; }
/// <summary>
/// Optional callback invoked with the <see cref="ApplicationConfiguration"/>
/// loaded from <see cref="ConfigurationFile"/> or
/// <see cref="ConfigurationStream"/>, after the document has been
/// read and validated but before the application-instance
/// certificate is checked. Use it to override individual settings
/// from code without editing the document. Ignored when neither
/// <see cref="ConfigurationFile"/> nor
/// <see cref="ConfigurationStream"/> is set.
/// </summary>
public Action<ApplicationConfiguration>? ConfigureLoadedConfiguration { get; set; }
/// <summary>
/// The application name. When set (and <see cref="Configuration"/>
/// is omitted), the root <c>ConfigureApplication(...)</c>
@@ -153,5 +216,13 @@ namespace Opc.Ua.Client
|| AutoAcceptUntrustedCertificates != null
|| RejectSHA1SignedCertificates != null
|| MinimumCertificateKeySize != null;
/// <summary>
/// <c>true</c> when an existing configuration XML document was
/// supplied via <see cref="ConfigurationFile"/> or
/// <see cref="ConfigurationStream"/>.
/// </summary>
internal bool HasSuppliedConfigurationDocument =>
!string.IsNullOrEmpty(ConfigurationFile) || ConfigurationStream != null;
}
}
+5
View File
@@ -44,6 +44,11 @@ var sessionFactory =
ManagedSession session = await sessionFactory(ct);
```
Applications that already own a classic configuration XML file can pass
it directly — `AddClient("MyClient.Config.xml", ...)`, or as a `Stream`
for embedded resources — and keep every setting from the file while
adopting dependency injection.
## Reverse connect lifecycle
`ReverseConnectManager` uses an asynchronous, cancellation-aware lifecycle:
@@ -0,0 +1,421 @@
/* ========================================================================
* 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/
* ======================================================================*/
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NUnit.Framework;
using Opc.Ua.Configuration;
namespace Opc.Ua.Client.Tests.ClientBuilder
{
/// <summary>
/// Exercises the <c>AddClient(configurationFile)</c> and
/// <c>AddClient(configurationStream)</c> migration paths: OPC UA client
/// services whose <see cref="ApplicationConfiguration"/> is loaded from
/// an existing OPC UA XML configuration document instead of being built
/// from options or the shared application. Covers the new
/// <c>IOpcUaBuilder</c> overloads, the
/// <see cref="OpcUaClientOptions.ConfigurationFile"/> /
/// <see cref="OpcUaClientOptions.ConfigurationStream"/> options and the
/// configuration-section binding, the lazily loading configuration
/// provider (including the application-instance certificate bootstrap
/// and the <see cref="OpcUaClientOptions.ConfigureLoadedConfiguration"/>
/// override callback), precedence over <c>ConfigureApplication(...)</c>,
/// ambiguous-combination validation, and load failures.
/// </summary>
[TestFixture]
[Category("Client")]
[Category("ClientBuilder")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public sealed class ClientConfigurationFileTests
{
[Test]
public async Task AddClientLoadsConfigurationFromXmlFileAsync()
{
const string applicationName = "CfgXmlClient";
string testRoot = CreateTestRoot();
string configurationFile = WriteConfigurationFile(testRoot, applicationName);
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient(
configurationFile,
options => options.ConfigureLoadedConfiguration = configuration =>
configuration.ClientConfiguration!.MinSubscriptionLifetime = 4242);
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions resolvedOptions =
provider.GetRequiredService<OpcUaClientOptions>();
// The supplied document loads lazily: the provider is wired but
// the configuration is not materialized until first use.
Assert.That(resolvedOptions.Configuration, Is.Null);
Assert.That(resolvedOptions.ConfigurationProvider, Is.Not.Null);
ApplicationConfiguration configuration = await resolvedOptions
.ConfigurationProvider!.GetAsync().ConfigureAwait(false);
// Every setting comes from the file; the override callback wins
// over the file value for the setting it touches.
Assert.That(configuration.SourceFilePath, Is.EqualTo(configurationFile));
Assert.That(configuration.ApplicationName, Is.EqualTo(applicationName));
Assert.That(
configuration.ApplicationUri,
Is.EqualTo("urn:opcfoundation:test:" + applicationName));
Assert.That(configuration.TransportQuotas!.MaxStringLength, Is.EqualTo(654321));
Assert.That(configuration.ClientConfiguration, Is.Not.Null);
Assert.That(
configuration.ClientConfiguration!.DefaultSessionTimeout,
Is.EqualTo(123456));
Assert.That(
configuration.ClientConfiguration.MinSubscriptionLifetime,
Is.EqualTo(4242));
// The load ran the client application-instance certificate
// bootstrap: the own store now holds the application certificate.
Assert.That(
Directory.Exists(Path.Combine(testRoot, "pki", "own")) &&
Directory.GetFiles(
Path.Combine(testRoot, "pki", "own"),
"*",
SearchOption.AllDirectories).Length > 0,
Is.True,
"The application instance certificate was not created.");
// The provider caches the loaded document.
ApplicationConfiguration second = await resolvedOptions
.ConfigurationProvider.GetAsync().ConfigureAwait(false);
Assert.That(second, Is.SameAs(configuration));
Assert.That(
resolvedOptions.ConfigurationProvider.Configuration,
Is.SameAs(configuration));
}
[Test]
public async Task AddClientLoadsConfigurationFromStreamAsync()
{
const string applicationName = "CfgXmlStreamClient";
string testRoot = CreateTestRoot();
var stream = new MemoryStream(Encoding.UTF8.GetBytes(
BuildConfigurationXml(applicationName, GetPkiRoot(testRoot))));
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient(stream);
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions resolvedOptions =
provider.GetRequiredService<OpcUaClientOptions>();
ApplicationConfiguration configuration = await resolvedOptions
.ConfigurationProvider!.GetAsync().ConfigureAwait(false);
// A stream has no source file path; the settings come from the
// document and the stream is disposed after the single read.
Assert.That(configuration.SourceFilePath, Is.Null);
Assert.That(configuration.ApplicationName, Is.EqualTo(applicationName));
Assert.That(configuration.TransportQuotas!.MaxStringLength, Is.EqualTo(654321));
Assert.That(stream.CanRead, Is.False);
}
[Test]
public async Task SuppliedDocumentTakesPrecedenceOverConfigureApplicationAsync()
{
const string applicationName = "CfgXmlPrecedenceClient";
string testRoot = CreateTestRoot();
string configurationFile = WriteConfigurationFile(testRoot, applicationName);
var services = new ServiceCollection();
services.AddLogging();
IOpcUaBuilder builder = services.AddOpcUa()
.ConfigureApplication(options =>
{
options.ApplicationName = "SharedClientApplication";
options.ApplicationUri = "urn:localhost:SharedClientApplication";
options.PkiRoot = Path.Combine(testRoot, "sharedpki");
options.AutoAcceptUntrustedCertificates = true;
});
builder.AddClient(configurationFile);
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions resolvedOptions =
provider.GetRequiredService<OpcUaClientOptions>();
ApplicationConfiguration configuration = await resolvedOptions
.ConfigurationProvider!.GetAsync().ConfigureAwait(false);
Assert.That(configuration.SourceFilePath, Is.EqualTo(configurationFile));
Assert.That(configuration.ApplicationName, Is.EqualTo(applicationName));
}
[Test]
public async Task MissingConfigurationFileFailsGetAsyncAsync()
{
string missingFile = Path.Combine(CreateTestRoot(), "DoesNotExist.Config.xml");
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient(missingFile);
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions resolvedOptions =
provider.GetRequiredService<OpcUaClientOptions>();
Assert.That(
() => resolvedOptions.ConfigurationProvider!.GetAsync(),
Throws.InstanceOf<ServiceResultException>());
Assert.That(
() => resolvedOptions.ConfigurationProvider!.Configuration,
Throws.InvalidOperationException);
}
[Test]
public void AddClientSuppliedDocumentValidatesArguments()
{
var services = new ServiceCollection();
IOpcUaBuilder builder = services.AddOpcUa();
Assert.That(
() => default(IOpcUaBuilder)!.AddClient("Client.Config.xml"),
Throws.ArgumentNullException.With
.Property(nameof(ArgumentNullException.ParamName)).EqualTo("builder"));
Assert.That(
() => builder.AddClient((string)null!),
Throws.ArgumentNullException.With
.Property(nameof(ArgumentNullException.ParamName)).EqualTo("configurationFile"));
Assert.That(
() => builder.AddClient(" "),
Throws.TypeOf<ArgumentException>().With
.Property(nameof(ArgumentException.ParamName)).EqualTo("configurationFile"));
using var stream = new MemoryStream();
Assert.That(
() => default(IOpcUaBuilder)!.AddClient(stream),
Throws.ArgumentNullException.With
.Property(nameof(ArgumentNullException.ParamName)).EqualTo("builder"));
Assert.That(
() => builder.AddClient((Stream)null!),
Throws.ArgumentNullException.With
.Property(nameof(ArgumentNullException.ParamName)).EqualTo("configurationStream"));
}
[Test]
public void AddClientSuppliedDocumentRejectsAmbiguousCombinations()
{
using var stream = new MemoryStream();
// File and stream together.
Assert.That(
() => new ServiceCollection().AddOpcUa().AddClient(options =>
{
options.ConfigurationFile = "Client.Config.xml";
options.ConfigurationStream = stream;
}),
Throws.InvalidOperationException.With.Message.Contains("only one"));
// Supplied document combined with an explicit Configuration.
Assert.That(
() => new ServiceCollection().AddOpcUa().AddClient(options =>
{
options.ConfigurationFile = "Client.Config.xml";
options.Configuration = CreateConfiguration();
}),
Throws.InvalidOperationException.With.Message.Contains("Configuration"));
// Supplied document combined with application identity options.
Assert.That(
() => new ServiceCollection().AddOpcUa().AddClient(options =>
{
options.ConfigurationFile = "Client.Config.xml";
options.ApplicationName = "SomeClient";
}),
Throws.InvalidOperationException.With.Message.Contains("identity options"));
// A white-space path that bypasses the AddClient(string) argument
// validation (e.g. from configuration binding).
Assert.That(
() => new ServiceCollection().AddOpcUa().AddClient(options =>
options.ConfigurationFile = " "),
Throws.InvalidOperationException.With.Message.Contains("white-space"));
}
[Test]
public async Task AddClientBindsConfigurationFileFromConfigurationSectionAsync()
{
IConfiguration configuration = new ConfigurationBuilder()
.Add(new MemoryConfigurationSource
{
InitialData = new Dictionary<string, string?>
{
["OpcUa:Client:ConfigurationFile"] = "Legacy/Client.Config.xml"
}
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient(configuration);
// The supplied-document provider is IAsyncDisposable-only (like
// the shared application provider), so the container must be
// disposed asynchronously once the provider was instantiated.
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions options = provider.GetRequiredService<OpcUaClientOptions>();
Assert.That(options.ConfigurationFile, Is.EqualTo("Legacy/Client.Config.xml"));
Assert.That(options.ConfigurationProvider, Is.Not.Null);
}
[Test]
public void OptionsValidationAcceptsSuppliedDocument()
{
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient("Client.Config.xml");
using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions boundOptions =
provider.GetRequiredService<IOptions<OpcUaClientOptions>>().Value;
// The start-time validator must accept a supplied document as a
// configuration source even though no shared application
// provider is registered and Configuration is still null.
foreach (IValidateOptions<OpcUaClientOptions> validator in
provider.GetServices<IValidateOptions<OpcUaClientOptions>>())
{
ValidateOptionsResult result =
validator.Validate(Options.DefaultName, boundOptions);
Assert.That(result.Failed, Is.False, result.FailureMessage);
}
}
private static string CreateTestRoot()
{
// Keep the root short: certificate file names would otherwise
// push the PFX path past MAX_PATH on .NET Framework.
string testRoot = Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"ccfg",
Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(testRoot);
return testRoot;
}
private static string GetPkiRoot(string testRoot)
{
return Path.Combine(testRoot, "pki").Replace('\\', '/');
}
private static ApplicationConfiguration CreateConfiguration()
{
return new ApplicationConfiguration(
Opc.Ua.Tests.NUnitTelemetryContext.Create());
}
private static string WriteConfigurationFile(string testRoot, string applicationName)
{
string configurationFile = Path.Combine(testRoot, applicationName + ".Config.xml");
File.WriteAllText(
configurationFile,
BuildConfigurationXml(applicationName, GetPkiRoot(testRoot)));
return configurationFile;
}
/// <summary>
/// Builds a classic OPC UA client configuration XML document of the
/// shape existing applications carry (security configuration,
/// transport quotas, client configuration) with distinctive values
/// the tests can assert on.
/// </summary>
private static string BuildConfigurationXml(string applicationName, string pkiRoot)
{
return $"""
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfiguration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"
xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd">
<ApplicationName>{applicationName}</ApplicationName>
<ApplicationUri>urn:opcfoundation:test:{applicationName}</ApplicationUri>
<ProductUri>uri:opcfoundation.org:test:{applicationName}</ProductUri>
<ApplicationType>Client_1</ApplicationType>
<SecurityConfiguration>
<ApplicationCertificates>
<CertificateIdentifier>
<StoreType>Directory</StoreType>
<StorePath>{pkiRoot}/own</StorePath>
<SubjectName>CN={applicationName}, O=OPC Foundation, DC=localhost</SubjectName>
<CertificateTypeString>RsaSha256</CertificateTypeString>
</CertificateIdentifier>
</ApplicationCertificates>
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>{pkiRoot}/issuer</StorePath>
</TrustedIssuerCertificates>
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>{pkiRoot}/trusted</StorePath>
</TrustedPeerCertificates>
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>{pkiRoot}/rejected</StorePath>
</RejectedCertificateStore>
<AutoAcceptUntrustedCertificates>true</AutoAcceptUntrustedCertificates>
</SecurityConfiguration>
<TransportConfigurations></TransportConfigurations>
<TransportQuotas>
<OperationTimeout>90000</OperationTimeout>
<MaxStringLength>654321</MaxStringLength>
<MaxByteStringLength>1048576</MaxByteStringLength>
<MaxArrayLength>65535</MaxArrayLength>
<MaxMessageSize>4194304</MaxMessageSize>
<MaxBufferSize>65535</MaxBufferSize>
<ChannelLifetime>300000</ChannelLifetime>
<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
</TransportQuotas>
<ClientConfiguration>
<DefaultSessionTimeout>123456</DefaultSessionTimeout>
<MinSubscriptionLifetime>10000</MinSubscriptionLifetime>
</ClientConfiguration>
</ApplicationConfiguration>
""";
}
}
}