/* ======================================================================== * 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.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Xml; using Microsoft.Extensions.Logging; using Opc.Ua.Schema.Model; using Opc.Ua.Types; namespace Opc.Ua.SourceGeneration { /// /// Generates data types and node state classes. /// internal sealed class NodeStateGenerator : IGenerator { /// /// Create node state generator /// public NodeStateGenerator(IGeneratorContext context) { m_context = context ?? throw new ArgumentNullException(nameof(context)); m_messageContext = ServiceMessageContext.CreateEmpty(context.Telemetry); m_logger = context.Telemetry.CreateLogger(); CollectNodesToGenerate(); } /// /// When true, emits the model's public NodeSet import factory /// provider. Only set for models whose consumer references /// Opc.Ua.Server. /// public bool GenerateNodeSetImportSupport { get; init; } /// /// When true, emits the import factory provider on its own and /// leaves the model types to the assembly that owns them. Set when a /// node manager binds to a model a referenced assembly supplies: the /// provider implements an Opc.Ua.Server contract that a /// model-only assembly cannot reference, so it has to be emitted /// beside the manager rather than beside the model. /// public bool ImportSupportOnly { get; init; } /// public IEnumerable Emit() { m_initializers.Clear(); if (ImportSupportOnly) { return GenerateNodeSetImportSupport ? [EmitNodeSetImportSupport()] : []; } if (m_instances.Count + m_nodes.Count == 0) { return GenerateNodeSetImportSupport ? [EmitNodeSetImportSupport()] : []; } List resources = [ EmitNodeStateClasses(), EmitExtensions() ]; if (GenerateNodeSetImportSupport) { resources.Add(EmitNodeSetImportSupport()); } Resource? initializers = EmbedInitializers(); if (initializers != null) { resources.Add(initializers); } return resources; } /// /// Create the nodestates classes /// private TextFileResource EmitNodeStateClasses() { string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix; string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format( "{0}.NodeStates.g.cs", nsPrefix)); using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName); using var templateWriter = new TemplateWriter(writer); var template = new Template(templateWriter, NodeStateTemplates.File); template.AddReplacement(Tokens.NamespacePrefix, nsPrefix); template.AddReplacement( Tokens.Namespace, nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal)); template.AddReplacement( Tokens.NamespaceUri, m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace( m_context.ModelDesign.TargetNamespace.Value)); // TODO: Switch to "instance" list and get "type" from entry.IsInstanceOfType template.AddReplacement( Tokens.ListOfTypes, m_nodes.Values, LoadTemplate_ListOfNodeStateClasses, WriteTemplate_ListOfNodeStateClasses); template.AddReplacement( Tokens.ListOfTypeActivators, NodeStateTemplates.ActivatorClass, m_nodes.Values, LoadTemplate_ListOfNodeStateActivators, WriteTemplate_ListOfNodeStateActivators); template.Render(); return fileName.AsTextFileResource(); } /// /// Create extensions /// /// /// Create the model's NodeSet import factory provider /// private TextFileResource EmitNodeSetImportSupport() { string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix; string typeStem = nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal); string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format( "{0}.NodeSetImportSupport.g.cs", nsPrefix)); using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName); using var templateWriter = new TemplateWriter(writer); var template = new Template( templateWriter, NodeStateTemplates.NodeSetImportSupport_File); template.AddReplacement(Tokens.NamespacePrefix, nsPrefix); template.AddReplacement( Tokens.ListOfTypes, ["body"], onLoad: context => { EmitNodeSetImportFactoryProvider( context.Out, typeStem + "NodeSetImportFactoryProvider"); return null; }); template.Render(); return fileName.AsTextFileResource(); } private void EmitNodeSetImportFactoryProvider( ITemplateWriter writer, string className) { List registrations = GetNodeSetImportFactoryRegistrations(); writer.WriteLine(" /// "); writer.WriteLine( " /// Public registration provider for typed NodeSet imports."); writer.WriteLine(" /// "); writer.WriteLine( " [global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{0}\", \"{1}\")]", typeof(NodeStateGenerator).Assembly.GetName().Name, typeof(NodeStateGenerator).Assembly.GetName().Version); writer.WriteLine( " public sealed class {0} :", className); writer.WriteLine( " global::Opc.Ua.Server.Nodes.INodeSetImportFactoryProvider"); writer.WriteLine(" {"); writer.WriteLine(" /// "); writer.WriteLine(" /// Initializes a provider."); writer.WriteLine(" /// "); writer.WriteLine(" public {0}()", className); writer.WriteLine(" {"); writer.WriteLine(" }"); writer.WriteLine(); writer.WriteLine(" /// "); writer.WriteLine(" /// Gets the shared provider instance."); writer.WriteLine(" /// "); writer.WriteLine( " public static {0} Instance {{ get; }} = new {0}();", className); writer.WriteLine(); writer.WriteLine(" /// "); writer.WriteLine( " public global::Opc.Ua.ArrayOf"); writer.WriteLine(" GetNodeSetImportFactories()"); writer.WriteLine(" {"); if (registrations.Count == 0) { writer.WriteLine( " return new global::Opc.Ua.ArrayOf<" + "global::Opc.Ua.Server.Nodes.INodeSetImportFactory>("); writer.WriteLine( " global::System.Array.Empty<" + "global::Opc.Ua.Server.Nodes.INodeSetImportFactory>());"); } else { writer.WriteLine( " return new global::Opc.Ua.ArrayOf<" + "global::Opc.Ua.Server.Nodes.INodeSetImportFactory>("); writer.WriteLine( " new global::Opc.Ua.Server.Nodes.INodeSetImportFactory[]"); writer.WriteLine(" {"); for (int i = 0; i < registrations.Count; i++) { writer.WriteLine( " ImportFactory{0:D4}.Instance{1}", i, i + 1 == registrations.Count ? string.Empty : ","); } writer.WriteLine(" });"); } writer.WriteLine(" }"); for (int i = 0; i < registrations.Count; i++) { EmitNodeSetImportFactory(writer, registrations[i], i); } writer.WriteLine(" }"); } private static void EmitNodeSetImportFactory( ITemplateWriter writer, ImportFactoryRegistration registration, int index) { writer.WriteLine(); writer.WriteLine( " private sealed class ImportFactory{0:D4} :", index); writer.WriteLine( " global::Opc.Ua.Server.Nodes.INodeSetImportFactory"); writer.WriteLine(" {"); writer.WriteLine( " private ImportFactory{0:D4}()", index); writer.WriteLine(" {"); writer.WriteLine(" }"); writer.WriteLine(); writer.WriteLine( " public static ImportFactory{0:D4} Instance {{ get; }} = new ImportFactory{0:D4}();", index); writer.WriteLine(); writer.WriteLine( " public global::Opc.Ua.NodeClass NodeClass"); writer.WriteLine( " => global::Opc.Ua.NodeClass.{0};", registration.NodeClass); writer.WriteLine(); writer.WriteLine( " public global::Opc.Ua.Server.Nodes.NodeSetImportDiscriminator Discriminator"); writer.WriteLine( " => global::Opc.Ua.Server.Nodes.NodeSetImportDiscriminator.{0};", registration.Discriminator); writer.WriteLine(); writer.WriteLine( " public global::Opc.Ua.ExpandedNodeId DiscriminatorId"); writer.WriteLine( " => {0};", registration.DiscriminatorId); writer.WriteLine(); writer.WriteLine( " public global::Opc.Ua.NodeState CreateEmptyState()"); writer.WriteLine(" {"); writer.WriteLine( " return {0};", registration.CreateExpression); writer.WriteLine(" }"); writer.WriteLine(" }"); } private List GetNodeSetImportFactoryRegistrations() { var registrations = new Dictionary( StringComparer.Ordinal); string targetNamespace = m_context.ModelDesign.TargetNamespace.Value; void Add( string nodeClass, string discriminator, NodeDesign discriminatorNode, string? createExpression) { if (discriminatorNode == null || string.IsNullOrEmpty(createExpression) || !HasResolvableNodeId(discriminatorNode)) { return; } string discriminatorId = discriminatorNode.GetExpandedNodeIdAsCode( m_context.ModelDesign.Namespaces); string key = nodeClass + "|" + discriminator + "|" + discriminatorId; registrations.TryAdd( key, new ImportFactoryRegistration( nodeClass, discriminator, discriminatorId, createExpression)); } foreach (NodeToGenerate node in m_nodes.Values) { if (node.Parent != null || !string.Equals( node.Design!.SymbolicId.Namespace, targetNamespace, StringComparison.Ordinal)) { continue; } switch (node.Design) { case ObjectTypeDesign objectType: Add( "ObjectType", "NodeId", objectType, "new global::Opc.Ua.BaseObjectTypeState()"); if (!objectType.IsAbstract && node.Instance != null) { Add( "Object", "TypeDefinition", objectType, GetDirectEmptyStateCreation( (InstanceDesign)node.Instance.Design!)); } break; case VariableTypeDesign variableType: Add( "VariableType", "NodeId", variableType, "new global::Opc.Ua.BaseDataVariableTypeState()"); if (!variableType.IsAbstract && node.Instance != null) { Add( "Variable", "TypeDefinition", variableType, GetDirectEmptyStateCreation( (InstanceDesign)node.Instance.Design!)); } break; case DataTypeDesign dataType: Add( "DataType", "NodeId", dataType, "new global::Opc.Ua.DataTypeState()"); break; case ReferenceTypeDesign referenceType: Add( "ReferenceType", "NodeId", referenceType, "new global::Opc.Ua.ReferenceTypeState()"); break; case ViewDesign view: Add( "View", "NodeId", view, "new global::Opc.Ua.ViewState()"); break; case MethodDesign method when method.IsMethodTypeDesign(): string? createExpression = GetDirectEmptyStateCreation(method); Add( "Method", "NodeId", method, createExpression); Add( "Method", "MethodDeclaration", method, createExpression); break; } } foreach (NodeToGenerate node in m_nodes.Values) { if (node.Design is MethodDesign method && node.RootIsTypeDefinition && !method.IsMethodTypeDesign() && string.Equals( method.SymbolicId.Namespace, targetNamespace, StringComparison.Ordinal)) { Add( "Method", "MethodDeclaration", method, GetDirectEmptyStateCreation(method)); } } foreach (NodeToGenerate node in m_nodes.Values) { if (node.Design is not InstanceDesign instance || !string.Equals( instance.SymbolicId.Namespace, targetNamespace, StringComparison.Ordinal) || !IsInAddressSpace(node)) { continue; } string? nodeClass = GetImportNodeClass(instance); if (nodeClass == null) { continue; } // Only declarations get an exact-NodeId factory. Registering // one per address-space instance would emit a factory per node // of the model, and an imported node that re-declares an // instance still resolves through its TypeDefinition or // MethodDeclaration below. if (IsDeclarationNode(node)) { Add( nodeClass, "NodeId", instance, GetDirectEmptyStateCreation(instance)); } if (instance is ObjectDesign && instance.TypeDefinitionNode is ObjectTypeDesign objectType && string.Equals( objectType.SymbolicId.Namespace, targetNamespace, StringComparison.Ordinal)) { Add( "Object", "TypeDefinition", objectType, GetDirectTypeInstanceCreation(objectType, instance)); } else if (instance is VariableDesign && instance.TypeDefinitionNode is VariableTypeDesign variableType && string.Equals( variableType.SymbolicId.Namespace, targetNamespace, StringComparison.Ordinal)) { Add( "Variable", "TypeDefinition", variableType, GetDirectTypeInstanceCreation(variableType, instance)); } if (instance is MethodDesign method && method.MethodDeclarationNode != null) { Add( "Method", "MethodDeclaration", method.MethodDeclarationNode, GetDirectEmptyStateCreation(method)); } } return [ .. registrations.Values .OrderBy( registration => registration.Discriminator, StringComparer.Ordinal) .ThenBy( registration => registration.NodeClass, StringComparer.Ordinal) .ThenBy( registration => registration.DiscriminatorId, StringComparer.Ordinal) ]; } private string? GetDirectTypeInstanceCreation( TypeDesign type, InstanceDesign fallbackInstance) { if (m_nodes.TryGetValue( type.SymbolicId, out NodeToGenerate? typeNode) && typeNode.Instance?.Design is InstanceDesign typeInstance) { return GetDirectEmptyStateCreation(typeInstance); } return GetDirectEmptyStateCreation(fallbackInstance); } private string? GetDirectEmptyStateCreation(InstanceDesign? instance) { string factory = instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true); return ConvertStateFactoryToDirectConstructor(factory); } private string? GetDirectEmptyStateCreation(MethodDesign method) { string factory = method.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true); return ConvertStateFactoryToDirectConstructor(factory); } private static string? ConvertStateFactoryToDirectConstructor( string factory) { if (string.IsNullOrEmpty(factory)) { return null; } if (factory.StartsWith("new ", StringComparison.Ordinal)) { return factory + "(null)"; } const string withMarker = ".With<"; int withIndex = factory.LastIndexOf( withMarker, StringComparison.Ordinal); if (withIndex >= 0) { string stateType = factory[..withIndex]; string builderType = factory[(withIndex + ".With".Length)..]; return "new " + stateType + ".Implementation" + builderType + "(null)"; } return null; } /// /// Gets whether a node belongs to a declaration - a type definition or /// a method type - rather than to the model's address space. /// private static bool IsDeclarationNode(NodeToGenerate node) { NodeToGenerate root = node; while (root.Parent != null) { root = root.Parent; } return root.RootIsTypeDefinition || (root.Design is MethodDesign method && method.IsMethodTypeDesign()); } private static string? GetImportNodeClass(InstanceDesign instance) { return instance switch { ObjectDesign => "Object", VariableDesign => "Variable", MethodDesign => "Method", _ => null }; } private TextFileResource EmitExtensions() { string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix; string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format( "{0}.NodeStates.ex.g.cs", nsPrefix)); using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName); using var templateWriter = new TemplateWriter(writer); var template = new Template( templateWriter, NodeStateTemplates.Extensions_File); template.AddReplacement(Tokens.NamespacePrefix, nsPrefix); template.AddReplacement( Tokens.Namespace, nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal)); template.AddReplacement( Tokens.NamespaceUri, m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace( m_context.ModelDesign.TargetNamespace.Value)); template.AddReplacement( Tokens.ListOfNodeStateInitializers, m_nodes.Values, LoadTemplate_ListOfNodeStateInitializers, WriteTemplate_ListOfNodeStateInitializers); template.AddReplacement( Tokens.ListOfNodeStateTypeFactories, m_nodes.Values, LoadTemplate_ListOfNodeStateFactories, WriteTemplate_ListOfNodeStateFactories); template.AddReplacement( Tokens.ListOfNodeStateInstanceFactories, m_instances.Values, LoadTemplate_ListOfNodeStateFactories, WriteTemplate_ListOfNodeStateFactories); // TODO: Adopt to "instance" list and get "type" from entry.IsInstanceOfType template.AddReplacement( Tokens.ListOfActivatorRegistrations, NodeStateTemplates.ActivatorRegistration, m_nodes.Values, LoadTemplate_ListOfNodeStateActivators, WriteTemplate_ListOfNodeStateActivators); template.Render(); return fileName.AsTextFileResource(); } private TemplateString? LoadTemplate_ListOfNodeStateActivators(ILoadContext context) { if (context.Target is not NodeToGenerate node) { return null; } if (ExcludeNodeStateClassGeneration(node)) { return null; } switch (node.Design) { case ObjectTypeDesign: case VariableTypeDesign: case MethodDesign method when MethodDesignArgumentResolver.HasMethodArguments(method) && method.IsMethodTypeDesign(): return context.TemplateString; default: return null; } } private bool WriteTemplate_ListOfNodeStateActivators(IWriteContext context) { if (context.Target is not NodeToGenerate node) { return false; } string nodeClass = node.Design!.GetNodeClassAsString(); context.Template.AddReplacement(Tokens.NodeClass, nodeClass); context.Template.AddBrowseNameReplacement( Tokens.BrowseName, Tokens.BrowseNameLiteral, node.Design!.SymbolicName.Name, m_logger); context.Template.AddReplacement(Tokens.SymbolicId, node.Design.SymbolicId.Name); if (node.Design is TypeDesign type) { context.Template.AddReplacement( Tokens.StateClassName, CoreUtils.Format("{0}State", type.ClassName)); } else if (node.Design is MethodDesign method) { context.Template.AddReplacement( Tokens.StateClassName, method.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, [])); } return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfNodeStateClasses(ILoadContext context) { if (context.Target is not NodeToGenerate node) { return null; } if (ExcludeNodeStateClassGeneration(node)) { return null; } return node.Design switch { ObjectTypeDesign => NodeStateTemplates.ObjectType_Class, VariableTypeDesign => NodeStateTemplates.VariableType_Class, MethodDesign method when MethodDesignArgumentResolver.HasMethodArguments(method) => NodeStateTemplates.MethodType_Class, _ => null }; } private bool WriteTemplate_ListOfNodeStateClasses(IWriteContext context) { if (context.Target is not NodeToGenerate node) { return false; } NodeDesign? root = node.Design; context.Template.AddReplacement(Tokens.NodeClass, root!.GetNodeClassAsString()); context.Template.AddReplacement(Tokens.TypeName, root!.SymbolicName.Name); context.Template.AddReplacement(Tokens.SymbolicId, root.SymbolicId.Name); context.Template.AddBrowseNameReplacement( Tokens.BrowseName, Tokens.BrowseNameLiteral, root.SymbolicName.Name, m_logger); context.Template.AddReplacement( Tokens.Description, root.Description != null ? root.Description.Value : string.Empty); context.Template.AddReplacement( Tokens.NamespaceUri, m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace( root.SymbolicName.Namespace)); context.Template.AddReplacement( Tokens.NamespacePrefix, m_context.ModelDesign.Namespaces.GetNamespacePrefix( root.SymbolicId.Namespace)); context.Template.AddReplacement( Tokens.XmlNamespaceUri, m_context.ModelDesign.Namespaces.GetConstantForXmlNamespace( root.SymbolicId.Namespace)); context.Template.AddReplacement( Tokens.BrowseNameNamespaceUri, m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode( root.SymbolicName.Namespace)); switch (root) { case MethodDesign method: AddNodeStateClassMethodTypeReplacements(context, method); break; case ObjectTypeDesign objectType: AddNodeStateClassObjectTypeReplacements(context, objectType); break; case VariableTypeDesign variableType: AddNodeStateClassVariableTypeReplacements(context, variableType); break; default: return false; } context.Template.AddReplacement( Tokens.InitializeOptionalChildren, NodeStateTemplates.InitializeOptionalChild, node.Children.Values, LoadTemplate_InitializeOptionalChildren, WriteTemplate_InitializeOptionalChildren); context.Template.AddReplacement( Tokens.ListOfNonMandatoryChildren, node.Children.Values, LoadTemplate_ListOfNonMandatoryChildren, WriteTemplate_ListOfNonMandatoryChildren); context.Template.AddReplacement( Tokens.ListOfFields, node.Children.Values, LoadTemplate_ListOfFields); context.Template.AddReplacement( Tokens.ListOfProperties, node.Children.Values, LoadTemplate_ListOfProperties, WriteTemplate_ListOfProperties); context.Template.AddReplacement( Tokens.ListOfChildOperations, NodeStateTemplates.ChildOperations, [node], LoadTemplate_FindChildMethods, WriteTemplate_FindChildMethods); List properties = GetChildrenWithProperties(node, true); context.Template.AddReplacement( Tokens.ListOfChildCopies, NodeStateTemplates.CloneChild, properties, WriteTemplate_ListOfChildren); context.Template.AddReplacement( Tokens.ListOfChildHashes, NodeStateTemplates.HashChild, properties, WriteTemplate_ListOfChildren); context.Template.AddReplacement( Tokens.ListOfEqualityComparers, NodeStateTemplates.CompareChild, properties, WriteTemplate_ListOfChildren); return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfNonMandatoryChildren(ILoadContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return null; } if (node.IsNotExplicitlyDefined) { return null; } if (instance.IsOverriddenWithSameClass( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)) { return null; } switch (GetEffectiveModellingRule(node, instance)) { case ModellingRule.Optional: return NodeStateTemplates.OptionalMethod; case ModellingRule.OptionalPlaceholder: case ModellingRule.MandatoryPlaceholder: return NodeStateTemplates.PlaceHolderMethod; case ModellingRule.ExposesItsArray: case ModellingRule.CardinalityRestriction: case ModellingRule.MandatoryShared: // TODO? break; } return null; } private bool WriteTemplate_ListOfNonMandatoryChildren(IWriteContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return false; } context.Template.AddReplacement(Tokens.AccessorSymbol, "public new"); if (!instance.IsOverridden()) { if (!s_builtInMethodNames.Contains(instance.SymbolicName.Name) && !HidesInheritedPlaceholderHelper(instance)) { context.Template.AddReplacement(Tokens.AccessorSymbol, "public"); } } else { instance = instance.GetMergedInstance(); } context.Template.AddReplacement(Tokens.Description, instance.Description != null ? instance.Description.Value : string.Empty); context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name); context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name); context.Template.AddReplacement( Tokens.NodeIdConstant, instance.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); return context.Template.Render(); } /// /// Tests the CLR helper signature rather than the qualified browse name: a placeholder /// in a different wire namespace can still hide the same Add helper on a generated base class. /// private static bool HidesInheritedPlaceholderHelper(InstanceDesign instance) { if (instance.ModellingRule is not ModellingRule.MandatoryPlaceholder and not ModellingRule.OptionalPlaceholder || instance.Parent is not TypeDesign declaringType) { return false; } for (TypeDesign? baseType = declaringType.BaseTypeNode; baseType != null; baseType = baseType.BaseTypeNode) { if (baseType.Children?.Items == null) { continue; } foreach (InstanceDesign child in baseType.Children.Items) { if (child.ModellingRule is ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder && child.SymbolicName.Name == instance.SymbolicName.Name) { return true; } } } return false; } private TemplateString? LoadTemplate_TypedVariableType(ILoadContext context) { if (context.Target is not VariableTypeDesign variableType) { return null; } if (variableType.DataTypeNode!.IsTemplateParameterRequired(variableType.ValueRank)) { return null; } return context.TemplateString; } private bool WriteTemplate_TypedVariableType(IWriteContext context) { if (context.Target is not VariableTypeDesign type) { return false; } context.Template.AddReplacement(Tokens.NodeClass, type.GetNodeClassAsString()); context.Template.AddReplacement(Tokens.ClassName, type.ClassName); context.Template.AddReplacement(Tokens.TypeName, type.SymbolicName.Name); context.Template.AddBrowseNameReplacement( Tokens.BrowseName, Tokens.BrowseNameLiteral, type.SymbolicName.Name, m_logger); return context.Template.Render(); } private TemplateString? LoadTemplate_VariableTypeValue(ILoadContext context) { if (context.Target is not VariableTypeDesign variableType) { return null; } Dictionary fields = []; CollectMatchingFields(variableType, fields); if (fields.Count == 0) { return null; } return context.TemplateString; } private bool WriteTemplate_VariableTypeValue(IWriteContext context) { if (context.Target is not VariableTypeDesign type) { return false; } Dictionary fields = []; CollectMatchingFields(type, fields); if (fields.Count == 0) { return false; } context.Template.AddReplacement(Tokens.ClassName, type.ClassName); context.Template.AddReplacement(Tokens.DataType, type.DataTypeNode!.GetDotNetTypeName( ValueRank.Scalar, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.NonNullable)); context.Template.AddReplacement( Tokens.ListOfChildInitializers, fields, LoadTemplate_VariableTypeValueInitializers); context.Template.AddReplacement( Tokens.ListOfUpdateChildrenChangeMasks, fields, LoadTemplate_VariableTypeValueChangeMasks); context.Template.AddReplacement( Tokens.ListOfChildMethods, NodeStateTemplates.VariableType_ValueMethods, fields, WriteTemplate_VariableTypeValueField); return context.Template.Render(); } private TemplateString? LoadTemplate_VariableTypeValueInitializers(ILoadContext context) { if (context.Target is not KeyValuePair field || field.Value == null) { return null; } string name = field.Key; string path = field.Key; context.Out.WriteLine("instance = m_variable.{0};", path); context.Out.WriteLine("if (instance != null)"); context.Out.WriteLine("{"); context.Out.WriteLine(" instance.OnReadValue = OnRead_{0};", name); context.Out.WriteLine(" instance.OnWriteValue = OnWrite_{0};", name); context.Out.WriteLine(" updateList.Add(instance);"); context.Out.WriteLine("}"); return null; } private TemplateString? LoadTemplate_VariableTypeValueChangeMasks(ILoadContext context) { if (context.Target is not KeyValuePair field || field.Value == null) { return null; } var dataType = field.Value.Parent as DataTypeDesign; string path = field.Key; // The value is addressed through the generated structure property, // which can differ from the authored field name (e.g. a field named // like its enclosing type or a reserved member gets a suffix). string valuePath = field.Value.GetPropertyName(); if (dataType!.IsDotNetEqualityComparable(field.Value.ValueRank)) { context.Out.WriteLine("if (m_value.{0} != newValue.{0})", valuePath); } else { context.Out.WriteLine( "if (!global::Opc.Ua.CoreUtils.IsEqual(m_value.{0}, newValue.{0}))", valuePath); } context.Out.WriteLine("{"); context.Out.WriteLine( " UpdateChildVariableStatus(m_variable.{0}, ref statusCode, ref timestamp);", path); context.Out.WriteLine("}"); return null; } private bool WriteTemplate_VariableTypeValueField(IWriteContext context) { if (context.Target is not KeyValuePair field || field.Value == null) { return false; } context.Template.AddReplacement(Tokens.ChildName, field.Key); context.Template.AddReplacement(Tokens.ChildPath, field.Key); context.Template.AddReplacement(Tokens.PropertyName, field.Value.GetPropertyName()); string childDataType = field.Value.DataTypeNode!.GetDotNetTypeName( field.Value.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.NonNullable); context.Template.AddReplacement(Tokens.ChildDataType, childDataType); if (field.Value.DataTypeNode!.NeedsCloning()) { context.Template.AddReplacement( Tokens.ValueWrite, CoreUtils.Format( "CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue", childDataType)); } else { context.Template.AddReplacement(Tokens.ValueWrite, "newValue"); } AddVariantAccessor(context, field.Value.DataTypeNode!); return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfFields(ILoadContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return null; } bool isMethodOverrideWithDifferentClass = IsMethodOverrideWithDifferentClass(instance); if (node.IsNotExplicitlyDefined || (instance.IsOverridden() && !isMethodOverrideWithDifferentClass)) { return null; } ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance); if (effectiveRule is ModellingRule.ExposesItsArray or ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder) { return null; } if (effectiveRule == ModellingRule.None) { return null; } if (instance is MethodDesign method && GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory && GetEffectiveModellingRule(node, method) != ModellingRule.Optional) { return null; } if (IsBuiltInProperty(node)) { return null; } if (isMethodOverrideWithDifferentClass) { instance = instance.GetMergedInstance(); } context.Out.WriteLine( "private {0}? {1};", instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces), instance.GetChildFieldName()); return null; } private TemplateString? LoadTemplate_ListOfInputArguments(ILoadContext context) { if (context.Target is not Parameter field) { return null; } string fieldName = GetMethodArgumentIdentifier(field); string typeName = field.DataTypeNode!.GetMethodArgumentTypeAsCode( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, false); switch (field.DataTypeNode!.BasicDataType) { case BasicDataType.UserDefined when HasTypedArgumentValueRank(field): context.Out.WriteLine( "_inputArguments[{2}].TryGetValue(" + "out {1} {0}, " + "global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" + ");", fieldName, typeName, context.Index); break; case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar: context.Out.WriteLine( "{1} {0} = _inputArguments[{2}];", fieldName, typeName, context.Index); break; default: context.Out.WriteLine( "_inputArguments[{2}].TryGetValue(out {1} {0});", fieldName, typeName, context.Index); break; } return null; } private TemplateString? LoadTemplate_ListOfOutputDeclarations(ILoadContext context) { if (context.Target is not Parameter field) { return null; } string fieldName = GetMethodArgumentIdentifier(field); string typeName = field.DataTypeNode!.GetMethodArgumentTypeAsCode( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, field.IsOptional); switch (field.DataTypeNode!.BasicDataType) { case BasicDataType.UserDefined when HasTypedArgumentValueRank(field): context.Out.WriteLine( "_outputArguments[{2}].TryGetValue(" + "out {1} {0}, " + "global::Opc.Ua.SystemContextExtensions.AsMessageContext(_context)" + ");", fieldName, typeName, context.Index); break; case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar: context.Out.WriteLine( "{1} {0} = _outputArguments[{2}];", fieldName, typeName, context.Index); break; default: context.Out.WriteLine( "_outputArguments[{2}].TryGetValue(out {1} {0});", fieldName, typeName, context.Index); break; } return null; } private TemplateString? LoadTemplate_ListOfOutputArguments(ILoadContext context) { if (context.Target is not Parameter field) { return null; } string fieldName = GetMethodArgumentIdentifier(field); switch (field.DataTypeNode!.BasicDataType) { case BasicDataType.UserDefined when HasTypedArgumentValueRank(field): context.Out.WriteLine( "_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure({0});", fieldName, context.Index); break; case BasicDataType.BaseDataType: context.Out.WriteLine( "_outputArguments[{1}] = {0};", fieldName, context.Index); break; default: context.Out.WriteLine( "_outputArguments[{1}] = global::Opc.Ua.Variant.From({0});", fieldName, context.Index); break; } return null; } private TemplateString? LoadTemplate_OnCallDeclaration(ILoadContext context) { if (context.Target is not MethodDesign method) { return null; } (Parameter[] inputArguments, Parameter[] outputArguments) = MethodDesignArgumentResolver.ResolveMethodArguments(method); AssignMethodArgumentCodeNames(method, inputArguments, outputArguments); context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,"); context.Out.WriteLine("global::Opc.Ua.MethodState _method,"); context.Out.Write("global::Opc.Ua.NodeId _objectId"); for (int ii = 0; ii < inputArguments.Length; ii++) { Parameter argument = inputArguments[ii]; context.Out.WriteLine(","); context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode!.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, argument.IsOptional)); } for (int ii = 0; ii < outputArguments.Length; ii++) { Parameter argument = outputArguments[ii]; context.Out.WriteLine(","); context.Out.Write("ref {1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode!.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, argument.IsOptional)); } context.Out.WriteLine(");"); return null; } private TemplateString? LoadTemplate_OnCallAsyncDeclaration(ILoadContext context) { if (context.Target is not MethodDesign method) { return null; } (Parameter[] inputArguments, Parameter[] outputArguments) = MethodDesignArgumentResolver.ResolveMethodArguments(method); AssignMethodArgumentCodeNames(method, inputArguments, outputArguments); context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,"); context.Out.WriteLine("global::Opc.Ua.MethodState _method,"); context.Out.Write("global::Opc.Ua.NodeId _objectId"); for (int ii = 0; ii < inputArguments.Length; ii++) { Parameter argument = inputArguments[ii]; context.Out.WriteLine(","); context.Out.Write("{1} {0}", GetMethodArgumentIdentifier(argument), argument.DataTypeNode!.GetMethodArgumentTypeAsCode( argument.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, argument.IsOptional)); } context.Out.WriteLine(","); context.Out.Write( "global::System.Threading.CancellationToken cancellationToken"); context.Out.WriteLine(");"); return null; } private TemplateString? LoadTemplate_ListOfOutputArgumentsFromResult(ILoadContext context) { if (context.Target is not Parameter field) { return null; } string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true); switch (field.DataTypeNode!.BasicDataType) { case BasicDataType.UserDefined when HasTypedArgumentValueRank(field): context.Out.WriteLine( "_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure(_result.{0});", fieldName, context.Index); break; case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar: context.Out.WriteLine( "_outputArguments[{1}] = _result.{0};", fieldName, context.Index); break; default: context.Out.WriteLine( "_outputArguments[{1}] = global::Opc.Ua.Variant.From(_result.{0});", fieldName, context.Index); break; } return null; } private TemplateString? LoadTemplate_ListOfResultProperties(ILoadContext context) { if (context.Target is not Parameter field) { return null; } string fieldName = GetMethodArgumentIdentifier(field, upperCamelCase: true); context.Out.WriteLine( "public {1} {0} {{ get; set; }}", fieldName, field.DataTypeNode!.GetMethodArgumentTypeAsCode( field.ValueRank, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, field.IsOptional)); return null; } private TemplateString? LoadTemplate_OnCallImplementation(ILoadContext context) { if (context.Target is not MethodDesign method) { return null; } (Parameter[] inputArguments, Parameter[] outputArguments) = MethodDesignArgumentResolver.ResolveMethodArguments(method); AssignMethodArgumentCodeNames(method, inputArguments, outputArguments); context.Out.WriteLine("_result = OnCall("); context.Out.WriteLine(" _context,"); context.Out.WriteLine(" this,"); context.Out.Write(" _objectId"); for (int ii = 0; ii < inputArguments.Length; ii++) { context.Out.WriteLine(","); context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii])); } for (int ii = 0; ii < outputArguments.Length; ii++) { context.Out.WriteLine(","); context.Out.Write(" ref {0}", GetMethodArgumentIdentifier(outputArguments[ii])); } context.Out.WriteLine(");"); return null; } private TemplateString? LoadTemplate_OnCallAsyncImplementation(ILoadContext context) { if (context.Target is not MethodDesign method) { return null; } (Parameter[] inputArguments, Parameter[] outputArguments) = MethodDesignArgumentResolver.ResolveMethodArguments(method); AssignMethodArgumentCodeNames(method, inputArguments, outputArguments); context.Out.WriteLine("_result = await OnCallAsync("); context.Out.WriteLine(" _context,"); context.Out.WriteLine(" this,"); context.Out.Write(" _objectId"); for (int ii = 0; ii < inputArguments.Length; ii++) { context.Out.WriteLine(","); context.Out.Write(" {0}", GetMethodArgumentIdentifier(inputArguments[ii])); } context.Out.WriteLine(","); context.Out.WriteLine(" cancellationToken).ConfigureAwait(false);"); return null; } private TemplateString? LoadTemplate_InitializeOptionalChildren(ILoadContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return null; } if (node.IsNotExplicitlyDefined) { return null; } if (GetEffectiveModellingRule(node, instance) != ModellingRule.Optional) { return null; } if (IsBuiltInProperty(node)) { return null; } return context.TemplateString; } private bool WriteTemplate_InitializeOptionalChildren(IWriteContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return context.Template.Render(); } context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name); context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name); context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName()); if (instance.Parent is MethodDesign method) { context.Template.AddReplacement(Tokens.ClassName, method.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, [])); } else if (instance.Parent is TypeDesign type) { context.Template.AddReplacement( Tokens.ClassName, type.ClassName); } return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfProperties(ILoadContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return null; } if (node.IsNotExplicitlyDefined) { return null; } ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance); if (effectiveRule is ModellingRule.ExposesItsArray or ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder) { return null; } if (effectiveRule == ModellingRule.None) { return null; } if (instance is MethodDesign method && GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory && GetEffectiveModellingRule(node, method) != ModellingRule.Optional) { return null; } if (instance.IsOverridden()) { if (instance.IsOverriddenWithSameClass( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)) { return null; } if (instance is MethodDesign) { return NodeStateTemplates.Property; } return NodeStateTemplates.PropertyOverride; } if (IsBuiltInProperty(node)) { return null; } return NodeStateTemplates.Property; } private bool WriteTemplate_ListOfProperties(IWriteContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance) { return false; } context.Template.AddReplacement(Tokens.AccessorSymbol, "public new"); if (!instance.IsOverridden()) { if (!RequiresNewModifier((node.Parent?.Design)!, instance.SymbolicName.Name) || (instance is VariableDesign && instance.SymbolicName.Name == "Value")) { context.Template.AddReplacement(Tokens.AccessorSymbol, "public"); } } else { instance = instance.GetMergedInstance(); } context.Template.AddReplacement(Tokens.Description, instance.Description != null ? instance.Description.Value : string.Empty); context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name); context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName()); return context.Template.Render(); } private TemplateString? LoadTemplate_FindChildMethods(ILoadContext context) { if (context.Target is not NodeToGenerate node) { return null; } int count = 0; foreach (NodeToGenerate child in node.Children.Values) { if (child.Design is not InstanceDesign instance) { continue; } if (child.IsNotExplicitlyDefined) { continue; } ModellingRule childEffectiveRule = GetEffectiveModellingRule(child, instance); if (childEffectiveRule is ModellingRule.ExposesItsArray or ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder) { continue; } if (childEffectiveRule is ModellingRule.None) { continue; } if (childEffectiveRule is ModellingRule.OptionalPlaceholder or ModellingRule.MandatoryPlaceholder) { continue; } if (instance.IsOverriddenWithSameClass( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)) { continue; } count++; } if (count == 0) { return null; } return context.TemplateString; } private bool WriteTemplate_FindChildMethods(IWriteContext context) { if (context.Target is not NodeToGenerate node || node.Design is not TypeDesign type) { return false; } List childrenWithProperties = GetChildrenWithProperties(node); context.Template.AddReplacement( Tokens.ListOfFindChildCase, NodeStateTemplates.FindChildCase, childrenWithProperties, WriteTemplate_ListOfChildren); context.Template.AddReplacement( Tokens.ListOfCreateOrReplaceChild, NodeStateTemplates.CreateOrReplaceChild, childrenWithProperties, WriteTemplate_ListOfChildren); List additionalChildren = GetAdditionalChildren(node); context.Template.AddReplacement( Tokens.ListOfFindChildren, NodeStateTemplates.FindChildren, additionalChildren, WriteTemplate_ListOfChildren); context.Template.AddReplacement( Tokens.ListOfRemoveChild, NodeStateTemplates.RemoveChild, additionalChildren, WriteTemplate_ListOfChildren); return context.Template.Render(); } private bool WriteTemplate_ListOfChildren(IWriteContext context) { if (context.Target is not InstanceDesign instance) { return false; } context.Template.AddReplacement(Tokens.AccessorSymbol, "public new"); if (!instance.IsOverridden()) { context.Template.AddReplacement(Tokens.AccessorSymbol, "public"); } context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement(Tokens.ClassFactory, instance.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true)); context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name); context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name); // The child's declaration NodeId. CreateOrReplace{Child} uses it to // detect a child that still carries the type-level id (and may // therefore be rebased onto a per-instance NodeId) versus one whose // NodeId the caller already owns. context.Template.AddReplacement( Tokens.NodeIdConstant, instance.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); // The runtime BrowseName.Name may differ from the symbolic name // (the design's attribute can override it, e.g. // Boiler's "InputPipe" InstanceDesign with BrowseName "PipeX001"). // FindChild switches on the runtime browse name, so the case // label must use that string verbatim. context.Template.AddBrowseNameReplacement( Tokens.ChildBrowseName, Tokens.ChildBrowseNameLiteral, !string.IsNullOrEmpty(instance.BrowseName) ? instance.BrowseName : instance.SymbolicName.Name, m_logger); context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName()); context.Template.AddReplacement(Tokens.NodeClass, instance.GetNodeClassAsString()); context.Template.AddBrowseNameReplacement( Tokens.BrowseName, Tokens.BrowseNameLiteral, instance.SymbolicName.Name, m_logger); context.Template.AddReplacement( Tokens.BrowseNameNamespacePrefix, m_context.ModelDesign.Namespaces.GetNamespacePrefix( instance.SymbolicName.Namespace)); context.Template.AddReplacement( Tokens.BrowseNameNamespaceUri, m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode( instance.SymbolicName.Namespace)); return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfNodeStateInitializers(ILoadContext context) { if (context.Target is not NodeToGenerate node) { return null; } if (node.Design.IsMethodTypeDesign() || node.InstanceOf != null || !IsInAddressSpace(node) || node.Design!.Purpose == DataTypePurpose.Testing) { return null; } if (node.Parent != null) { // No registering of children return null; } if (node.Design is MethodDesign method) { return MethodDesignArgumentResolver.HasMethodArguments(method) ? NodeStateTemplates.AddMethod : null; } return NodeStateTemplates.Add; } private bool WriteTemplate_ListOfNodeStateInitializers(IWriteContext context) { if (context.Target is not NodeToGenerate node) { return false; } context.Template.AddReplacement(Tokens.SymbolicId, node.Design!.SymbolicId.Name); context.Template.AddReplacement(Tokens.SymbolicName, node.Design.SymbolicName.Name); return context.Template.Render(); } private TemplateString? LoadTemplate_ListOfNodeStateFactories(ILoadContext context) { if (context.Target is not NodeToGenerate node) { return null; } if (context.Token == Tokens.ListOfNodeStateInstanceFactories) { if (node.Parent != null) { return null; } return node.Design switch { ObjectDesign => NodeStateTemplates.Create_InstanceOfObjectType, VariableDesign => NodeStateTemplates.Create_InstanceOfVariableType, MethodDesign => NodeStateTemplates.Create_InstanceOfMethodType, _ => null }; } if (node.Parent != null) { if (node.Design is ViewDesign) { // Views are never emitted as AddChild components (ViewState is // not a BaseInstanceState). They are collected as standalone // root nodes and linked purely via Organizes references, the // same way DataType/ReferenceType type designs are handled. return null; } if (node.Design is not InstanceDesign instance) { return null; } if (GetEffectiveModellingRule(node, instance) is not ModellingRule.MandatoryPlaceholder and not ModellingRule.OptionalPlaceholder) { return node.Design switch { ObjectDesign => NodeStateTemplates.Create_ChildObject, VariableDesign => NodeStateTemplates.Create_ChildVariable, MethodDesign => NodeStateTemplates.Create_ChildMethod, _ => null }; } return node.Design switch { ObjectDesign => NodeStateTemplates.Create_ChildObject_Placeholder, VariableDesign => NodeStateTemplates.Create_ChildVariable_Placeholder, MethodDesign => NodeStateTemplates.Create_ChildMethod_Placeholder, _ => null }; } return node.Design switch { ObjectTypeDesign => NodeStateTemplates.Create_ObjectType, VariableTypeDesign => NodeStateTemplates.Create_VariableType, ReferenceTypeDesign => NodeStateTemplates.Create_ReferenceType, DataTypeDesign => NodeStateTemplates.Create_DataType, ObjectDesign => NodeStateTemplates.Create_Object, VariableDesign => NodeStateTemplates.Create_Variable, ViewDesign => NodeStateTemplates.Create_View, _ => null }; } private bool WriteTemplate_ListOfNodeStateFactories(IWriteContext context) { if (context.Target is not NodeToGenerate node) { return false; } NodeDesign? root = node.Design; // Common replacements for all node types context.Template.AddReplacement( Tokens.SymbolicId, root!.SymbolicId.Name); context.Template.AddReplacement( Tokens.SymbolicName, root.SymbolicName.Name); context.Template.AddBrowseNameReplacement( Tokens.BrowseName, Tokens.BrowseNameLiteral, root.SymbolicName.Name, m_logger); context.Template.AddReplacement(Tokens.NumericIdValue, root.FindNumericIdentifier() ?? 0); context.Template.AddReplacement( Tokens.TypeName, node.InstanceOf?.Design!.SymbolicName.Name); context.Template.AddReplacement( Tokens.NodeIdConstant, root.GetNodeIdAsCode(m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); AddInstanceNodeIdOverrideReplacement(context, node); // .net efficiently interns constant string, so usage of the BrowseName constants is not needed. string symbolicNameSymbol = root.SymbolicName.Name.AsStringLiteral(); context.Template.AddReplacement(Tokens.BrowseNameSymbol, string.IsNullOrEmpty(root.BrowseName) ? symbolicNameSymbol : root.BrowseName.AsStringLiteral()); context.Template.AddReplacement(Tokens.SymbolicNameSymbol, symbolicNameSymbol); context.Template.AddReplacement(Tokens.DisplayName, GetDisplayNameValue(root) ?? symbolicNameSymbol); context.Template.AddReplacement( Tokens.BrowseNameNamespaceUri, m_context.ModelDesign.Namespaces.GetNamespaceUriAsCode( root.SymbolicName.Namespace)); context.Template.AddReplacement( Tokens.DescriptionValue, GetDescriptionValue(root)); context.Template.AddReplacement( Tokens.WriteMaskValue, root.GetWriteMaskAsCode()); context.Template.AddReplacement( Tokens.UserWriteMaskValue, root.GetWriteMaskAsCode()); // Add Children context.Template.AddReplacement( Tokens.ListOfChildNodeStates, node.AllChildren, LoadTemplate_ReplaceChild); context.Template.AddReplacement( Tokens.ListOfOptionalChildNodeStates, node.AllChildren, LoadTemplate_ReplaceChild); // Add References HashSet references = GetReferences(node); context.Template.AddReplacement( Tokens.ListOfReferences, references, LoadTemplate_AddReference); // Node type-specific replacements switch (root) { case ObjectTypeDesign objectType: AddObjectTypeStateFactoryReplacements(context, objectType); break; case VariableTypeDesign variableType: AddVariableTypeStateFactoryReplacements(context, variableType); break; case ReferenceTypeDesign referenceType: AddReferenceTypeStateFactoryReplacements(context, referenceType); break; case DataTypeDesign dataType: AddDataTypeStateFactoryReplacements(context, dataType); break; case ObjectDesign objectDesign: AddObjectReplacements(context, node, objectDesign, references); break; case VariableDesign variableDesign: AddVariableStateFactoryReplacements(context, node, variableDesign, references); break; case MethodDesign methodDesign: AddMethodStateFactoryReplacements(context, node, methodDesign); break; case ViewDesign viewDesign: AddViewStateFactoryReplacements(context, viewDesign); break; } // Release status Export.ReleaseStatus releaseStatus = root.ReleaseStatus.ToNodeSetReleaseStatus(); context.Template.AddReplacement( Tokens.ReleaseStatusValue, releaseStatus != Export.ReleaseStatus.Released ? CoreUtils.Format( "nodeState.ReleaseStatus = global::Opc.Ua.Export.ReleaseStatus.{0};", releaseStatus) : null); // Categories context.Template.AddReplacement( Tokens.CategoriesValue, !string.IsNullOrEmpty(root.Category) ? CoreUtils.Format( "nodeState.Categories = new string[] {{ {0} }};", string.Join( ", ", root.Category .Split([',']) .Select(c => c.Trim().AsStringLiteral()))) : null); // Specification context.Template.AddReplacement( Tokens.SpecificationValue, root.PartNo != 0 ? CoreUtils.Format("nodeState.Specification = \"Part{0}\";", root.PartNo) : null); // Access restrictions — emit on all nodes (type and instance). // Type hierarchy nodes carry restrictions as metadata but the server // bypasses enforcement via IsPartOfTypeHierarchy at runtime. string? accessRestrictions = root.AccessRestrictions.GetAccessRestrictionsAsCode( root.AccessRestrictionsSpecified) ?? root.DefaultAccessRestrictions.GetAccessRestrictionsAsCode( root.DefaultAccessRestrictionsSpecified); context.Template.AddReplacement( Tokens.AccessRestrictionsValue, accessRestrictions != null ? CoreUtils.Format("nodeState.AccessRestrictions = {0};", accessRestrictions) : null); // Role permissions HashSet rolePermissions = GetRolePermissions(root); context.Template.AddReplacement( Tokens.ListOfRolePermissions, NodeStateTemplates.ListOfRolePermissions, [rolePermissions], WriteTemplate_RolePermissions); return context.Template.Render(); } private TemplateString? LoadTemplate_AddReference(ILoadContext context) { if (context.Target is not ReferenceToGenerate reference) { return null; } bool placeholder = reference.TargetNode is InstanceDesign { ModellingRule: ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder }; if (placeholder) { context.Out.WriteLine("if (!forInstance)"); context.Out.WriteLine("{"); } context.Out.WriteLine( "{0}state.AddReference({1}, {2}, {3});", placeholder ? " " : string.Empty, m_context.ModelDesign.GetNodeIdConstant( reference.ReferenceTypeId, "", kNamespaceTableContextVariable), reference.IsInverse ? "true" : "false", reference.TargetNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); if (placeholder) { context.Out.WriteLine("}"); } return null; } /// /// Whether applies: /// the option is set and the target model was read from a NodeSet2. /// private bool EmitsNodeSetNodesOnly => m_context.Options.EmitNodeSetNodesOnly && m_context.ModelDesign is ModelDesignValidator { IsSourceNodeSet: true }; /// /// With , a /// predefined node of a NodeSet2 input gets only the children the /// NodeSet2 declares: a child the generator derives from a supertype /// or a type definition is not added to it. /// /// The child factories of a type are also called by the /// CreateInstanceOf* factories with forInstance: true, so /// in a type the omitted child is still created when its parent is /// created for an instance; only the factories of a predefined instance /// leave it out unconditionally. /// /// private bool IsOmittedFromNodeSetAddressSpace(NodeToGenerate node) { return EmitsNodeSetNodesOnly && node.IsNotExplicitlyDefined && GetRoot(node).InstanceOf == null; } /// /// Whether the predefined instance that belongs /// to omits the undeclared nodes of its hierarchy, so that a reference /// may only end in a node of the hierarchy that it creates. The /// factories of a type keep their references: they are the ones the /// CreateInstanceOf* factories reuse at run time. /// private bool OmitsUndeclaredHierarchyNodes(NodeToGenerate node) { return EmitsNodeSetNodesOnly && !node.RootIsTypeDefinition && GetRoot(node).InstanceOf == null; } /// /// Whether the predefined instance that belongs /// to creates the node at of its hierarchy: the /// node is generated and neither it nor any of its ancestors is /// omitted (see ). /// private static bool IsCreatedInNodeSetAddressSpace(NodeToGenerate node, string path) { NodeToGenerate root = GetRoot(node); if (path == root.Path) { return true; } NodeToGenerate? target = FindDescendant(root, path); if (target == null) { return false; } for (NodeToGenerate current = target; current.Parent != null; current = current.Parent) { if (current.IsNotExplicitlyDefined) { return false; } } return true; } private static NodeToGenerate? FindDescendant(NodeToGenerate node, string path) { foreach (NodeToGenerate child in node.AllChildren) { if (child.Path == path) { return child; } NodeToGenerate? descendant = FindDescendant(child, path); if (descendant != null) { return descendant; } } return null; } private static NodeToGenerate GetRoot(NodeToGenerate node) { NodeToGenerate root = node; while (root.Parent != null) { root = root.Parent; } return root; } private TemplateString? LoadTemplate_ReplaceChild(ILoadContext context) { if (context.Target is not NodeToGenerate node || node.Design is not InstanceDesign instance || node.Design is ViewDesign) { return null; } // A predefined instance's factories only build that instance, and a // factory's type-template block only runs for the predefined type // node (forInstance: false), so neither creates the omitted child. bool omittedFromNodeSet = IsOmittedFromNodeSetAddressSpace(node); if (omittedFromNodeSet && (!node.RootIsTypeDefinition || context.Token == Tokens.ListOfOptionalChildNodeStates)) { return null; } // Determine which modelling rule drives factory-method emission. // // For SINGLETON instance overrides (a top-level in // StandardTypes.xml whose parent has no type def), an override of // a type-def Optional child to Mandatory must NOT force emission // of the child when UseTypeDefinitionModellingRules is on - the // SDK is responsible for adding only those children it actually // implements (fixes issue #3768). // // The gate applies TRANSITIVELY to every Variable/Method descendant // of a top-level singleton instance (e.g. Server, // ServerConfiguration, HistoryServerCapabilities). Optional // Object instances are intentionally exempt: they are still // emitted by the singleton-specific factory with their correct // instance-level NodeIds, because their subtrees use well-known // descendant NodeIds (e.g. // ServerConfiguration.CertificateGroups.DefaultHttpsGroup. // TrustList.Open at NodeId 14095) and the SDK binds against // them directly (e.g. ConfigurationNodeManager pairs // CertificateGroupState by NodeId in // , // RoleStateBinding looks up // ). // Patching every descendant NodeId from the SDK after a // type-level factory call would require hard-coded mapping of // dozens of well-known NodeIds per Object root and is brittle. // Optional Object descendants' own Variable/Method children are // still gated transitively (e.g. OperationLimits.MaxNodesPerRead // is suppressed and the SDK programmatically adds it back via // ). // // For TYPE-LEVEL promotions (e.g. AnalogItemType promoting EURange // from Optional on BaseAnalogType to Mandatory), the type-def // tree itself is being captured and the promotion is part of the // declared type definition, so the instance's Mandatory rule // must be honoured even when the option is on. The synthetic // type-instance NodeToGenerate (root.InstanceOf != null) and the // type-def NodeToGenerate (root.RootIsTypeDefinition) are both // excluded because IsUnderSingletonInstance is propagated only // through the singleton-instance subtree. bool isSingletonInstanceContext = m_context.Options.UseTypeDefinitionModellingRules && node.IsUnderSingletonInstance && node.Parent != null && instance is VariableDesign or MethodDesign or ObjectDesign; ModellingRule effectiveRule = isSingletonInstanceContext ? GetEffectiveModellingRule(node, instance) : instance.ModellingRule; // The "top-level non-typed parent" exception below forces children // of an explicitly-declared singleton instance (e.g. // in // StandardTypes.xml) into the always-emitted mandatory list so // declared overrides materialize on the actual node. This // exception must be suppressed for SINGLETON-INSTANCE roots when // // is on; otherwise singleton instance overrides could re-introduce // the very Optional/unimplemented children we are trying to // exclude from the actual instance. // // TYPE-DEFINITION roots (ObjectTypeDesign, VariableTypeDesign, // MethodTypeDesign) must still emit their Optional children // unconditionally — the type-def factory is called from many // places (instance.Create, ConfigurationNodeManager. // CreateNamespaceMetadataStateAsync, etc.) with // forInstance=true, and removing them breaks deep code paths // that rely on the type-def factory shape. Only suppress when // the parent is itself an InstanceDesign singleton. bool topLevelInstanceException = node.Parent != null && node.Parent.Parent == null && node.Parent.InstanceOf == null && (node.Parent.Design is not InstanceDesign || !m_context.Options.UseTypeDefinitionModellingRules); // Explicitly-declared descendants of a concrete predefined instance // (a user-modelled / declared under // Objects, e.g. a device instance) must be materialised on the // actual instance regardless of their (possibly type-inherited) // modelling rule: they are real nodes present in the model, not // modelling-rule-driven optional children the SDK adds on demand. // The topLevelInstanceException above only covers the instance's // DIRECT children (node.Parent.Parent == null); deeper descendants // — e.g. the EURange / EngineeringUnits properties of a // BaseAnalogType / AnalogUnitType variable nested under the // instance (issue #3964) — inherit IsUnderSingletonInstance and // would otherwise land in the `if (!forInstance)` type-template // block and be dropped from the instance. SDK-managed singletons // (Server, ServerConfiguration, …) are generated with // UseTypeDefinitionModellingRules on and keep the #3768 // optional-child suppression, so they are excluded here. bool concreteInstanceDescendant = node.IsUnderSingletonInstance && !node.RootIsTypeDefinition && node.InstanceOf == null && !node.IsNotExplicitlyDefined && !m_context.Options.UseTypeDefinitionModellingRules; bool alwaysEmitInstanceChild = topLevelInstanceException || concreteInstanceDescendant; if (context.Token == Tokens.ListOfOptionalChildNodeStates) { if (effectiveRule == ModellingRule.Mandatory) { return null; } // Real instance children of a top-level (non-typed) parent belong in // the always-emitted list so they materialize for the actual instance, // not only when the node is treated as a type template. if (alwaysEmitInstanceChild) { return null; } } // Otherwise only add mandatory children - all others are created on demand else if (effectiveRule != ModellingRule.Mandatory) { // Exception: real instance children of a top-level (non-typed) parent. if (!alwaysEmitInstanceChild) { return null; } } string forInstanceVariableValue = node.RootIsTypeDefinition ? "forInstance" : node.Parent?.InstanceOf != null || node.Parent?.Parent == null ? "true" : "forInstance"; if (!omittedFromNodeSet) { EmitChildFactoryCall( context.Out, node, instance, effectiveRule, forInstanceVariableValue); return null; } // An omitted child of a type is created for an instance only. context.Out.WriteLine("if (forInstance)"); context.Out.WriteLine("{"); EmitChildFactoryCall( new IndentedTemplateWriter(context.Out), node, instance, effectiveRule, forInstanceVariableValue); context.Out.WriteLine("}"); return null; } /// /// Emits the call that creates and adds it to /// the state of its parent's factory. /// private void EmitChildFactoryCall( ITemplateWriter output, NodeToGenerate node, InstanceDesign instance, ModellingRule effectiveRule, string forInstanceVariableValue) { if (node.Parent != null && IsInAddressSpace(node.Parent)) { switch (node.Parent.Design) { case TypeDesign: output.WriteLine( "state.AddChild(Create{0}(context, state, forInstance: {1}));", instance.SymbolicId.Name, forInstanceVariableValue); return; case InstanceDesign parentInstance: if (HasFixedChildSlot(parentInstance.TypeDefinitionNode, instance.SymbolicName.Name) || IsBuiltInProperty(node)) { // Children whose SymbolicName ends in "_Placeholder" // come from OptionalPlaceholder/MandatoryPlaceholder // slots on the type, which emit Add*_Placeholder // methods (cardinality 0..N) instead of // CreateOrReplace*_Placeholder methods (which only // exist for fixed Mandatory/Optional slots). // Fall through to AddChild for these. bool isPlaceholderSlot = instance.SymbolicName.Name .EndsWith("_Placeholder", StringComparison.Ordinal); if (!isPlaceholderSlot) { switch (effectiveRule) { case ModellingRule.Mandatory: case ModellingRule.Optional: EmitCreateOrReplaceChild( output, node, instance, forInstanceVariableValue); return; case ModellingRule.OptionalPlaceholder: case ModellingRule.MandatoryPlaceholder: // TODO break; case ModellingRule.ExposesItsArray: case ModellingRule.None: case ModellingRule.CardinalityRestriction: case ModellingRule.MandatoryShared: break; } } } break; } } output.WriteLine( "state.AddChild(Create{0}(context, state, forInstance: {1}));", instance.SymbolicId.Name, forInstanceVariableValue); } /// /// Emits the state.CreateOrReplace{Name}(context, Create{Symbolic}(context, state, forInstance: …)) /// call site for a child of an InstanceDesign parent. When the child /// lives in a type-level factory body ( /// is true) AND the type has well-known singleton instances /// whose corresponding child factories have been collected, also /// dispatches the forInstance: true path into the appropriate /// singleton-instance child factory. This ensures that lazy-added /// methods on singleton owners (e.g. /// serverObject.AddGetMonitoredItems(context, MethodIds.Server_GetMonitoredItems)) /// produce synthesized children (InputArguments, /// OutputArguments) at their well-known instance NodeIds /// rather than the type-level placeholders. /// private void EmitCreateOrReplaceChild( ITemplateWriter output, NodeToGenerate node, InstanceDesign instance, string forInstanceVariableValue) { List< ( NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)> singletonChildren = FindSingletonChildren(node); if (singletonChildren.Count == 0) { output.WriteLine( "state.CreateOrReplace{0}(context, Create{1}(context, state, " + "forInstance: {2}), assignInstanceNodeIds: false);", instance.SymbolicName.Name, instance.SymbolicId.Name, forInstanceVariableValue); return; } // Dispatch on the parent's NodeId so the singleton-instance // child factory is invoked when the type-level factory is // materialising an actual well-known instance subtree. The // gate keeps the type-template // emission path (forInstance: false) unchanged. output.WriteLine("if ({0} && parent != null)", forInstanceVariableValue); output.WriteLine("{"); for (int i = 0; i < singletonChildren.Count; i++) { (NodeToGenerate singletonRoot, NodeToGenerate _, string singletonChildSymbolicId) = singletonChildren[i]; string keyword = i == 0 ? "if" : "else if"; output.WriteLine( " {0} (parent.NodeId.Equals({1}))", keyword, singletonRoot.Design.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); output.WriteLine(" {"); output.WriteLine( " state.CreateOrReplace{0}(context, Create{1}(context, state, " + "forInstance: true), assignInstanceNodeIds: false);", instance.SymbolicName.Name, singletonChildSymbolicId); output.WriteLine(" }"); } output.WriteLine(" else"); output.WriteLine(" {"); output.WriteLine( " state.CreateOrReplace{0}(context, Create{1}(context, state, " + "forInstance: true), assignInstanceNodeIds: false);", instance.SymbolicName.Name, instance.SymbolicId.Name); output.WriteLine(" }"); output.WriteLine("}"); output.WriteLine("else"); output.WriteLine("{"); output.WriteLine( " state.CreateOrReplace{0}(context, Create{1}(context, state, " + "forInstance: {2}), assignInstanceNodeIds: false);", instance.SymbolicName.Name, instance.SymbolicId.Name, forInstanceVariableValue); output.WriteLine("}"); } private bool WriteTemplate_RolePermissions(IWriteContext context) { if (context.Target is not HashSet rolePermissions || rolePermissions.Count == 0) { return false; } context.Template.AddReplacement( Tokens.ListOfRolePermissions, NodeStateTemplates.RolePermission, rolePermissions, LoadTemplate_RolePermissionEntry, WriteTemplate_RolePermissionEntry); return context.Template.Render(); } private bool WriteTemplate_ArgumentCollection(IWriteContext context) { switch (context.Target) { case IList arguments: // The parameter that is packed into the Argument value context.Template.AddReplacement( Tokens.ListOfValues, NodeStateTemplates.ArgumentValue, arguments, WriteTemplate_Argument); break; case Parameter[] parameters: context.Template.AddReplacement( Tokens.ListOfValues, NodeStateTemplates.ArgumentValue, parameters, WriteTemplate_Argument); break; default: return false; } context.Template.AddReplacement(Tokens.DataType, "global::Opc.Ua.Argument"); return context.Template.Render(); } private bool WriteTemplate_Argument(IWriteContext context) { Argument? argument = null; Parameter? parameter; switch (context.Target) { case Argument arg: argument = arg; parameter = argument.Value as Parameter; break; case Parameter parm: parameter = parm; break; default: return false; } context.Template.AddReplacement( Tokens.Name, parameter == null ? argument!.Name.AsStringLiteral() : parameter.Name.AsStringLiteral()); context.Template.AddReplacement( Tokens.DataType, m_context.ModelDesign.GetNodeIdConstant( parameter?.DataType, "", kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.ValueRank, parameter?.ValueRank.GetValueRankAsCode(parameter.ArrayDimensions)); context.Template.AddReplacement( Tokens.ArrayDimensions, parameter?.ValueRank.GetArrayDimensionsAsCode(parameter.ArrayDimensions) ?? "default"); context.Template.AddReplacement( Tokens.Description, (parameter?.Description).GetLocalizedTextAsCode(true)); return context.Template.Render(); } private bool WriteTemplate_MethodArguments(IWriteContext context) { if (context.Target is not MethodArgumentsToGenerate arguments) { return false; } context.Template.AddReplacement( Tokens.PropertyName, arguments.PropertyName); context.Template.AddReplacement( Tokens.BrowseName, arguments.BrowseName); context.Template.AddReplacement( Tokens.ListOfValues, NodeStateTemplates.ArgumentValue, arguments.Arguments, WriteTemplate_Argument); return context.Template.Render(); } private TemplateString? LoadTemplate_RolePermissionEntry(ILoadContext context) { if (context.Target is not RolePermission) { return null; } return NodeStateTemplates.RolePermission; } private bool WriteTemplate_RolePermissionEntry(IWriteContext context) { if (context.Target is not RolePermission rolePermission) { return false; } if (!m_context.ModelDesign.TryFindNode( rolePermission.Role, rolePermission.Role.Name, "RoleType", out ObjectDesign? roleNode)) { return false; } context.Template.AddReplacement( Tokens.RoleIdConstant, roleNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.PermissionsValue, CoreUtils.Format( "(uint)({0})", rolePermission.Permission.GetPermissionTypeAsCode())); return context.Template.Render(); } private void AddNodeStateClassVariableTypeReplacements( IWriteContext context, VariableTypeDesign variableType) { AddNodeStateClassTypeReplacements(context, variableType); BasicDataType basicType = variableType.DataTypeNode!.BasicDataType; if (variableType.SymbolicName.Name == "TwoStateDiscreteType") { variableType.ValueRank = ValueRank.Scalar; } if (!variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank)) { // Overrides a variable type that does not require a template parameter so we // need to add it context.Template.AddReplacement(Tokens.BaseT, string.Empty); } else { // Overrides a typed variable type where the base class requires a template // parameter, so the same template parameter is needed plus the Variant builder // to access the variant content of the type T. string parameter = GetTemplateParameter(variableType, out string variantBuilder); if (parameter == "" && variableType.ValueRank != ValueRank.Scalar) { parameter = ""; variantBuilder = "global::Opc.Ua.VariantBuilder"; } if (string.IsNullOrEmpty(parameter)) { context.Template.AddReplacement(Tokens.BaseT, string.Empty); } else { // The class extends from the generic one through its inner // Implementation class to provide generic variant // value accessors context.Template.AddReplacement( Tokens.BaseT, CoreUtils.Format("{0}.Implementation<{1}>", parameter, variantBuilder)); } } string valueRank = variableType.ValueRank.GetValueRankAsCode( variableType.ArrayDimensions); if (variableType.ValueRank == ValueRank.ScalarOrArray) { for (TypeDesign? baseType = variableType.BaseTypeNode; baseType != null; baseType = baseType.BaseTypeNode) { if (baseType.SymbolicId == new XmlQualifiedName("DataItemType", Namespaces.OpcUa)) { valueRank = $"global::Opc.Ua.ValueRanks.{ValueRank.Scalar}"; } } } AddVariantAccessor(context, variableType.DataTypeNode); if (variableType.DataTypeNode.NeedsCloning()) { // Must be the fully qualified type name: the cast lands in the // generated VariableTypeValue class, whose enclosing namespace is // not the one declaring the data type for a cross-model structure. // The same name the value field is declared with. context.Template.AddReplacement( Tokens.ValueWrite, CoreUtils.Format( "CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue", variableType.DataTypeNode.GetDotNetTypeName( ValueRank.Scalar, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.NonNullable))); } else { context.Template.AddReplacement(Tokens.ValueWrite, "newValue"); } if (variableType.DataTypeNode.IsDotNetEqualityComparable(variableType.ValueRank)) { context.Template.AddReplacement(Tokens.ValueComparison, "m_value != newValue"); } else { context.Template.AddReplacement(Tokens.ValueComparison, "!global::Opc.Ua.CoreUtils.IsEqual(m_value, newValue)"); } // The VariableType default value is emitted by the type's factory // (AddVariableTypeStateFactoryReplacements), which maps namespace // indexes through the NodeSet's table (OPC 10000-6 F.14). The state // class template has no default value: computing one here without // that table produced a NodeSet-local literal nobody rendered and // registered an unused XML value resource. context.Template.AddReplacement(Tokens.ValueRank, valueRank); context.Template.AddReplacement( Tokens.ArrayDimensions, variableType.ValueRank.GetArrayDimensionsAsCode( variableType.ArrayDimensions) ?? "default"); context.Template.AddReplacement(Tokens.IsAbstract, variableType.IsAbstract); context.Template.AddReplacement( Tokens.AccessLevelValue, variableType.AccessLevel.GetAccessLevelAsCode()); context.Template.AddReplacement( Tokens.MinimumSamplingIntervalValue, variableType.GetMinimumSamplingIntervalAsCode()); context.Template.AddReplacement( Tokens.Historizing, variableType.Historizing); context.Template.AddReplacement( Tokens.DataType, variableType.DataTypeNode.SymbolicName.Name); context.Template.AddReplacement( Tokens.DataTypeNamespacePrefix, m_context.ModelDesign.Namespaces.GetNamespacePrefix( variableType.DataTypeNode.SymbolicId.Namespace)); context.Template.AddReplacement( Tokens.DataTypeNamespaceUri, m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace( variableType.DataTypeNode.SymbolicId.Namespace)); context.Template.AddReplacement( Tokens.TypedVariableType, NodeStateTemplates.VariableTypeWithTypedValue_Class, [variableType], LoadTemplate_TypedVariableType, WriteTemplate_TypedVariableType); context.Template.AddReplacement( Tokens.VariableTypeValue, NodeStateTemplates.VariableTypeValue_Class, [variableType], LoadTemplate_VariableTypeValue, WriteTemplate_VariableTypeValue); } private void AddNodeStateClassObjectTypeReplacements( IWriteContext context, ObjectTypeDesign objectType) { AddNodeStateClassTypeReplacements(context, objectType); context.Template.AddReplacement(Tokens.BaseT, string.Empty); context.Template.AddReplacement(Tokens.IsAbstract, objectType.IsAbstract); context.Template.AddReplacement(Tokens.EventNotifier, objectType.GetEventNotifierAsCode()); context.Template.AddReplacement( Tokens.ElementNamespaceOverride, BuildElementNamespaceOverride(objectType)); } /// /// Emits the ElementNamespaceUri override for /// FiniteStateMachineType subtypes. Part 5 qualifies the /// numeric state and transition NodeIds a machine reports in /// CurrentState/Id and LastTransition/Id with the /// namespace of the model that declares those elements — which /// is not necessarily the machine's own namespace: a subtype /// that adds no states of its own keeps reporting the base /// type's, and the base class default (the OPC UA namespace) is /// then already right. Returns null (collapsing the /// template line) for every other object type. /// private string? BuildElementNamespaceOverride(ObjectTypeDesign objectType) { if (!IsFiniteStateMachineSubtype(objectType)) { return null; } string? namespaceUri = FindElementNamespaceUri(objectType); if (string.IsNullOrEmpty(namespaceUri) || namespaceUri == Namespaces.OpcUa) { // The base class already returns the OPC UA namespace. return null; } string? constant; try { constant = m_context.ModelDesign.Namespaces .GetConstantSymbolForNamespace(namespaceUri); } catch (ArgumentException e) { // A namespace entry with a URI but no Name — surface it // instead of failing the whole generation for a // diagnostic-only feature. m_logger.LogWarning( "Cannot emit ElementNamespaceUri for {Type}: {Error}", objectType.SymbolicName?.Name, e.Message); return null; } if (string.IsNullOrEmpty(constant)) { // Failing open here means the machine keeps the base // default (the OPC UA namespace) and its element ids // land in namespace 0 — say so rather than being silent. m_logger.LogWarning( "No namespace constant for '{Uri}' — {Type} will " + "qualify its state and transition ids with the OPC " + "UA namespace. Add the declaring model to the " + "design's to fix this.", namespaceUri, objectType.SymbolicName?.Name); return null; } return CoreUtils.Format( "/// {0}" + " protected override string ElementNamespaceUri => {1};", Environment.NewLine, constant); } /// /// The namespace of the nearest type in the hierarchy that /// actually declares StateType / TransitionType /// children. A subtype that only adds behaviour inherits its /// elements — and therefore their namespace — from its base. /// internal static string? FindElementNamespaceUri(ObjectTypeDesign objectType) { TypeDesign? current = objectType; while (current != null) { if (DeclaresStateMachineElements(current)) { return current.SymbolicName?.Namespace; } current = current.BaseTypeNode; } return null; } internal static bool DeclaresStateMachineElements(TypeDesign type) { InstanceDesign[]? children = type.Children?.Items; if (children == null) { return false; } foreach (InstanceDesign child in children) { if (child is not ObjectDesign childObject || childObject.TypeDefinition == null || !string.Equals( childObject.TypeDefinition.Namespace, Namespaces.OpcUa, StringComparison.Ordinal)) { continue; } if (childObject.OveriddenNode != null) { // Re-declaring an inherited state (to attach a // description, a modelling rule, ...) does not make // this type the one that declares the elements — // the ids still live in the base model's namespace. continue; } string typeDefName = childObject.TypeDefinition.Name; if (string.Equals(typeDefName, kStateTypeName, StringComparison.Ordinal) || string.Equals(typeDefName, kInitialStateTypeName, StringComparison.Ordinal) || string.Equals(typeDefName, kTransitionTypeName, StringComparison.Ordinal)) { return true; } } return false; } /// /// Whether the type derives, however indirectly, from the /// standard FiniteStateMachineType. /// internal static bool IsFiniteStateMachineSubtype(ObjectTypeDesign objectType) { TypeDesign? current = objectType; while (current != null) { if (string.Equals( current.SymbolicName?.Name, kFiniteStateMachineTypeName, StringComparison.Ordinal) && string.Equals( current.SymbolicName?.Namespace, Namespaces.OpcUa, StringComparison.Ordinal)) { return true; } current = current.BaseTypeNode; } return false; } private const string kFiniteStateMachineTypeName = "FiniteStateMachineType"; private const string kStateTypeName = "StateType"; private const string kInitialStateTypeName = "InitialStateType"; private const string kTransitionTypeName = "TransitionType"; private void AddNodeStateClassMethodTypeReplacements( IWriteContext context, MethodDesign method) { (Parameter[] inputArguments, Parameter[] outputArguments) = MethodDesignArgumentResolver.ResolveMethodArguments(method); string targetNamespace = m_context.ModelDesign.TargetNamespace.Value; string declaredClassName = method.GetNodeStateClassName( targetNamespace, [], applyStandardFallback: false); ModelDesignExtensions.AssignMethodArgumentCodeNames( inputArguments, outputArguments, s_methodArgumentCodeNameScope, declaredClassName + "Result"); // Declarations require an unqualified identifier, while fallback // tracking requires the fully-qualified name. Neither may apply // the reference-only fallback policy. StandardMethodStateFallback.RecordDeclaredMethodState( method.GetNodeStateClassName( targetNamespace, m_context.ModelDesign.Namespaces, applyStandardFallback: false)); context.Template.AddReplacement( Tokens.ClassName, declaredClassName); context.Template.AddReplacement( Tokens.OwnerClassName, declaredClassName); context.Template.AddReplacement( Tokens.ListOfInputArguments, inputArguments, LoadTemplate_ListOfInputArguments); context.Template.AddReplacement( Tokens.OnCallDeclaration, [method], LoadTemplate_OnCallDeclaration); context.Template.AddReplacement( Tokens.OnCallAsyncDeclaration, [method], LoadTemplate_OnCallAsyncDeclaration); context.Template.AddReplacement( Tokens.OnCallImplementation, [method], LoadTemplate_OnCallImplementation); context.Template.AddReplacement( Tokens.OnCallAsyncImplementation, [method], LoadTemplate_OnCallAsyncImplementation); context.Template.AddReplacement( Tokens.ListOfOutputDeclarations, outputArguments, LoadTemplate_ListOfOutputDeclarations); context.Template.AddReplacement( Tokens.ListOfOutputArgumentsFromResult, outputArguments, LoadTemplate_ListOfOutputArgumentsFromResult); context.Template.AddReplacement( Tokens.ListOfOutputArguments, outputArguments, LoadTemplate_ListOfOutputArguments); context.Template.AddReplacement( Tokens.ListOfResultProperties, outputArguments, LoadTemplate_ListOfResultProperties); } private void AddNodeStateClassTypeReplacements( IWriteContext context, TypeDesign type) { context.Template.AddReplacement(Tokens.ClassName, type.ClassName); // OwnerClassName mirrors ClassName at the type scope so per-child // sub-templates (e.g. NodeStateTemplates.OptionalMethod) can refer // to the parent (owner) class when generating Add{Child} methods // that return `this` for chaining. The per-child WriteTemplate // overrides Tokens.ClassName but leaves OwnerClassName untouched, // and the template engine cascades to this outer template when // OwnerClassName is not found locally. The class declaration // template appends "State" to ClassName (see // NodeStateClassObjectType / NodeStateClassVariableType), so do // the same here so the return type matches the declared class. context.Template.AddReplacement( Tokens.OwnerClassName, type.ClassName + "State"); context.Template.AddReplacement( Tokens.BaseClassName, type.BaseTypeNode!.GetClassName(m_context.ModelDesign.Namespaces)); context.Template.AddReplacement( Tokens.BaseTypeNamespacePrefix, m_context.ModelDesign.Namespaces.GetNamespacePrefix( type.BaseTypeNode!.SymbolicId.Namespace)); context.Template.AddReplacement( Tokens.BaseTypeNamespaceUri, m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace( type.BaseTypeNode.SymbolicId.Namespace)); } private void AddObjectTypeStateFactoryReplacements( IWriteContext context, ObjectTypeDesign node) { context.Template.AddReplacement( Tokens.SuperTypeId, node.BaseTypeNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract); context.Template.AddReplacement( Tokens.StateClassName, "global::Opc.Ua.BaseObjectTypeState"); context.Template.AddReplacement( Tokens.StateClassFactory, "new global::Opc.Ua.BaseObjectTypeState"); } private void AddVariableTypeStateFactoryReplacements( IWriteContext context, VariableTypeDesign node) { context.Template.AddReplacement(Tokens.SuperTypeId, node.BaseTypeNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract); context.Template.AddReplacement( Tokens.StateClassName, "global::Opc.Ua.BaseDataVariableTypeState"); context.Template.AddReplacement( Tokens.StateClassFactory, "new global::Opc.Ua.BaseDataVariableTypeState"); if (IsNodeSetValueNull(node.NodeSetValueIsNull, node.DefaultValue, node.DecodedValue)) { context.Template.AddReplacement(Tokens.ValueCode, kNullValueCode); } else { context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format( "baseState.WrappedValue = {0};", node.DataTypeNode!.GetValueAsCode( node.ValueRank, node.DefaultValue, node.DecodedValue, true, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, m_messageContext, () => AddXmlInitializerForComplexValue( node, node.DataTypeNode!, node.DefaultValue)!, node.DecodedValueNamespaceUris, kNamespaceTableContextVariable))); } string dataTypeId = GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces); context.Template.AddReplacement( Tokens.DataTypeIdConstant, dataTypeId ?? "global::Opc.Ua.NodeId.Null"); string valueRank = node.ValueRank.GetValueRankAsCode( node.ArrayDimensions); context.Template.AddReplacement(Tokens.ValueRank, valueRank); string? arrayDims = node.ValueRank.GetArrayDimensionsAsCode( node.ArrayDimensions); context.Template.AddReplacement( Tokens.ArrayDimensions, !string.IsNullOrEmpty(arrayDims) ? CoreUtils.Format("baseState.ArrayDimensions = {0};", arrayDims) : null); } private void AddVariableStateFactoryReplacements( IWriteContext context, NodeToGenerate nodeToGenerate, VariableDesign node, HashSet references) { context.Template.AddReplacement( Tokens.StateClassName, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement( Tokens.StateClassFactory, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true)); context.Template.AddReplacement( Tokens.TypeDefinitionId, node.TypeDefinitionNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.ReferenceTypeId, m_context.ModelDesign.GetNodeIdConstant( node.ReferenceType, "", kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.ModellingRuleId, GetModellingRuleReplacement( GetEffectiveModellingRule(nodeToGenerate, node))); context.Template.AddReplacement( Tokens.DataTypeIdConstant, GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement( Tokens.ValueRank, node.ValueRank.GetValueRankAsCode(node.ArrayDimensions)); string? arrayDims = node.ValueRank.GetArrayDimensionsAsCode(node.ArrayDimensions); context.Template.AddReplacement( Tokens.ArrayDimensions, !string.IsNullOrEmpty(arrayDims) ? CoreUtils.Format("baseState.ArrayDimensions = {0};", arrayDims) : null); context.Template.AddReplacement( Tokens.AccessLevelValue, node.GetAccessLevelAsCode()); context.Template.AddReplacement( Tokens.UserAccessLevelValue, node.GetUserAccessLevelAsCode()); context.Template.AddReplacement( Tokens.MinimumSamplingIntervalValue, node.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture)); context.Template.AddReplacement( Tokens.HistorizingValue, node.Historizing); // set dictionary to point to embedded schemas if (node.TypeDefinitionNode!.SymbolicId == new XmlQualifiedName("DataTypeDictionaryType", Namespaces.OpcUa)) { NodeDesign? typeSystemReference = references.FirstOrDefault()?.TargetNode; switch (typeSystemReference?.SymbolicId.Name) { case "XmlSchema_TypeSystem": context.Template.AddReplacement( Tokens.ValueCode, "baseState.WrappedValue = global::Opc.Ua.Variant.From(" + "global::Opc.Ua.ByteString.From(XmlSchemas.TypesXsd.ToArray()));"); return; case "OPCBinarySchema_TypeSystem": context.Template.AddReplacement( Tokens.ValueCode, "baseState.WrappedValue = global::Opc.Ua.Variant.From(" + "global::Opc.Ua.ByteString.From(XmlSchemas.TypesBsd.ToArray()));"); return; } // unknown type system context.Template.AddReplacement( Tokens.ValueCode, "baseState.WrappedValue = global::Opc.Ua.Variant.Null;"); return; } Parameter[]? methodArguments = GetMethodArgumentParameters(nodeToGenerate); if (methodArguments != null) { context.Template.AddReplacement( Tokens.ValueCode, NodeStateTemplates.VariantArrayOfValue, [methodArguments], WriteTemplate_ArgumentCollection); } else if (node.DecodedValue is IList args) { context.Template.AddReplacement( Tokens.ValueCode, NodeStateTemplates.VariantArrayOfValue, [args], WriteTemplate_ArgumentCollection); } else if (node.DecodedValue is ArrayOf arguments) { context.Template.AddReplacement( Tokens.ValueCode, NodeStateTemplates.VariantArrayOfValue, [arguments.ToList()], WriteTemplate_ArgumentCollection); } else if (IsNodeSetValueNull(node.NodeSetValueIsNull, node.DefaultValue, node.DecodedValue)) { context.Template.AddReplacement(Tokens.ValueCode, kNullValueCode); } else { context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format( "baseState.WrappedValue = {0};", node.DataTypeNode!.GetValueAsCode( node.ValueRank, node.DefaultValue, node.DecodedValue, true, m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, m_messageContext, () => AddXmlInitializerForComplexValue( node, node.DataTypeNode!, node.DefaultValue)!, node.DecodedValueNamespaceUris, kNamespaceTableContextVariable))); } } /// /// A Variable or VariableType imported from a NodeSet2 without a Value /// has a null Value. Only a ModelDesign implies the DataType default. /// private static bool IsNodeSetValueNull( bool nodeSetValueIsNull, System.Xml.XmlElement? defaultValue, object? decodedValue) { return nodeSetValueIsNull && defaultValue == null && decodedValue == null; } private const string kNullValueCode = "baseState.WrappedValue = global::Opc.Ua.Variant.Null;"; private static Parameter[]? GetMethodArgumentParameters(NodeToGenerate node) { if (node.Parent?.Design is not MethodDesign method) { return null; } if (node.Design!.SymbolicName == s_inputArgumentsBrowseName) { return MethodDesignArgumentResolver.ResolveMethodInputs(method); } if (node.Design.SymbolicName == s_outputArgumentsBrowseName) { return MethodDesignArgumentResolver.ResolveMethodOutputs(method); } return null; } private void AddReferenceTypeStateFactoryReplacements( IWriteContext context, ReferenceTypeDesign node) { context.Template.AddReplacement(Tokens.SuperTypeId, node.BaseTypeNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract); context.Template.AddReplacement(Tokens.SymmetricValue, node.Symmetric); context.Template.AddReplacement(Tokens.InverseNameValue, GetInverseNameValue(node)); } private void AddDataTypeStateFactoryReplacements( IWriteContext context, DataTypeDesign node) { context.Template.AddReplacement(Tokens.SuperTypeId, node.BaseTypeNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract); context.Template.AddReplacement(Tokens.Purpose, (Export.DataTypePurpose)(int)(node.Purpose == DataTypePurpose.Testing ? DataTypePurpose.CodeGenerator : node.Purpose)); if (node.BasicDataType is not BasicDataType.Enumeration and not BasicDataType.UserDefined) { context.Template.AddReplacement( Tokens.DataTypeDefinition, "global::Opc.Ua.ExtensionObject.Null"); } else { context.Template.AddReplacement( Tokens.DataTypeDefinition, CoreUtils.Format( "new global::Opc.Ua.ExtensionObject(global::{0}.DataTypeDefinitions.Create{1}({2}))", m_context.ModelDesign.Namespaces.GetNamespacePrefix(node.SymbolicName.Namespace), node.SymbolicName.Name, kNamespaceTableContextVariable)); } } private void AddObjectReplacements( IWriteContext context, NodeToGenerate nodeToGenerate, ObjectDesign node, HashSet references) { context.Template.AddReplacement( Tokens.StateClassName, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement( Tokens.StateClassFactory, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true)); context.Template.AddReplacement( Tokens.TypeDefinitionId, node.TypeDefinitionNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.ReferenceTypeId, m_context.ModelDesign.GetNodeIdConstant( node.ReferenceType, "", kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.ModellingRuleId, GetModellingRuleReplacement( GetEffectiveModellingRule(nodeToGenerate, node))); context.Template.AddReplacement( Tokens.EventNotifier, node.SupportsEvents || HasForwardEventReferences(references) ? "global::Opc.Ua.EventNotifiers.SubscribeToEvents" : "global::Opc.Ua.EventNotifiers.None"); } private void AddMethodStateFactoryReplacements( IWriteContext context, NodeToGenerate nodeToGenerate, MethodDesign node) { context.Template.AddReplacement( Tokens.ReferenceTypeId, m_context.ModelDesign.GetNodeIdConstant( node.ReferenceType, "", kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.StateClassName, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)); context.Template.AddReplacement( Tokens.StateClassFactory, node.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, asFactory: true)); context.Template.AddReplacement( Tokens.ModellingRuleId, GetModellingRuleReplacement( GetEffectiveModellingRule(nodeToGenerate, node))); bool executable = !node.NonExecutable; context.Template.AddReplacement(Tokens.ExecutableValue, executable); context.Template.AddReplacement( Tokens.MethodDeclarationId, node.MethodDeclarationNode != null ? CoreUtils.Format( "baseState.MethodDeclarationId = {0};", (HasResolvableNodeId(node.MethodDeclarationNode) || !node.NumericIdSpecified ? node.MethodDeclarationNode : node).GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)) : null); AddMethodArgumentsReplacement( context, nodeToGenerate, Tokens.ListOfInputArguments, BrowseNames.InputArguments, "inputArguments", node.InputArguments); AddMethodArgumentsReplacement( context, nodeToGenerate, Tokens.ListOfOutputArguments, BrowseNames.OutputArguments, "outputArguments", node.OutputArguments); } /// /// Emits the InputArguments or OutputArguments property of a method. /// The property is normally produced by its own child node factory, /// which carries the well known NodeId of the argument property and /// must therefore not be replaced - /// keeps the /// property that was assigned first. Only the arguments themselves are /// re-emitted from the resolved model, and only when the value of the /// property cannot be trusted (see /// ). When no child factory /// exists the whole property is created inline. /// private void AddMethodArgumentsReplacement( IWriteContext context, NodeToGenerate nodeToGenerate, string token, string browseName, string propertyName, Parameter[] arguments) { if (arguments == null || arguments.Length == 0) { return; } bool hasChild = HasArgumentChild(nodeToGenerate, browseName); if (hasChild && !RequiresArgumentValueOverride(arguments)) { return; } context.Template.AddReplacement( token, hasChild ? NodeStateTemplates.Assign_MethodArgumentValues : NodeStateTemplates.Create_MethodArguments, [ new MethodArgumentsToGenerate(propertyName, browseName, arguments) ], WriteTemplate_MethodArguments); } /// /// The value of an argument property imported from a nodeset is emitted /// verbatim as the XML of the source nodeset, where the NodeId of the /// argument data type is indexed against the namespace table of that /// nodeset. Such an index does not survive the import and resolves to /// an unrelated namespace at runtime, so the arguments have to be /// re-emitted from the resolved model. Arguments that only reference /// the OPC UA namespace always decode to the same value and are left /// alone. /// private static bool RequiresArgumentValueOverride(Parameter[] arguments) { return Array.Exists( arguments, argument => argument.DataTypeNode != null && !string.Equals( argument.DataTypeNode.SymbolicId.Namespace, Namespaces.OpcUa, StringComparison.Ordinal)); } /// /// The declaration node of a method is only usable as /// when it resolves to a /// real NodeId. Method type declarations that are synthesized while /// importing a nodeset have no identifier of their own and would emit a /// NodeId built from their symbolic name, which does not exist in any /// address space. In that case the method node itself is the /// declaration. /// private static bool HasResolvableNodeId(NodeDesign node) { return node.NumericIdSpecified || !string.IsNullOrEmpty(node.StringId) || node.HasNonConstantIdentifier() || node.FindNumericIdentifier().HasValue; } /// /// Indicates that the model generates a dedicated child node factory /// for the InputArguments or OutputArguments property of a method. /// private static bool HasArgumentChild(NodeToGenerate node, string browseName) { return node.AllChildren.Exists( child => child.Design is InstanceDesign instance && string.Equals( instance.SymbolicName?.Name, browseName, StringComparison.Ordinal)); } private static void AddViewStateFactoryReplacements( IWriteContext context, ViewDesign node) { context.Template.AddReplacement( Tokens.EventNotifier, node.SupportsEvents ? "global::Opc.Ua.EventNotifiers.SubscribeToEvents" : "global::Opc.Ua.EventNotifiers.None"); context.Template.AddReplacement(Tokens.ContainsNoLoopsValue, node.ContainsNoLoops); } /// /// Returns true if the references include a forward HasEventSource /// (i=36) or HasNotifier (i=48) reference. Per OPC UA Part 3, the /// EventNotifier attribute must be set on the source node of these /// references to indicate that events can be subscribed to. /// private static bool HasForwardEventReferences(HashSet references) { if (references == null) { return false; } foreach (ReferenceToGenerate reference in references) { if (!reference.IsInverse && reference.ReferenceTypeId != null && (reference.ReferenceTypeId.Name == "HasEventSource" || reference.ReferenceTypeId.Name == "HasNotifier")) { return true; } } return false; } private void CollectNodesToGenerate() { foreach (NodeDesign node in m_context.ModelDesign.Nodes) { if (m_context.ModelDesign.IsExcluded(node)) { continue; } NodeToGenerate? entry = null; if (node is not InstanceDesign) { entry = new NodeToGenerate( Parent: null, Path: string.Empty, Hierarchy: node.Hierarchy, Design: node, IsNotExplicitlyDefined: false, RootIsTypeDefinition: true, InstanceOf: null, IsUnderSingletonInstance: false); } else { entry = new NodeToGenerate( Parent: null, Path: string.Empty, Hierarchy: node.Hierarchy, Design: node.Hierarchy!.NodeList[0].Instance, IsNotExplicitlyDefined: false, RootIsTypeDefinition: false, InstanceOf: null, IsUnderSingletonInstance: true); } if (!m_nodes.TryAdd(entry.Design!.SymbolicId, entry) && m_logger.IsEnabled(LogLevel.Debug)) { m_logger.LogDebug( "Removing duplicate entry for {Node}.", entry.Design.SymbolicId.Name); } GetChildren(entry, m_nodes, false); bool isMethodWithArguments = node is MethodDesign method && MethodDesignArgumentResolver.HasMethodArguments(method); if ((node is not ObjectTypeDesign and not VariableTypeDesign) && !node.IsMethodTypeDesign() && !isMethodWithArguments) { continue; } // Add instances to generate if (node.Hierarchy == null || !node.Hierarchy.Nodes.TryGetValue( string.Empty, out HierarchyNode? hierarchyNode)) { continue; } if (hierarchyNode.Identifier != null) { if (hierarchyNode.Identifier is uint numericId) { hierarchyNode.Instance!.NumericId = numericId; hierarchyNode.Instance.NumericIdSpecified = true; } else if (hierarchyNode.Identifier is string stringId) { hierarchyNode.Instance!.StringId = stringId; } else if (hierarchyNode.Identifier is Guid or ByteString) { hierarchyNode.Instance.SetIdentifier(hierarchyNode.Identifier); } else { throw new InvalidOperationException(CoreUtils.Format( "Invalid identifier {0}", hierarchyNode.Identifier)); } } var instanceToGenerate = new NodeToGenerate( Parent: null, Path: string.Empty, Hierarchy: node.Hierarchy, Design: hierarchyNode.Instance, IsNotExplicitlyDefined: false, RootIsTypeDefinition: false, InstanceOf: entry, // Mark as instance of a type design IsUnderSingletonInstance: false); entry.Instance = instanceToGenerate; if (!m_instances.TryAdd(instanceToGenerate.Design!.SymbolicId, instanceToGenerate) && m_logger.IsEnabled(LogLevel.Debug)) { m_logger.LogDebug( "Removing duplicate entry for instance {Node}.", instanceToGenerate.Design.SymbolicId.Name); } GetChildren(instanceToGenerate, m_instances, true); } // Collect DataType encoding nodes (Default Binary, Default XML, Default JSON). // These are ObjectDesign instances with TypeDefinition=DataTypeEncodingType // that are linked to DataTypes via HasEncoding references. They don't have // a Hierarchy from the model validation, so we build a minimal one with // their references to emit them into the address space. CollectEncodingNodes(); // Index well-known singleton roots by their TypeDefinition so the // child-emission writer can dispatch the type-level factory's // forInstance: true branch into the matching singleton-instance // child factory (e.g. CreateServerType_GetMonitoredItems can // produce InputArguments at the well-known Server_* // NodeIds when the parent is the Server singleton). BuildSingletonInstanceIndex(); } /// /// Populates with every singleton /// instance root or nested well-known instance in /// grouped by its /// 's SymbolicId. /// Top-level entries correspond to declared /// <opc:Object> / <opc:Variable> /// singletons such as Server or /// WellKnownRole_Observer; nested entries cover Mandatory /// well-known children such as Server_ServerCapabilities /// (an instance of ServerCapabilitiesType) so the type-level /// factory's child-dispatch can produce the well-known instance /// NodeIds at every depth. /// private void BuildSingletonInstanceIndex() { foreach (NodeToGenerate entry in m_nodes.Values) { if (entry.Design is not InstanceDesign instance) { continue; } TypeDesign? typeDef = instance.TypeDefinitionNode; if (typeDef == null || typeDef.SymbolicId == null) { continue; } if (!m_singletonsByType.TryGetValue(typeDef.SymbolicId, out List? singletons)) { singletons = []; m_singletonsByType[typeDef.SymbolicId] = singletons; } singletons.Add(entry); } } /// /// Given the type-level child that is /// about to be emitted inside a type-level factory body (e.g. the /// InputArguments slot inside CreateServerType_GetMonitoredItems), /// returns the singleton-instance equivalents — the same relative /// path under each known singleton instance of the type root, when /// such a child has been collected. /// private List<(NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)> FindSingletonChildren(NodeToGenerate typeChild) { var result = new List< ( NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)>(); if (typeChild == null || !typeChild.RootIsTypeDefinition) { return result; } // Walk up to the type root NodeToGenerate. NodeToGenerate typeRoot = typeChild; while (typeRoot.Parent != null) { typeRoot = typeRoot.Parent; } if (typeRoot.Design is not TypeDesign typeDef || typeDef.SymbolicId == null) { return result; } // Find singletons of this type, if any. if (!m_singletonsByType.TryGetValue(typeDef.SymbolicId, out List? singletons)) { return result; } // The relative path suffix is the part of the child's SymbolicId // that follows the type-root SymbolicId. Both type and singleton // trees use the same "_" path scheme, so we // can compute the singleton child SymbolicId by prefix swap. string typeRootName = typeDef.SymbolicId.Name; string childName = typeChild.Design!.SymbolicId.Name; if (childName == null || !childName.StartsWith(typeRootName, StringComparison.Ordinal)) { return result; } string relativeSuffix = childName[typeRootName.Length..]; // Only count singletons whose corresponding child has actually // been collected (m_nodes contains the singleton-instance // descendants). For example, RoleType has both modifiable // (Observer/Operator/...) and immutable (Anonymous/...) // singletons; only the modifiable ones have the Mandatory // promoted AddIdentity child. foreach (NodeToGenerate singleton in singletons) { if (singleton.Design?.SymbolicId == null) { continue; } string singletonChildName = singleton.Design.SymbolicId.Name + relativeSuffix; var singletonChildSymbolicId = new XmlQualifiedName( singletonChildName, typeChild.Design.SymbolicId.Namespace); if (m_nodes.TryGetValue(singletonChildSymbolicId, out NodeToGenerate? singletonChild)) { result.Add((singleton, singletonChild, singletonChildName)); } } return result; } /// /// Registers the /// replacement for the type-level factory currently being emitted. /// When the node has well-known singleton instances (e.g. a method /// declared under ServerType whose Server singleton /// has its own well-known NodeId), the wrapper template /// renders a runtime dispatch that rebinds nodeState.NodeId to /// the singleton-instance value when forInstance: true is /// passed AND parent matches one of the known singleton /// instances. The token expands to nothing when no singleton /// dispatch is applicable. /// private void AddInstanceNodeIdOverrideReplacement( IWriteContext context, NodeToGenerate node) { List<(NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)> singletonChildren = FindSingletonChildren(node); context.Template.AddReplacement( Tokens.InstanceNodeIdOverride, NodeStateTemplates.InstanceNodeIdOverride, singletonChildren.Count > 0 ? [singletonChildren] : [], WriteTemplate_InstanceNodeIdOverride); } /// /// Wrapper writer for ; /// expands the inner /// token by iterating the collected per-singleton branches. /// private bool WriteTemplate_InstanceNodeIdOverride(IWriteContext context) { if (context.Target is not List< ( NodeToGenerate Singleton, NodeToGenerate SingletonChild, string SingletonChildSymbolicId)> branches) { return false; } context.Template.AddReplacement( Tokens.ListOfInstanceNodeIdBranches, NodeStateTemplates.InstanceNodeIdBranch, [.. branches.Cast()], WriteTemplate_InstanceNodeIdBranch); return context.Template.Render(); } /// /// Per-branch writer for ; /// the first branch emits if, subsequent ones else if. /// private bool WriteTemplate_InstanceNodeIdBranch(IWriteContext context) { if (context.Target is not ValueTuple branch) { return false; } context.Template.AddReplacement( Tokens.IfOrElseIf, context.Index == 0 ? "if" : "else if"); context.Template.AddReplacement( Tokens.ParentNodeIdConstant, branch.Item1.Design.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); context.Template.AddReplacement( Tokens.NodeIdConstant, branch.Item2.Design.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)); return context.Template.Render(); } private void CollectEncodingNodes() { foreach (NodeDesign node in m_context.ModelDesign.Nodes) { if (node is not ObjectDesign encoding) { continue; } if (encoding.TypeDefinition == null || encoding.TypeDefinition.Name != "DataTypeEncodingType") { continue; } if (encoding.NotInAddressSpace) { continue; } if (m_context.ModelDesign.IsExcluded(encoding)) { continue; } // Skip if already collected if (m_nodes.ContainsKey(encoding.SymbolicId)) { continue; } // Build a synthetic Hierarchy with the encoding's references var hierarchy = new Hierarchy(); if (encoding.References != null) { foreach (Reference reference in encoding.References) { if (reference.ReferenceType == null) { continue; } hierarchy.References.Add(new HierarchyReference { SourcePath = string.Empty, ReferenceType = reference.ReferenceType, IsInverse = reference.IsInverse, TargetId = reference.TargetId, TargetPath = null }); } } encoding.Hierarchy = hierarchy; var entry = new NodeToGenerate( Parent: null, Path: string.Empty, Hierarchy: hierarchy, Design: encoding, IsNotExplicitlyDefined: false, RootIsTypeDefinition: false, InstanceOf: null); m_nodes.TryAdd(encoding.SymbolicId, entry); } } private bool ExcludeNodeStateClassGeneration(NodeToGenerate node) { // Filter unncessary nodes if (!IsInAddressSpace(node)) { if (m_logger.IsEnabled(LogLevel.Debug)) { m_logger.LogDebug( "Excluded node {Node} as it is marked NotInAddressSpace.", node.Design!.SymbolicId.Name); } return true; } // Only process type designs and method types for definitions // Also allow DataTypeEncodingType instances (Default Binary/XML/JSON) if (node.Design is not VariableTypeDesign and not ObjectTypeDesign && !node.Design.IsMethodTypeDesign() && !IsDataTypeEncodingInstance(node.Design)) { return true; } if (node.Design!.SymbolicName.Namespace == Namespaces.OpcUa && node.Design.NumericId < 256) { switch (node.Design.SymbolicName.Name) { case "DataTypeDictionaryType": case "DataTypeDescriptionType": case "DataTypeSystemType": case "DataTypeEncodingType": case "ModellingRuleType": break; default: if (m_logger.IsEnabled(LogLevel.Debug)) { m_logger.LogDebug( "Skipping built-in node state class generation for {Node}.", node.Design.SymbolicId.Name); } return true; } } return false; } private static bool IsInAddressSpace(NodeToGenerate node) { bool isInAddressSpace = !node.Design!.NotInAddressSpace; if (node.Design is InstanceDesign instanceDesign && instanceDesign.TypeDefinition != null && instanceDesign.TypeDefinition.Name == "DataTypeEncodingType") { isInAddressSpace = instanceDesign.Parent == null || !instanceDesign.Parent.NotInAddressSpace; } return isInAddressSpace; } private static bool IsDataTypeEncodingInstance(NodeDesign? node) { return node is ObjectDesign objectDesign && objectDesign.TypeDefinition != null && objectDesign.TypeDefinition.Name == "DataTypeEncodingType"; } /// /// Checks if the instance is a built in property of a base node state /// implementation (variable/method) that should not be generated because /// it is already defined on the root nodestate class. /// private static bool IsBuiltInProperty(NodeToGenerate node) { switch (node.Parent?.Design) { case MethodDesign: if (node.Design!.SymbolicName == new XmlQualifiedName("InputArguments", Namespaces.OpcUa)) { return true; } if (node.Design.SymbolicName == new XmlQualifiedName("OutputArguments", Namespaces.OpcUa)) { return true; } break; case VariableDesign: case VariableTypeDesign: if (node.Design!.SymbolicName == new XmlQualifiedName("EnumStrings", Namespaces.OpcUa)) { return true; } break; } return false; } internal static bool HasFixedChildSlot(TypeDesign? typeDefinitionNode, string symbolicName) { if (typeDefinitionNode == null) { return false; } if (typeDefinitionNode.Children?.Items != null) { foreach (InstanceDesign child in typeDefinitionNode.Children.Items) { if (child.SymbolicName.Name == symbolicName && child.ModellingRule is ModellingRule.Mandatory or ModellingRule.Optional) { return true; } } } return HasFixedChildSlot(typeDefinitionNode.BaseTypeNode, symbolicName); } /// /// Returns the effective modelling rule for a . /// When /// is enabled (opt-in) and the node belongs to an instance hierarchy, /// the type definition's modelling rule is used unconditionally. /// When the option is off (default), the instance may only /// promote the type-definition rule per OPC UA rules: /// /// Optional → Mandatory /// OptionalPlaceholder → Mandatory | MandatoryPlaceholder /// MandatoryPlaceholder → Mandatory /// /// Any other instance override (demotion) is ignored and the /// type-definition rule is returned instead. /// private ModellingRule GetEffectiveModellingRule( NodeToGenerate node, InstanceDesign instance) { if (!node.RootIsTypeDefinition && node.TypeDefinitionModellingRule.HasValue) { ModellingRule typeDefRule = node.TypeDefinitionModellingRule.Value; if (m_context.Options.UseTypeDefinitionModellingRules) { return typeDefRule; } // When not opt-in, only allow valid promotions per // the OPC UA modelling rule standard. if (!IsValidPromotion(typeDefRule, instance.ModellingRule)) { return typeDefRule; } } return instance.ModellingRule; } /// /// Returns the effective modelling rule for a hierarchy node. /// Overload used in where the hierarchy /// node and root-is-type-definition flag are available directly. /// private ModellingRule GetEffectiveModellingRule( HierarchyNode hierarchyNode, InstanceDesign child, bool rootIsTypeDefinition) { if (!rootIsTypeDefinition && hierarchyNode.TypeDefinitionModellingRule.HasValue) { ModellingRule typeDefRule = hierarchyNode.TypeDefinitionModellingRule.Value; if (m_context.Options.UseTypeDefinitionModellingRules) { return typeDefRule; } // When not opt-in, only allow valid promotions per // the OPC UA modelling rule standard. if (!IsValidPromotion(typeDefRule, child.ModellingRule)) { return typeDefRule; } } return child.ModellingRule; } /// /// Determines whether the instance modelling rule is a valid /// promotion of the type-definition modelling rule per OPC UA: /// /// Optional → Mandatory /// OptionalPlaceholder → Mandatory | MandatoryPlaceholder /// MandatoryPlaceholder → Mandatory /// /// Returns true when the instance rule is an allowed /// promotion (or identical to the type-definition rule), and /// false when it is a demotion that should be rejected. /// private static bool IsValidPromotion( ModellingRule typeDefRule, ModellingRule instanceRule) { if (instanceRule == typeDefRule) { return true; } return typeDefRule switch { ModellingRule.Optional => instanceRule == ModellingRule.Mandatory, ModellingRule.OptionalPlaceholder => instanceRule is ModellingRule.Mandatory or ModellingRule.MandatoryPlaceholder, ModellingRule.MandatoryPlaceholder => instanceRule == ModellingRule.Mandatory, // Mandatory, None, ExposesItsArray, etc. cannot be promoted _ => false }; } private void GetChildren( NodeToGenerate node, Dictionary children, bool forInstance) { if (!forInstance) { if (node.Design!.Children?.Items != null) { foreach (InstanceDesign child in node.Design.Children.Items) { if (m_context.ModelDesign.IsExcluded(child)) { continue; } string childPath = child.SymbolicName.Name; if (!string.IsNullOrEmpty(node.Path)) { childPath = CoreUtils.Format( "{0}{1}{2}", node.Path, NodeDesign.PathChar, childPath); } node.Children[child.SymbolicName.Name] = new NodeToGenerate( node, childPath, node.Hierarchy, child, IsNotExplicitlyDefined: false, RootIsTypeDefinition: node.RootIsTypeDefinition, InstanceOf: null); } } } if (node.Hierarchy == null) { return; } foreach (HierarchyNode current in node.Hierarchy.NodeList) { string? childPath = current.RelativePath; // only looking for nodes in the current tree. if (!childPath!.StartsWith(node.Path!, StringComparison.Ordinal)) { continue; } // ignore reference to the current base node. if (childPath == node.Path) { continue; } // relative should always end in the name of the current instance. if (!childPath.EndsWith( current.Instance!.SymbolicName.Name, StringComparison.Ordinal)) { continue; } // get the parent path. if (childPath.Length <= current.Instance.SymbolicName.Name.Length) { if (!string.IsNullOrEmpty(node.Path)) { continue; } } else { int idx = childPath.Length - current.Instance.SymbolicName.Name.Length - 1; string parentPath = current.RelativePath![..idx]; if (parentPath != node.Path) { continue; } } if (!string.IsNullOrEmpty(node.Path)) { childPath = childPath[(node.Path.Length + 1)..]; childPath = CoreUtils.Format( "{0}{1}{2}", node.Path, NodeDesign.PathChar, childPath); } if (current.Instance is not InstanceDesign child) { continue; } ModellingRule effectiveRule = GetEffectiveModellingRule( current, child, node.RootIsTypeDefinition); if (!node.RootIsTypeDefinition && !current.ExplicitlyDefined && effectiveRule != ModellingRule.Mandatory && effectiveRule != ModellingRule.Optional) { continue; } if (!current.ExplicitlyDefined && effectiveRule != ModellingRule.Mandatory && effectiveRule != ModellingRule.None && effectiveRule != ModellingRule.ExposesItsArray && effectiveRule != ModellingRule.OptionalPlaceholder && effectiveRule != ModellingRule.MandatoryPlaceholder) { continue; } if (node.RootIsTypeDefinition && !current.ExplicitlyDefined && current.Inherited && current.AdHocInstance && effectiveRule != ModellingRule.Mandatory) { // Required descendants of generic containers such as DI ParameterSet // must also initialize derived instances. Keep this shallow-level // suppression only for non-mandatory inherited ad-hoc declarations. if (!node.Path!.Contains(NodeDesign.PathChar, StringComparison.Ordinal)) { continue; } } bool add = false; if (node.Design is DataTypeDesign or ViewDesign or ReferenceTypeDesign) { add = true; } else if (node.RootIsTypeDefinition) { if (effectiveRule == ModellingRule.Mandatory) { add = true; } else if (current.ExplicitlyDefined && effectiveRule == ModellingRule.Optional) { add = true; } else if (current.ExplicitlyDefined && (effectiveRule is ModellingRule.ExposesItsArray or ModellingRule.OptionalPlaceholder or ModellingRule.MandatoryPlaceholder)) { add = true; } else if (current.StaticValue && !current.Inherited) { add = true; } else if (effectiveRule == ModellingRule.None && current.ExplicitlyDefined) { // Include ModellingRule=None children that are explicitly defined // on this type (e.g., state machine states/transitions, type-level // methods like ConditionRefresh, and type-level properties like // Creatable or SupportsFilteredRetain). add = true; } else if (effectiveRule is not ModellingRule.None) { if (m_logger.IsEnabled(LogLevel.Debug)) { m_logger.LogDebug( "Excluding child node {Node} from generation.", current.Instance.SymbolicId.Name); } } } else if (effectiveRule == ModellingRule.Mandatory) { add = true; } else if (current.ExplicitlyDefined) { add = true; } else if (m_logger.IsEnabled(LogLevel.Information)) { m_logger.LogInformation( "Excluding child node {Node} from generation.", current.Instance.SymbolicId.Name); } if (add) { XmlQualifiedName symbolicId = current.Instance.SymbolicId; var childNodeToGenerate = new NodeToGenerate( node, childPath, node.Hierarchy, child, IsNotExplicitlyDefined: !current.ExplicitlyDefined, RootIsTypeDefinition: node.RootIsTypeDefinition, InstanceOf: null, TypeDefinitionModellingRule: current.TypeDefinitionModellingRule, IsUnderSingletonInstance: node.IsUnderSingletonInstance); if (!children.TryAdd(symbolicId, childNodeToGenerate) && m_logger.IsEnabled(LogLevel.Information)) { m_logger.LogInformation( "Removing duplicate entry for child {Node}.", symbolicId.Name); } node.AllChildren.Add(childNodeToGenerate); GetChildren(childNodeToGenerate, children, forInstance); } } } private HashSet GetReferences(NodeToGenerate node) { if (node.Hierarchy == null) { return []; } Hierarchy hierarchy = node.Hierarchy; NodeDesign? root = node.Design; // A predefined instance of a NodeSet2 input does not create the // nodes it omits, so no reference may end in one of them. bool omitsUndeclaredNodes = OmitsUndeclaredHierarchyNodes(node); HashSet references = []; foreach (HierarchyReference reference in hierarchy.References) { if (reference.SourcePath != node.Path && reference.TargetPath != node.Path) { continue; } bool isInverse = reference.IsInverse; // The type's GeneratesEvent references describe its instances; an Object or // Variable cannot be their source (Part 3 §7.15), so they stay on the type. if (!isInverse && reference.DefinedOnType && reference.SourcePath == node.Path && root is ObjectDesign or VariableDesign && IsGeneratesEventReference(reference.ReferenceType)) { continue; } if (reference.TargetId != null) { if (!m_context.ModelDesign.TryFindNode( reference.TargetId, root!.SymbolicId.Name, reference.ReferenceType!.Name, out NodeDesign? targetNode)) { continue; } references.Add(new ReferenceToGenerate( targetNode, reference.ReferenceType, isInverse)); continue; } if (reference.TargetPath != null && reference.TargetPath.Length == 0 && node.Parent?.Design != null) { // An empty target path names the root of the hierarchy // (the type or the top-level instance), which is only the // immediate parent for a direct child. NodeToGenerate hierarchyRoot = node.Parent; while (hierarchyRoot.Parent?.Design != null) { hierarchyRoot = hierarchyRoot.Parent; } references.Add(new ReferenceToGenerate( hierarchyRoot.Design!, reference.ReferenceType!, isInverse)); continue; } if (reference.SourcePath == node.Path) { if (!hierarchy.Nodes.TryGetValue( reference.TargetPath!, out HierarchyNode? target)) { continue; } if (!target.ExplicitlyDefined && node.RootIsTypeDefinition) { continue; } if (omitsUndeclaredNodes && !IsCreatedInNodeSetAddressSpace(node, reference.TargetPath!)) { continue; } references.Add(new ReferenceToGenerate( target.Instance!, reference.ReferenceType!, isInverse)); continue; } if (!hierarchy.Nodes.TryGetValue(reference.SourcePath!, out HierarchyNode? source)) { continue; } if (!source.ExplicitlyDefined && node.RootIsTypeDefinition) { continue; } if (omitsUndeclaredNodes && !IsCreatedInNodeSetAddressSpace(node, reference.SourcePath!)) { continue; } references.Add(new ReferenceToGenerate( source.Instance!, reference.ReferenceType!, !isInverse)); } // Apply the instance filter after resolving both identifier and hierarchy-path targets. if (!node.RootIsTypeDefinition) { references.RemoveWhere(reference => reference.TargetNode is InstanceDesign { ModellingRule: ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder }); } // Inherited and declared designs can differ structurally while // resolving to the same runtime reference. HashSet<(string ReferenceTypeId, bool IsInverse, string TargetId)> referenceIds = []; references.RemoveWhere(reference => !referenceIds.Add(( m_context.ModelDesign.GetNodeIdConstant( reference.ReferenceTypeId, "", kNamespaceTableContextVariable), reference.IsInverse, reference.TargetNode.GetNodeIdAsCode( m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable)))); return references; } /// /// True for GeneratesEvent and its subtypes such as AlwaysGeneratesEvent. /// private bool IsGeneratesEventReference(XmlQualifiedName? referenceTypeId) { var generatesEvent = new XmlQualifiedName("GeneratesEvent", Ua.Types.Namespaces.OpcUa); if (referenceTypeId == generatesEvent) { return true; } if (referenceTypeId == null || !m_context.ModelDesign.TryFindNode( referenceTypeId, string.Empty, string.Empty, out NodeDesign? design)) { return false; } for (TypeDesign? type = (design as ReferenceTypeDesign)?.BaseTypeNode; type != null; type = type.BaseTypeNode) { if (type.SymbolicId == generatesEvent) { return true; } } return false; } private List GetAdditionalChildren(NodeToGenerate node) { List additionalChildren = []; foreach (NodeToGenerate child in node.Children.Values) { if (child.Design is not InstanceDesign instance) { continue; } if (child.IsNotExplicitlyDefined) { continue; } if (GetEffectiveModellingRule(child, instance) is not ModellingRule.Mandatory and not ModellingRule.Optional) { continue; } if (instance.IsOverridden() && !IsMethodOverrideWithDifferentClass(instance)) { continue; } additionalChildren.Add(instance); } return additionalChildren; } /// /// Returns whether a method overrides the supertype's method with /// another method state class. The subtype then declares a field and a /// property of its own that hide the supertype's, so it also has to /// enumerate and remove the child itself: the supertype's field stays /// empty. /// private bool IsMethodOverrideWithDifferentClass(InstanceDesign instance) { return instance is MethodDesign && instance.IsOverridden() && !instance.IsOverriddenWithSameClass( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces); } private List GetChildrenWithProperties( NodeToGenerate node, bool excludeBuiltIn = false) { List childrenWithProperties = []; foreach (NodeToGenerate child in node.Children.Values) { if (child.Design is not InstanceDesign instance) { continue; } if (child.IsNotExplicitlyDefined) { continue; } if (GetEffectiveModellingRule(child, instance) is not ModellingRule.Mandatory and not ModellingRule.Optional) { continue; } if (instance.IsOverriddenWithSameClass( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces)) { continue; } if (excludeBuiltIn && IsBuiltInProperty(child)) { continue; } childrenWithProperties.Add(instance); } return childrenWithProperties; } private HashSet GetRolePermissions(NodeDesign node) { var rolePermissions = new HashSet(); // Emit RolePermissions on all nodes (type and instance). // Type hierarchy nodes carry permissions as metadata but the server // bypasses enforcement via IsPartOfTypeHierarchy at runtime. RolePermission[]? nodeRolePermissions = (node.RolePermissions?.RolePermission) ?? (node.DefaultRolePermissions?.RolePermission); if (nodeRolePermissions != null) { foreach (RolePermission rp in nodeRolePermissions) { ObjectDesign? roleNode = m_context.ModelDesign.FindNode( rp.Role, rp.Role.Name, "RoleType"); if (roleNode != null) { rolePermissions.Add(rp); } } } return rolePermissions; } private static string? GetDisplayNameValue(NodeDesign node) { if (node.DisplayName != null) { return CoreUtils.Format( "{0}, string.Empty, {1}", node.DisplayName.Key.AsStringLiteral(), (node.DisplayName.Value?.Trim()).AsStringLiteral()); } return null; } private static string? GetDescriptionValue(NodeDesign node) { if (node.Description != null && !node.Description.IsAutogenerated) { // The generated templates assign through a nodeState local that // is statically typed as NodeState, so models with a child // Property named "Description" cannot shadow this assignment. return CoreUtils.Format( "nodeState.Description = {0};", node.Description.GetLocalizedTextAsCode()); } return null; } private static string? GetInverseNameValue(ReferenceTypeDesign node) { if (!node.Symmetric && node.InverseName != null) { return CoreUtils.Format( "state.InverseName = {0};", node.InverseName.GetLocalizedTextAsCode()); } if (node.Symmetric) { return "state.InverseName = global::Opc.Ua.LocalizedText.Null;"; } return null; } private static string? GetModellingRuleReplacement(ModellingRule modellingRule) { string? constant = modellingRule switch { ModellingRule.Mandatory => "global::Opc.Ua.Objects.ModellingRule_Mandatory", ModellingRule.Optional => "global::Opc.Ua.Objects.ModellingRule_Optional", ModellingRule.MandatoryPlaceholder => "global::Opc.Ua.Objects.ModellingRule_MandatoryPlaceholder", ModellingRule.OptionalPlaceholder => "global::Opc.Ua.Objects.ModellingRule_OptionalPlaceholder", ModellingRule.ExposesItsArray => "global::Opc.Ua.Objects.ModellingRule_ExposesItsArray", _ => null }; return constant != null ? CoreUtils.Format("baseState.ModellingRuleId = new global::Opc.Ua.NodeId({0});", constant) : null; } private string GetNodeIdConstantForDataType( VariableTypeDesign type, Namespace[] namespaceUris) { if (!m_context.ModelDesign.UseAllowSubtypes) { DataTypeDesign? dataType = m_context.ModelDesign.FindNode( type.DataType, type.SymbolicId.Name, "DataType"); return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable); } return type.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable); } private string GetNodeIdConstantForDataType( VariableDesign instance, Namespace[] namespaceUris) { if (!m_context.ModelDesign.UseAllowSubtypes) { DataTypeDesign? dataType = m_context.ModelDesign.FindNode( instance.DataType, instance.SymbolicId.Name, "DataType"); return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable); } return instance.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable); } private string GetTemplateParameter(TypeDesign type, out string variantBuilder) { variantBuilder = "global::Opc.Ua.VariantBuilder"; if (type is not VariableTypeDesign variableType) { return string.Empty; } if (type.BaseTypeNode == null) { return ""; } if (GetTemplateParameter(type.BaseTypeNode, out variantBuilder) != "") { return string.Empty; } BasicDataType basicType = variableType.DataTypeNode!.BasicDataType; if (basicType == BasicDataType.BaseDataType) // == Variant, so any { return ""; } string scalarName; switch (basicType) { case BasicDataType.UserDefined: scalarName = variableType.DataTypeNode.GetClassName( m_context.ModelDesign.Namespaces); variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName); break; default: scalarName = variableType.DataTypeNode.GetDotNetTypeName( m_context.ModelDesign.TargetNamespace.Value, m_context.ModelDesign.Namespaces, nullable: NullableAnnotation.NonNullable, true); variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName); break; } if (variableType.ValueRank == ValueRank.Scalar) { return $"<{scalarName}>"; } if (variableType.ValueRank == ValueRank.Array) { return $">"; } if (variableType.ValueRank == ValueRank.OneOrMoreDimensions && variableType.DataTypeNode.SupportsMatrixOf()) { return $">"; } variantBuilder = "global::Opc.Ua.VariantBuilder"; return ""; } private static void CollectMatchingFields( VariableTypeDesign variableType, Dictionary fields) { CollectFields( variableType.DataTypeNode!, variableType.ValueRank, string.Empty, fields); List availablePaths = [.. fields.Keys]; for (int ii = 0; ii < availablePaths.Count; ii++) { if (!variableType.Hierarchy.Nodes.ContainsKey(availablePaths[ii])) { fields.Remove(availablePaths[ii]); } } } private static void CollectFields( DataTypeDesign dataType, ValueRank valueRank, string basePath, Dictionary fields) { if (dataType.BasicDataType != BasicDataType.UserDefined || valueRank != ValueRank.Scalar) { return; } for (DataTypeDesign? parent = dataType; parent != null; parent = parent.BaseTypeNode as DataTypeDesign) { if (parent.Fields != null) { for (int ii = 0; ii < parent.Fields.Length; ii++) { Parameter parameter = parent.Fields[ii]; string fieldPath = parameter.Name; if (!string.IsNullOrEmpty(basePath)) { fieldPath = CoreUtils.Format("{0}_{1}", basePath, parameter.Name); } fields[fieldPath] = parameter; } } } } private string? AddXmlInitializerForComplexValue( NodeDesign node, DataTypeDesign dataType, System.Xml.XmlElement element) { string? xml = element?.OuterXml; if (string.IsNullOrEmpty(xml)) { return null; } string resourceName = CoreUtils.Format("Values.{0}", node.SymbolicId.Name); string uniqueName = resourceName; for (int i = 0; i < 1000; i++) { if (m_initializers.TryAdd(uniqueName, new TextResource(uniqueName, xml))) { // Get code to create the variant from the XML resource reference return CoreUtils.Format( "global::Opc.Ua.Variant.FromXml({0}AsStream, context)", uniqueName); } uniqueName = resourceName + i; } throw new InvalidOperationException("Unexpected duplicate resource names"); } private static void AddVariantAccessor(IWriteContext context, DataTypeDesign dataType) { switch (dataType.BasicDataType) { case BasicDataType.UserDefined when !dataType.IsEnumeration: context.Template.AddReplacement(Tokens.VariantFrom, "FromStructure"); context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetStructure"); break; default: context.Template.AddReplacement(Tokens.VariantFrom, "From"); context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetValue"); break; } } /// /// Embed all initializers as source code /// private Resource? EmbedInitializers() { if (m_initializers.Count == 0) { return null; } var initializers = new ResourceGenerator(m_context); return initializers.Embed( m_context.ModelDesign.TargetNamespace.Prefix, "NodeStates.i", internalAccess: true, [.. m_initializers.Values]); } /// /// Node state generation template /// private record class NodeToGenerate( NodeToGenerate? Parent = null, string? Path = null, Hierarchy? Hierarchy = null, NodeDesign? Design = null, bool IsNotExplicitlyDefined = false, bool RootIsTypeDefinition = false, NodeToGenerate? InstanceOf = null, ModellingRule? TypeDefinitionModellingRule = null, bool IsUnderSingletonInstance = false) { /// /// Full inherited list of children /// public List AllChildren { get; } = []; /// /// Direclty defined /// public Dictionary Children { get; } = []; public NodeToGenerate? Instance { get; set; } /// public override string ToString() { return CoreUtils.Format("{0} (Parent: {1})", Design, Parent); } } private record class ReferenceToGenerate( NodeDesign TargetNode, XmlQualifiedName ReferenceTypeId, bool IsInverse); /// /// True when the argument's code type is the data type itself (scalar, /// ArrayOf or MatrixOf). Every other value rank (ScalarOrArray, /// ScalarOrOneDimension, Any) is carried as a plain Variant, see /// ModelDesignExtensions.GetMethodArgumentTypeAsCode. /// private static bool HasTypedArgumentValueRank(Parameter parameter) { return parameter.ValueRank is ValueRank.Scalar or ValueRank.Array or ValueRank.OneOrMoreDimensions; } private static string GetMethodArgumentIdentifier( Parameter parameter, bool upperCamelCase = false) { return parameter.GetGeneratedCodeIdentifier( upperCamelCase, s_methodArgumentCodeNameScope); } private void AssignMethodArgumentCodeNames( MethodDesign method, Parameter[] inputArguments, Parameter[] outputArguments) { string declaredClassName = method.GetNodeStateClassName( m_context.ModelDesign.TargetNamespace.Value, [], applyStandardFallback: false); ModelDesignExtensions.AssignMethodArgumentCodeNames( inputArguments, outputArguments, s_methodArgumentCodeNameScope, declaredClassName + "Result"); } private sealed record MethodArgumentsToGenerate( string PropertyName, string BrowseName, Parameter[] Arguments); private sealed record ImportFactoryRegistration( string NodeClass, string Discriminator, string DiscriminatorId, string CreateExpression); private const string kNamespaceTableContextVariable = "context.NamespaceUris"; private static readonly XmlQualifiedName s_inputArgumentsBrowseName = new( "InputArguments", Namespaces.OpcUa); private static readonly XmlQualifiedName s_outputArgumentsBrowseName = new( "OutputArguments", Namespaces.OpcUa); private static readonly MethodArgumentCodeNameScope s_methodArgumentCodeNameScope = new( "_", "await", "ct", "cancellationToken", "context", "_context", "inputArguments", "_inputArguments", "method", "_method", "objectId", "_objectId", "onCall", "onCallAsync", "outputArguments", "_outputArguments", "_result", "results", "serviceResult", "ServiceResult"); private static readonly string[] s_builtInPropertyNames = [ "Description", "Save", "Handle", "Specification", "Update", "Delete", // Children whose generated accessor property shadows the // identically-named global::Opc.Ua.NodeState.Extensions property, // which carries the NodeSet2 elements rather than an // address-space member, and therefore must be declared "public new". "Extensions", // Method children whose generated accessor property shadows the // identically-named global::Opc.Ua.NodeState.Validate(ISystemContext) // instance method and therefore must be declared "public new". "Validate" ]; /// /// True when a child property named on the /// generated class of hides an inherited /// member of the runtime base class and must be declared /// public new. /// internal static bool RequiresNewModifier(NodeDesign parent, string name) { return s_builtInPropertyNames.Contains(name) || HidesRuntimeBaseMember(parent, name); } /// /// True when a child property named on the /// generated class of hides a member it /// inherits from the runtime base class (for example a child Property /// named DataType on a VariableType class hides /// BaseVariableState.DataType, one named Categories /// hides NodeState.Categories), so it must be declared /// public new. /// private static bool HidesRuntimeBaseMember(NodeDesign parent, string name) { return parent switch { ObjectTypeDesign or ObjectDesign => s_objectStateMemberNames.Value.Contains(name), VariableTypeDesign or VariableDesign => s_variableStateMemberNames.Value.Contains(name), MethodDesign => s_methodStateMemberNames.Value.Contains(name), _ => false }; } /// /// Returns the names of the members of and its /// base classes that a derived class sees (public and protected), i.e. /// the names a generated child property hides. Read from the runtime /// types themselves so a member added to them is never missed. /// private static HashSet GetInheritedMemberNames(Type type) { const BindingFlags kFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; var names = new HashSet(StringComparer.Ordinal); for (Type? current = type; current != null; current = current.BaseType) { foreach (MemberInfo member in current.GetMembers(kFlags)) { bool visible = member switch { ConstructorInfo => false, MethodInfo method => !method.IsSpecialName && IsVisibleToDerived(method), PropertyInfo property => property.GetIndexParameters().Length == 0 && (IsVisibleToDerived(property.GetGetMethod(true)) || IsVisibleToDerived(property.GetSetMethod(true))), EventInfo @event => IsVisibleToDerived(@event.GetAddMethod(true)), FieldInfo field => !field.IsSpecialName && (field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly), Type nested => nested.IsNestedPublic || nested.IsNestedFamily || nested.IsNestedFamORAssem, _ => false }; if (visible) { names.Add(member.Name); } } } return names; } private static bool IsVisibleToDerived(MethodBase? method) { return method != null && (method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly); } /// /// Members inherited by generated Object and ObjectType classes. /// private static readonly Lazy> s_objectStateMemberNames = new(() => GetInheritedMemberNames(typeof(BaseObjectState))); /// /// Members inherited by generated Variable and VariableType classes /// ("Value" is handled separately). /// private static readonly Lazy> s_variableStateMemberNames = new(() => GetInheritedMemberNames(typeof(BaseVariableState))); /// /// Members inherited by generated Method classes. /// private static readonly Lazy> s_methodStateMemberNames = new(() => GetInheritedMemberNames(typeof(MethodState))); private static readonly string[] s_builtInMethodNames = [ "Child" ]; private readonly Dictionary m_initializers = []; private readonly Dictionary m_nodes = []; private readonly Dictionary m_instances = []; private readonly Dictionary> m_singletonsByType = []; private readonly ILogger m_logger; private readonly IServiceMessageContext m_messageContext; private readonly IGeneratorContext m_context; } }