/* ======================================================================== * 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; namespace Opc.Ua.Server.Fluent { /// /// Extension methods that create new instance nodes via a user-supplied /// factory delegate. Designed to integrate with the source generator's /// emitted CreateXxx factory methods for AOT-safe instance /// creation, while also accepting hand-rolled factories. /// /// /// /// Two flavours are exposed: /// /// /// /// /// — typed factory. Use when the caller already has a /// concrete state class and an instance factory. Pair with /// generated factories like /// context.CreatePumpType(parent, browseName). /// /// /// /// — same as above but also stamps an explicit /// TypeDefinitionId on the new instance. /// /// /// /// The new instance is attached to the parent via /// ; if the /// owning manager needs the instance indexed in /// PredefinedNodes for direct NodeId lookup, the caller is /// responsible for invoking /// AsyncCustomNodeManager.AddPredefinedNodeAsync with the /// returned . /// /// /// NodeIds for the new instance follow the /// {parentIdentifier}_{browseName} pattern used by the /// generated NodeIdFactory and the existing /// /// helper. /// /// public static class InstanceCreationBuilderExtensions { /// /// Creates a new instance of under /// the resolved parent. /// /// Concrete instance state class. /// The owning parent builder. /// Browse name of the new instance. /// /// Factory that constructs the state instance, typically a /// generated method such as /// (p) => context.CreatePumpType(p, browseName). /// /// A typed instance builder for further configuration. public static IInstanceBuilder CreateInstance( this INodeBuilder parent, QualifiedName browseName, Func factory) where TState : BaseInstanceState { return CreateInstance(parent, browseName, NodeId.Null, factory); } /// /// Creates a new instance of under /// the resolved parent and stamps an explicit /// TypeDefinitionId. /// /// Concrete instance state class. public static IInstanceBuilder CreateInstance( this INodeBuilder parent, QualifiedName browseName, NodeId typeDefinitionId, Func factory) where TState : BaseInstanceState { if (parent == null) { throw new ArgumentNullException(nameof(parent)); } if (browseName.IsNull) { throw new ArgumentNullException(nameof(browseName)); } if (factory == null) { throw new ArgumentNullException(nameof(factory)); } string symbolicName = browseName.Name ?? string.Empty; TState instance = factory(parent.Node) ?? throw ServiceResultException.Create( StatusCodes.BadInvalidArgument, "Factory returned null for instance '{0}'.", browseName); instance.SymbolicName = symbolicName; instance.BrowseName = browseName; instance.DisplayName = new LocalizedText(symbolicName); string parentIdentifier = parent.Node.NodeId.IdentifierAsString; instance.NodeId = new NodeId( string.Concat(parentIdentifier, "_", symbolicName), parent.Node.NodeId.NamespaceIndex); if (!typeDefinitionId.IsNull) { instance.TypeDefinitionId = typeDefinitionId; } parent.Node.AddChild(instance); return new InstanceBuilder(parent, instance); } } /// /// Typed fluent builder for an instance node created via /// . /// /// Concrete instance state class. public interface IInstanceBuilder where TState : BaseInstanceState { /// /// The newly created instance. /// TState Node { get; } /// /// The owning parent builder. /// INodeBuilder Parent { get; } /// /// Returns a view of the new /// instance for further fluent wiring (e.g. /// WithProperty, Organizes, CreateLimitAlarm). /// INodeBuilder AsNode(); /// /// Invokes with the typed /// view of the new instance. /// Use for inline configuration: /// /// parent.CreateInstance<PumpTypeState>("Pump #2", factory) /// .Configure(b => b.WithProperty("Manufacturer", "...")); /// /// IInstanceBuilder Configure(Action> configure); /// /// Returns control to the parent builder for further chaining. /// INodeBuilder Done(); } /// /// Internal implementation of . /// Reuses the same ad-hoc builder used by /// /// so chained fluent calls work uniformly. /// /// Concrete instance state class. internal sealed class InstanceBuilder : IInstanceBuilder where TState : BaseInstanceState { public InstanceBuilder(INodeBuilder parent, TState node) { Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Node = node ?? throw new ArgumentNullException(nameof(node)); } public TState Node { get; } public INodeBuilder Parent { get; } public INodeBuilder AsNode() { return new AdHocInstanceNodeBuilder(Parent.Builder, Node); } public IInstanceBuilder Configure(Action> configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } configure(AsNode()); return this; } public INodeBuilder Done() { return Parent; } } /// /// Minimal typed node builder for instances that don't live in the /// manager's PredefinedNodes dictionary. Mirrors the structure /// of ReferenceBuilderExtensions.AdHocNodeBuilder but exposes /// a strongly-typed property. /// /// Concrete node state class. internal sealed class AdHocInstanceNodeBuilder : INodeBuilder where TState : NodeState { internal AdHocInstanceNodeBuilder(INodeManagerBuilder owner, TState node) { Builder = owner ?? throw new ArgumentNullException(nameof(owner)); Node = node ?? throw new ArgumentNullException(nameof(node)); } public TState Node { get; } NodeState INodeBuilder.Node => Node; NodeState INodeStateBuilder.Node => Node; public INodeManagerBuilder Builder { get; } INodeStateBuilder INodeStateBuilder.Child(QualifiedName browseName) { return Child(browseName); } INodeStateBuilder INodeStateBuilder.Child(QualifiedName browseName) { return Child(browseName); } public INodeBuilder As() where TOther : NodeState { if (Node is not TOther typed) { throw ServiceResultException.Create( StatusCodes.BadTypeMismatch, "Node '{0}' is not assignable to '{1}'.", Node.BrowseName, typeof(TOther).Name); } return new AdHocInstanceNodeBuilder(Builder, typed); } public INodeBuilder OnRead(NodeValueEventHandler handler) { return SetVariable(v => v.OnReadValue = handler); } public INodeBuilder OnRead(NodeValueSimpleEventHandler handler) { return SetVariable(v => v.OnSimpleReadValue = handler); } public INodeBuilder OnWrite(NodeValueEventHandler handler) { return SetVariable(v => v.OnWriteValue = handler); } public INodeBuilder OnWrite(NodeValueSimpleEventHandler handler) { return SetVariable(v => v.OnSimpleWriteValue = handler); } public INodeBuilder OnRead(NodeValueEventHandlerAsync handler) { return SetVariable(v => v.OnReadValueAsync = handler); } public INodeBuilder OnRead(NodeValueSimpleEventHandlerAsync handler) { return SetVariable(v => v.OnSimpleReadValueAsync = handler); } public INodeBuilder OnWrite(NodeValueWriteEventHandlerAsync handler) { return SetVariable(v => v.OnWriteValueAsync = handler); } public INodeBuilder OnWrite(NodeValueSimpleWriteEventHandlerAsync handler) { return SetVariable(v => v.OnSimpleWriteValueAsync = handler); } public INodeBuilder OnCall(GenericMethodCalledEventHandler2 handler) { return SetMethod(m => m.OnCallMethod2 = handler); } public INodeBuilder OnCall(GenericMethodCalledEventHandler2Async handler) { return SetMethod(m => m.OnCallMethod2Async = handler); } public INodeBuilder OnNodeAdded(NodeLifecycleHandler handler) { handler(Builder.Context, Node); return this; } public INodeBuilder OnNodeRemoved(NodeLifecycleHandler handler) { return this; } public INodeBuilder OnHistoryRead(HistoryReadHandler handler) { return this; } public INodeBuilder OnHistoryUpdate(HistoryUpdateHandler handler) { return this; } public INodeBuilder OnConditionRefresh(ConditionRefreshHandler handler) { return this; } public INodeBuilder OnMonitoredItemCreated(MonitoredItemCreatedHandler handler) { return this; } public INodeBuilder OnEvent(EventNotificationHandler handler) { Node.OnReportEvent = (ctx, n, ev) => handler(ctx, n, ev); return this; } public INodeBuilder AllowMultipleEventConsumers(bool enable = true) { if (Builder is NodeManagerBuilder nmb) { nmb.RegisterMultiConsumerNode(Node, enable); } return this; } public INodeBuilder Child(QualifiedName browseName) { NodeState? c = Node.FindChild(Builder.Context, browseName) ?? throw ServiceResultException.Create( StatusCodes.BadNodeIdUnknown, "Child '{0}' not found on '{1}'.", browseName, Node.BrowseName); return new AdHocInstanceNodeBuilder(Builder, c); } public INodeBuilder Child(QualifiedName browseName) where TChild : NodeState { NodeState? c = Node.FindChild(Builder.Context, browseName); if (c is not TChild typed) { throw ServiceResultException.Create( StatusCodes.BadTypeMismatch, "Child '{0}' on '{1}' is not '{2}'.", browseName, Node.BrowseName, typeof(TChild).Name); } return new AdHocInstanceNodeBuilder(Builder, typed); } public IVariableBuilder Variable(QualifiedName browseName) { NodeState? c = Node.FindChild(Builder.Context, browseName); if (c is not BaseVariableState variable) { throw ServiceResultException.Create( StatusCodes.BadTypeMismatch, "Child '{0}' on '{1}' is not a variable.", browseName, Node.BrowseName); } return new VariableBuilder(Builder, variable); } private AdHocInstanceNodeBuilder SetVariable(Action wire) { if (Node is not BaseVariableState v) { throw ServiceResultException.Create( StatusCodes.BadInvalidArgument, "Cannot wire variable callback on '{0}' (not a BaseVariableState).", Node.BrowseName); } wire(v); return this; } private AdHocInstanceNodeBuilder SetMethod(Action wire) { if (Node is not MethodState m) { throw ServiceResultException.Create( StatusCodes.BadInvalidArgument, "Cannot wire method callback on '{0}' (not a MethodState).", Node.BrowseName); } wire(m); return this; } } }