/* ======================================================================== * 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 add references and child objects to the /// node currently focused by the fluent builder. These cover the /// common "FunctionalGroup" wiring pattern from OPC UA DI /// (OPC 10000-100): /// /// /// /// /// — adds an Organizes reference (DI uses this to gather /// unrelated variables under a FunctionalGroup). /// /// / /// — common hierarchical references. /// /// — generic escape hatch for any /// reference type. /// /// — creates a new child (typically /// of Opc.Ua.Di.ObjectTypes.FunctionalGroupType) for grouping /// other nodes. /// /// /// Newly created child objects are attached to the parent via /// ; their NodeIds are generated from /// the parent's NodeId by appending the child browse name (matching /// the pattern used by the generated state classes and the /// PumpDeviceIntegrationServer NodeIdFactory). Direct NodeId lookup on a newly /// created child requires the owning node manager to index the new /// node via AddPredefinedNodeAsync; until then the child is /// reachable through navigation from the parent. /// /// public static class ReferenceBuilderExtensions { /// /// Adds an reference /// from the current node to . /// /// The owning node builder. /// NodeId of the organized target. /// The same builder, for chaining. public static INodeBuilder Organizes( this INodeBuilder builder, NodeId targetId) { return builder.AddReference(ReferenceTypeIds.Organizes, isInverse: false, targetId); } /// /// Convenience overload that takes a for /// the target. /// public static INodeBuilder Organizes( this INodeBuilder builder, NodeState target) { if (target == null) { throw new ArgumentNullException(nameof(target)); } return builder.Organizes(target.NodeId); } /// /// Adds a reference /// from the current node to . /// public static INodeBuilder HasComponent( this INodeBuilder builder, NodeId targetId) { return builder.AddReference(ReferenceTypeIds.HasComponent, isInverse: false, targetId); } /// /// Adds a reference /// from the current node to . /// public static INodeBuilder HasProperty( this INodeBuilder builder, NodeId targetId) { return builder.AddReference(ReferenceTypeIds.HasProperty, isInverse: false, targetId); } /// /// Adds an arbitrary reference from the current node to /// . Equivalent to /// . /// /// The owning node builder. /// The reference type id. /// /// for an inverse reference; /// for a forward reference. /// /// NodeId of the target node. /// The same builder, for chaining. public static INodeBuilder AddReference( this INodeBuilder builder, NodeId referenceTypeId, bool isInverse, NodeId targetId) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (referenceTypeId.IsNull) { throw new ArgumentNullException(nameof(referenceTypeId)); } if (targetId.IsNull) { throw new ArgumentNullException(nameof(targetId)); } builder.Node.AddReference(referenceTypeId, isInverse, targetId); return builder; } /// /// Creates and attaches a new child /// to the resolved parent. Useful for synthesising /// FunctionalGroups (DI) or other grouping objects in code rather /// than declaring them in the NodeSet XML. /// /// The owning parent builder. /// /// Browse name of the new child (qualified with the desired /// namespace index). /// /// /// Optional TypeDefinitionId for the new object. When /// (the default), the /// is used. Pass /// Opc.Ua.Di.ObjectTypes.FunctionalGroupType (resolved /// through the NamespaceTable) when the group should appear as /// a DI FunctionalGroup. /// /// /// A typed for the /// newly created child, ready for further wiring (e.g. /// ). /// public static INodeBuilder AddObject( this INodeBuilder parent, QualifiedName browseName, NodeId typeDefinitionId = default) { if (parent == null) { throw new ArgumentNullException(nameof(parent)); } if (browseName.IsNull) { throw new ArgumentNullException(nameof(browseName)); } NodeId typeDef = typeDefinitionId.IsNull ? ObjectTypeIds.BaseObjectType : typeDefinitionId; string symbolicName = browseName.Name ?? string.Empty; var child = new BaseObjectState(parent.Node) { BrowseName = browseName, DisplayName = new LocalizedText(symbolicName), SymbolicName = symbolicName, TypeDefinitionId = typeDef }; // Generate a NodeId that mirrors the parent's identifier scope. // Pattern matches PumpDeviceIntegrationServer's NodeIdFactory: // "{parentIdentifier}_{childBrowseName}" in the parent's namespace. string parentIdentifier = parent.Node.NodeId.IdentifierAsString; child.NodeId = new NodeId( string.Concat(parentIdentifier, "_", symbolicName), parent.Node.NodeId.NamespaceIndex); parent.Node.AddChild(child); // Return a typed builder pointing at the new child. Reuse the // existing NodeManagerBuilder.AsTyped path by going through a // small adapter that doesn't require re-registration. return new AdHocNodeBuilder(parent.Builder, child); } /// /// Minimal wrapper for nodes /// that don't live in the manager's PredefinedNodes /// dictionary. Provides direct access to the underlying node and /// the parent builder, plus pass-through of all On* / /// child wiring through the same mechanisms used by the /// production NodeBuilder. /// /// Concrete node state class. private sealed class AdHocNodeBuilder : INodeBuilder where TState : NodeState { internal AdHocNodeBuilder(INodeManagerBuilder ownerBuilder, TState node) { Builder = ownerBuilder ?? throw new ArgumentNullException(nameof(ownerBuilder)); 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 AdHocNodeBuilder(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 AdHocNodeBuilder(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 AdHocNodeBuilder(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 AdHocNodeBuilder 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 AdHocNodeBuilder 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; } } } }