/* ======================================================================== * Copyright (c) 2005-2025 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.Threading; using System.Threading.Tasks; using Opc.Ua.Identity; namespace Opc.Ua.Client { /// /// Fluent builder for constructing a . The /// builder accumulates an immutable /// snapshot via With*/Use*/Configure methods. Call /// to obtain the snapshot, or /// to construct and open the session in one /// step. /// /// /// /// var session = await new ManagedSessionBuilder(configuration, telemetry) /// .UseEndpoint(endpoint) /// .WithSessionName("My App") /// .WithReconnectPolicy(p => p with { Strategy = BackoffStrategy.Linear }) /// .ConnectAsync(ct); /// /// public sealed class ManagedSessionBuilder { private readonly ApplicationConfiguration m_configuration; private readonly ITelemetryContext m_telemetry; private ManagedSessionOptions m_options = new(); private IReconnectPolicy? m_reconnectPolicy; private IServerRedundancyHandler? m_redundancyHandler; private ISessionFactory? m_sessionFactory; /// /// Initializes a new builder. /// public ManagedSessionBuilder( ApplicationConfiguration configuration, ITelemetryContext telemetry) { m_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); m_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry)); } /// /// Use the supplied configured endpoint. /// /// is null. public ManagedSessionBuilder UseEndpoint(ConfiguredEndpoint endpoint) { if (endpoint == null) { throw new ArgumentNullException(nameof(endpoint)); } m_options = m_options with { Endpoint = endpoint }; return this; } /// /// Use a discovery endpoint URL with optional security mode/policy. /// /// is null. public ManagedSessionBuilder UseEndpoint( string url, MessageSecurityMode securityMode = MessageSecurityMode.SignAndEncrypt, string securityPolicyUri = SecurityPolicies.Basic256Sha256) { if (url == null) { throw new ArgumentNullException(nameof(url)); } var endpointDescription = new EndpointDescription { EndpointUrl = url, SecurityMode = securityMode, SecurityPolicyUri = securityPolicyUri }; return UseEndpoint(new ConfiguredEndpoint(null, endpointDescription, null)); } /// /// Set the user identity used when activating the session. /// /// is null. public ManagedSessionBuilder WithUserIdentity(IUserIdentity identity) { if (identity == null) { throw new ArgumentNullException(nameof(identity)); } #pragma warning disable CS0618 // Legacy eager identity remains supported for compatibility. m_options = m_options with { Identity = identity }; #pragma warning restore CS0618 return this; } /// /// Set the lazy identity provider used to refresh identities after connect. /// /// is null. public ManagedSessionBuilder WithIdentityProvider(IClientIdentityProvider provider) { if (provider == null) { throw new ArgumentNullException(nameof(provider)); } m_options = m_options with { IdentityProvider = provider }; return this; } /// /// Set the time provider used by proactive identity refresh. /// /// is null. public ManagedSessionBuilder WithTimeProvider(TimeProvider timeProvider) { if (timeProvider == null) { throw new ArgumentNullException(nameof(timeProvider)); } m_options = m_options with { TimeProvider = timeProvider }; return this; } /// /// Set the session display name. /// /// public ManagedSessionBuilder WithSessionName(string name) { if (string.IsNullOrEmpty(name)) { throw new ArgumentException("Value required", nameof(name)); } m_options = m_options with { SessionName = name }; return this; } /// /// Set the requested session timeout. /// /// public ManagedSessionBuilder WithSessionTimeout(TimeSpan timeout) { if (timeout <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(timeout)); } m_options = m_options with { SessionTimeout = timeout }; return this; } /// /// Set the preferred locales for the session. /// /// is null. public ManagedSessionBuilder WithPreferredLocales(params string[] locales) { if (locales == null) { throw new ArgumentNullException(nameof(locales)); } m_options = m_options with { PreferredLocales = locales }; return this; } /// /// Enable or disable server-certificate domain validation. /// public ManagedSessionBuilder WithCheckDomain(bool checkDomain = true) { m_options = m_options with { CheckDomain = checkDomain }; return this; } /// /// Configure the reconnect policy via a transformation of the /// underlying record. /// /// is null. public ManagedSessionBuilder WithReconnectPolicy( Func configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } m_options = m_options with { ReconnectPolicy = configure(m_options.ReconnectPolicy) }; m_reconnectPolicy = null; return this; } /// /// Use the supplied directly. Overrides /// any options-based reconnect configuration. /// /// public ManagedSessionBuilder WithReconnectPolicy(IReconnectPolicy policy) { m_reconnectPolicy = policy ?? throw new ArgumentNullException(nameof(policy)); return this; } /// /// Enable server redundancy with a default handler. /// public ManagedSessionBuilder WithServerRedundancy() { m_options = m_options with { EnableServerRedundancy = true }; m_redundancyHandler = null; return this; } /// /// Enable server redundancy with the supplied handler. /// /// public ManagedSessionBuilder WithServerRedundancy(IServerRedundancyHandler handler) { m_redundancyHandler = handler ?? throw new ArgumentNullException(nameof(handler)); m_options = m_options with { EnableServerRedundancy = true }; return this; } /// /// Use a specific subscription engine factory. Defaults to the V2 /// engine when not specified. /// /// is null. public ManagedSessionBuilder UseSubscriptionEngine(ISubscriptionEngineFactory factory) { if (factory == null) { throw new ArgumentNullException(nameof(factory)); } m_options = m_options with { SubscriptionEngineFactory = factory }; return this; } /// /// Opt the V2 subscription engine into transfer-on-recreate. /// When enabled, the V2 SubscriptionManager attempts to /// transfer existing server-side subscriptions from the /// previous session to the new one on each session re-create /// (e.g. failover via /// ) and falls back /// to per-subscription recreate when transfer is not /// available. Disabled by default — recreate is the /// universal, server-agnostic fallback; transfer requires /// server support. /// /// /// Has no effect when the classic subscription engine is in /// use; the classic engine drives recreate through the /// 's template-based path. /// public ManagedSessionBuilder WithTransferSubscriptionsOnRecreate( bool transferOnRecreate = true) { m_options = m_options with { TransferSubscriptionsOnRecreate = transferOnRecreate }; return this; } /// /// Enable activator-level pooling of V2 subscription /// notification payload instances. When enabled, the V2 /// subscription dispatcher calls /// on notification /// payload objects (such as MonitoredItemNotification) /// after each handler dispatch, releasing them back to their /// activator's pool for reuse on the next publish. Handlers /// that retain values past the dispatch call must copy them. /// Disabled by default. /// /// /// Has no effect when the classic subscription engine is in /// use; this option only applies to the V2 /// . /// public ManagedSessionBuilder WithPoolNotifications( bool poolNotifications = true) { m_options = m_options with { PoolNotifications = poolNotifications }; return this; } /// /// Enables address-space model change tracking. The session /// auto-starts a /// after connect that invalidates the node cache and exposes /// changes via . /// Disabled by default. /// public ManagedSessionBuilder WithModelChangeTracking( bool enabled = true) { m_options = m_options with { ModelChangeTracking = enabled }; return this; } /// /// Use a specific session factory. By default, the builder /// creates a new configured with /// the V2 subscription engine. /// /// public ManagedSessionBuilder UseSessionFactory(ISessionFactory factory) { m_sessionFactory = factory ?? throw new ArgumentNullException(nameof(factory)); return this; } /// /// Returns the immutable options snapshot accumulated so far. /// public ManagedSessionOptions Build() { return m_options; } /// /// Construct and connect a using the /// accumulated options. /// /// public async Task ConnectAsync(CancellationToken ct = default) { ManagedSessionOptions opts = m_options; if (opts.Endpoint == null) { throw new InvalidOperationException( "ManagedSessionBuilder.UseEndpoint must be called before ConnectAsync."); } ISubscriptionEngineFactory engineFactory = opts.SubscriptionEngineFactory ?? (opts.TimeProvider == null ? DefaultSubscriptionEngineFactory.Instance : new DefaultSubscriptionEngineFactory(opts.TimeProvider)); ISessionFactory sessionFactory = m_sessionFactory ?? new DefaultSessionFactory(m_telemetry) { SubscriptionEngineFactory = engineFactory, TimeProvider = opts.TimeProvider }; IReconnectPolicy reconnect = m_reconnectPolicy ?? new ReconnectPolicy(opts.ReconnectPolicy); IServerRedundancyHandler? redundancy = m_redundancyHandler ?? (opts.EnableServerRedundancy ? new DefaultServerRedundancyHandler() : null); ArrayOf preferredLocales = default; if (opts.PreferredLocales is { Count: > 0 } locales) { string[] arr = new string[locales.Count]; for (int i = 0; i < locales.Count; i++) { arr[i] = locales[i]; } preferredLocales = new ArrayOf(arr); } #pragma warning disable CS0618 // Legacy eager identity remains supported when no provider is configured. IUserIdentity? identity = opts.Identity; #pragma warning restore CS0618 ManagedSession session = await ManagedSession.CreateAsync( m_configuration, opts.Endpoint, sessionFactory, identity, reconnect, redundancy, m_telemetry, opts.SessionName, (uint)opts.SessionTimeout.TotalMilliseconds, preferredLocales, opts.CheckDomain, engineFactory, opts.TransferSubscriptionsOnRecreate, opts.PoolNotifications, opts.IdentityProvider, opts.TimeProvider, ct).ConfigureAwait(false); if (opts.ModelChangeTracking) { await session.EnableModelChangeTrackingAsync(ct).ConfigureAwait(false); } return session; } } }