/* ======================================================================== * 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.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Opc.Ua.Di.Server; using Opc.Ua.Di.Server.Hosting; using Opc.Ua.Server.Hosting; namespace Microsoft.Extensions.DependencyInjection { /// /// extensions that integrate the /// OPC UA Device Integration (DI, OPC 10000-100) server with the /// unified AddOpcUa() Microsoft.Extensions DI hosting model. /// /// /// /// Usage: /// /// /// services.AddOpcUa() /// .AddServer(o => { ... }) /// .AddOpcUaDi() // plain DI manager /// .ConfigureDevicesFor<DiNodeManager>(ctx => /// { /// ctx.CreateDeviceAsync(...).AsTask().Wait(); /// }); /// /// /// For companion-spec managers that already embed DI (e.g. a pump /// manager that loads DI + Machinery + Pumps node sets), do not /// call — register the companion factory /// directly via /// and use /// ConfigureDevicesFor against the companion manager type. /// Calling AddOpcUaDi() alongside a companion-spec manager /// would double-register the DI namespace. /// /// public static class OpcUaServerDiBuilderExtensions { /// /// Registers the so a plain /// DI server (no companion specs) can be hosted, plus the /// shared used by every /// DI-aware manager to dispatch /// instances. /// /// The server builder. /// The same builder, for chaining. /// /// is null. /// /// /// has already been called on this /// service collection. /// public static IOpcUaServerBuilder AddOpcUaDi(this IOpcUaServerBuilder builder) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } // Add the shared runner exactly once. Multiple calls to // ConfigureDevicesFor share the same runner. EnsureRunnerRegistered(builder); // Fail fast on duplicate AddOpcUaDi calls. foreach (ServiceDescriptor d in builder.Services) { if (d.ServiceType == typeof(DiNodeManagerRegistrationMarker)) { throw new InvalidOperationException( "AddOpcUaDi has already been called. " + "At most one DiNodeManagerFactory may be registered per service collection."); } } builder.Services.AddSingleton(); // Register the factory through the existing AddNodeManager // pipeline so the hosted service picks it up. return builder.AddNodeManager(); } /// /// Registers a synchronous device-configuration delegate that /// runs once the address space of a /// instance is initialised. /// Multiple registrations run in registration order. /// /// /// The exact (or base) DI node-manager type to target. Use /// for plain DI managers or a /// companion-spec subclass for targeted configuration. /// /// The server builder. /// Configuration delegate. /// The same builder, for chaining. public static IOpcUaServerBuilder ConfigureDevicesFor( this IOpcUaServerBuilder builder, Action configure) where TNodeManager : DiNodeManager { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } return builder.ConfigureDevicesFor((Func)(ctx => { configure(ctx); return default; })); } /// /// Asynchronous overload of /// . /// The delegate receives the same context and is awaited inline /// by the runner; thrown exceptions abort hosted-server startup. /// /// Target node-manager subclass. public static IOpcUaServerBuilder ConfigureDevicesFor( this IOpcUaServerBuilder builder, Func configure) where TNodeManager : DiNodeManager { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } EnsureRunnerRegistered(builder); builder.Services.AddSingleton( new DelegateDiPostSetupConfigurator(typeof(TNodeManager), configure)); return builder; } private static void EnsureRunnerRegistered(IOpcUaServerBuilder builder) { builder.Services.TryAddSingleton(); } /// /// Marker type used to detect duplicate /// calls. /// private sealed class DiNodeManagerRegistrationMarker { } /// /// Delegate-backed configurator created by the /// ConfigureDevicesFor extension methods. /// private sealed class DelegateDiPostSetupConfigurator : IDiPostSetupConfigurator { private readonly Func m_configure; public DelegateDiPostSetupConfigurator( Type targetManagerType, Func configure) { TargetManagerType = targetManagerType ?? throw new ArgumentNullException(nameof(targetManagerType)); m_configure = configure ?? throw new ArgumentNullException(nameof(configure)); } public Type TargetManagerType { get; } public ValueTask RunAsync(IDiPostSetupContext context) { return m_configure(context); } } } }