/* ======================================================================== * 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; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Xml; using Opc.Ua.SourceGeneration; using Opc.Ua.Types; namespace Opc.Ua.Schema.Model { /// /// Defines where to add nullable annotations /// internal enum NullableAnnotation { /// /// Types should be non nullable /// NonNullable, /// /// Types are nullable except for data types /// NullableExceptDataTypes, /// /// All types are nullable /// Nullable } /// /// Identifies one generated method-argument symbol scope and the names /// reserved by that scope's emitted scaffolding. /// internal sealed class MethodArgumentCodeNameScope { public MethodArgumentCodeNameScope(params string[] reservedNames) : this(null, reservedNames) { } public MethodArgumentCodeNameScope( Func>? getCollisionIdentifiers, params string[] reservedNames) { ReservedNames = reservedNames == null ? [] : [.. reservedNames]; m_getCollisionIdentifiers = getCollisionIdentifiers; } public IReadOnlyCollection ReservedNames { get; } public IEnumerable GetCollisionIdentifiers( string identifier, bool output) { return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier]; } private readonly Func>? m_getCollisionIdentifiers; } /// /// Dotnet code generation support. /// internal static class ModelDesignExtensions { /// /// Get basic data type from data type design. /// /// /// public static BasicDataType DetermineBasicDataType(this DataTypeDesign? dataType) { if (dataType == null) { return BasicDataType.BaseDataType; } if (dataType.IsOptionSet) { return BasicDataType.Enumeration; } // check if it is a built in data type. if (dataType.IsBasicDataType(out BasicDataType basicDataType)) { return basicDataType; } // recursively search hierarchy if not base data type yet BasicDataType basicType = DetermineBasicDataType( dataType.BaseTypeNode as DataTypeDesign); // data type is user defined if a sub-type of structure. if (basicType == BasicDataType.Structure) { return BasicDataType.UserDefined; } return basicType; } /// /// Get type node state class name /// public static string GetNodeStateClassName( this TypeDesign type, Namespace[] namespaces) { string className = type.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); return CoreUtils.Format("{0}State", className); } /// /// Returns the class name to use when creating an instance of the type. /// public static string GetNodeStateClassName( this InstanceDesign? instance, string targetNamespace, Namespace[] namespaces, bool asFactory = false, bool applyStandardFallback = true) { if (instance is MethodDesign method) { string className; XmlQualifiedName methodStateIdentity = MethodDesignArgumentResolver.ResolveMethodStateIdentity(method); if (methodStateIdentity != null) { className = methodStateIdentity.AsFullyQualifiedTypeSymbol(namespaces); if (className.EndsWith("MethodType", StringComparison.Ordinal)) { className = className[..^"MethodType".Length]; } else if (className.EndsWith("Type", StringComparison.Ordinal)) { className = className[..^"Type".Length]; } } else { className = method.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); } if (MethodDesignArgumentResolver.HasMethodArguments(method)) { // Only method types have a typed class. A reference to a // method that declares its arguments inline uses the base // MethodState; declaration names keep the typed name. if (applyStandardFallback && !MethodDesignArgumentResolver.HasTypedMethodState(method)) { return CoreUtils.Format( "{0}global::Opc.Ua.MethodState", asFactory ? "new " : string.Empty); } string typedClassName = CoreUtils.Format( "{0}{1}MethodState", asFactory ? "new " : string.Empty, className); // Degrade a reference to a standard global::Opc.Ua.*MethodState // to the base MethodState when a curated Opc.Ua.Core omits the // typed class and the current model pass does not declare it // (e.g. the FileDirectoryType methods on a Robotics Programs // object). Model-generation-scoped; see // StandardMethodStateFallback. if (applyStandardFallback && StandardMethodStateFallback.ShouldFallBackToBase(typedClassName)) { return CoreUtils.Format( "{0}global::Opc.Ua.MethodState", asFactory ? "new " : string.Empty); } return typedClassName; } return CoreUtils.Format( "{0}global::Opc.Ua.MethodState", asFactory ? "new " : string.Empty); } if (instance is not VariableDesign variable) { // No state class is emitted for an excluded ObjectType: use // the nearest emitted supertype's. TypeDesign? typeDefinition = instance!.TypeDefinitionNode; while (typeDefinition is ObjectTypeDesign { IsExcludedFromGeneration: true } && typeDefinition.BaseTypeNode != null) { typeDefinition = typeDefinition.BaseTypeNode; } return GetNodeStateNameSimple(typeDefinition!); } var variableType = instance.TypeDefinitionNode as VariableTypeDesign; // A VariableType materialised from a referenced assembly's // dependency payload used to arrive without its DataType // restriction, leaving DataTypeNode null and crashing here with a // bare NullReferenceException that named neither the variable nor // the type. The payload now carries the restriction; this check // keeps any remaining gap diagnosable instead of unreadable. if (variableType?.DataTypeNode == null) { throw new InvalidDataException( $"The data type of variable type " + $"'{instance.TypeDefinition}' could not be resolved, so the " + $"node state class of '{instance.SymbolicId?.Name ?? instance.BrowseName}' " + "cannot be determined. The type is most likely supplied by a " + "referenced assembly whose dependency payload predates the " + "VariableType data type entry - rebuild that assembly."); } // check if the variable type restricted the datatype to eliminate the // need for a template parameter. if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank)) { return GetNodeStateNameSimple(variableType); } // check if the variable instance did not restrict the datatype. if (!variable.DataTypeNode!.IsTemplateParameterRequired(variable.ValueRank)) { return GetNodeStateNameSimple(variableType); } // instance restricted the datatype but the type did not. string scalarName = GetDotNetTypeName( variable.DataTypeNode, targetNamespace, namespaces, nullable: NullableAnnotation.NonNullable, true); if (variable.ValueRank == ValueRank.Array) { if (asFactory) { return CoreUtils.Format( "{0}State>.With<{2}>", GetClassName(variableType, namespaces), scalarName, GetVariantBuilder(variable.DataTypeNode, scalarName)); } return CoreUtils.Format( "{0}State>", GetClassName(variableType, namespaces), scalarName); } if (variable.ValueRank == ValueRank.OneOrMoreDimensions && variable.DataTypeNode!.SupportsMatrixOf() && variable.DataTypeNode!.BasicDataType is not BasicDataType.BaseDataType and not BasicDataType.Number and not BasicDataType.UInteger and not BasicDataType.Integer) { if (asFactory) { return CoreUtils.Format( "{0}State>.With<{2}>", GetClassName(variableType, namespaces), scalarName, GetVariantBuilder(variable.DataTypeNode, scalarName)); } return CoreUtils.Format( "{0}State>", GetClassName(variableType, namespaces), scalarName); } if (IsIndeterminateType(variable)) { return GetNodeStateNameSimple(variableType); } // add hack for TwoStateDiscreteType which always has to be bool. if (variableType.SymbolicName == new XmlQualifiedName("TwoStateDiscreteType", Namespaces.OpcUa)) { return GetNodeStateNameSimple(variableType); } if (asFactory) { return CoreUtils.Format( "{0}State<{1}>.With<{2}>", GetClassName(variableType, namespaces), scalarName, GetVariantBuilder(variable.DataTypeNode, scalarName)); } return CoreUtils.Format( "{0}State<{1}>", GetClassName(variableType, namespaces), scalarName); string GetNodeStateNameSimple(TypeDesign variableType) { return CoreUtils.Format( "{0}{1}State", asFactory ? "new " : string.Empty, GetClassName(variableType, namespaces)); } } /// /// Get the variant builder to use to access or mutate a variant of type T /// public static string GetVariantBuilder( this DataTypeDesign? datatype, string typeName) { switch (datatype!.BasicDataType) { case BasicDataType.UserDefined: if (datatype.IsEnumeration) { return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName); } return CoreUtils.Format("global::Opc.Ua.StructureBuilder<{0}>", typeName); case BasicDataType.Enumeration: if (datatype.SymbolicId == new XmlQualifiedName("Enumeration", Namespaces.OpcUa)) { return "global::Opc.Ua.VariantBuilder"; } if (datatype.IsOptionSet) { return GetVariantBuilder((DataTypeDesign)datatype.BaseTypeNode!, typeName); } return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName); default: return "global::Opc.Ua.VariantBuilder"; } } /// /// Returns the class name to use when creating an instance of the type. /// /// public static string GetBaseClassName(this TypeDesign type, Namespace[] namespaces) { if (type?.BaseTypeNode?.SymbolicName == null) { throw new ArgumentNullException(nameof(type)); } if (type is not DataTypeDesign dataType || dataType.BaseTypeNode is not DataTypeDesign dtd) { return type.BaseTypeNode.GetClassName(namespaces); } if (dtd.BasicDataType == BasicDataType.Structure) { return "global::Opc.Ua.IEncodeable"; } return dtd.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); } /// /// Fully qualify a symbol /// /// /// public static string AsFullyQualifiedTypeSymbol( this XmlQualifiedName symbol, Namespace[] namespaces, string? alternativeName = null) { string? name = alternativeName ?? symbol?.Name; if (string.IsNullOrEmpty(name)) { throw new ArgumentException("Symbol name is empty"); } string? ns = namespaces.GetNamespacePrefix((symbol?.Namespace)!); if (string.IsNullOrEmpty(ns)) { return name; } return CoreUtils.Format("global::{0}.{1}", ns, name); } /// /// Returns the class name to use when creating an instance of the type. /// /// public static bool IsDerivedDataType(this TypeDesign type) { if (type is null) { throw new ArgumentNullException(nameof(type)); } if (type is not DataTypeDesign || type.BaseTypeNode is not DataTypeDesign dtd) { return true; } return dtd.BasicDataType != BasicDataType.Structure; } /// /// Returns a qualifier for the namespace to use in code. /// public static string? GetNamespacePrefix(this Namespace[] namespaces, string namespaceUri) { if (namespaces != null) { foreach (Namespace ns in namespaces) { if (ns.Value == namespaceUri) { return CoreUtils.Format("{0}", ns.Prefix); } } } return null; } /// /// Returns a constant string for the namespace uri. /// /// /// public static string? GetConstantSymbolForNamespace( this Namespace[] namespaces, string namespaceUri) { if (namespaces is null) { throw new ArgumentNullException(nameof(namespaces)); } Namespace? ns = namespaces.GetNamespace(namespaceUri); if (ns == null) { return null; } if (string.IsNullOrWhiteSpace(ns.Name)) { throw new ArgumentException("Found namespace has no name"); } if (string.IsNullOrWhiteSpace(ns.Prefix)) // TODO: should this work? { return CoreUtils.Format("Namespaces.{0}", ns.Name); } return CoreUtils.Format("global::{0}.Namespaces.{1}", ns.Prefix, ns.Name); } /// /// Returns the expression for a namespace URI in generated code: the /// model's namespace constant when the model declares the namespace, /// otherwise the URI as an escaped string literal. /// /// /// A namespace the model does not declare can still appear in /// generated code: an inherited child keeps the browse namespace of /// the model that declared it (DI below a Robotics subtype), and a /// reference can target a node of a namespace the NodeSet lists only /// in its NamespaceUris. /// /// /// public static string GetNamespaceUriAsCode( this Namespace[] namespaces, string namespaceUri) { return namespaces.GetConstantSymbolForNamespace(namespaceUri) ?? namespaceUri.AsStringLiteral(); } public static bool IsOverriddenWithSameClass( this InstanceDesign instance, string targetNamespace, Namespace[] namespaces) { if (instance is null) { throw new ArgumentNullException(nameof(instance)); } if (!instance.IsOverridden()) { return false; } string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces); string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces); // Only the exact same class counts. An override that refines the // generic argument (BaseDataVariableState overridden as // BaseDataVariableState) is a different class and has to be // redeclared, or the subtype keeps exposing the base's value type. // (A special case treating every BaseDataVariableState<...> pair as // equal used to compare against the unqualified class name, which // GetNodeStateClassName never returns - it had no effect and this // is the behaviour the generated code has always had.) return x == y; } private static bool IsIndeterminateType(this InstanceDesign instance) { if (instance is VariableDesign variable) { if (variable.ValueRank is not ValueRank.Scalar and not ValueRank.Array) { // Matrix-rank with a concrete data type that supports // `MatrixOf` is determinate - the generator emits a // typed `State>` for it. Everything else // (ScalarOrArray, ScalarOrOneDimension, Any, or matrix // over an abstract numeric base) stays on the Variant // fallback. return variable.ValueRank != ValueRank.OneOrMoreDimensions || variable.DataTypeNode == null || !variable.DataTypeNode.SupportsMatrixOf() || !(variable.DataTypeNode.BasicDataType is not BasicDataType.BaseDataType and not BasicDataType.Number and not BasicDataType.UInteger and not BasicDataType.Integer); } if (variable.DataType == new XmlQualifiedName(BrowseNames.Enumeration, Namespaces.OpcUa)) { return true; } } return false; } public static string? EnsureUniqueEnumName(this Parameter target) { if (target?.Parent is DataTypeDesign dt && dt.HasFields && dt.Fields != null) { HashSet names = []; foreach (Parameter field in dt.Fields) { int count = 1; string check = field.Name; if (names.Contains(check)) { check = field.Name + "_" + field.Identifier; } while (names.Contains(check)) { check = field.Name + "_v" + count++; } if (target.Identifier == field.Identifier) { return check; } names.Add(check); } } return target?.Name; } /// /// Returns the canonical C# identifier for a method argument. /// internal static string GetGeneratedCodeIdentifier( this Parameter field, bool upperCamelCase = false, MethodArgumentCodeNameScope? scope = null) { string? name = field?.Name; scope ??= s_defaultMethodArgumentCodeNameScope; if (field != null) { lock (s_generatedCodeNamesLock) { if (s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState? state) && state.TryGetName(scope, out string generatedName) && !string.IsNullOrEmpty(generatedName)) { name = generatedName; } } } return name!.ToCSharpIdentifier(upperCamelCase); } /// /// Assigns unique generated-code names against the requested scope while /// preserving the authored OPC UA names. /// internal static void AssignMethodArgumentCodeNames( this MethodDesign method, MethodArgumentCodeNameScope? scope = null, params string[] additionalReservedNames) { AssignMethodArgumentCodeNames( (method?.InputArguments)!, (method?.OutputArguments)!, scope, additionalReservedNames); } /// /// Assigns unique generated-code names for an effective method signature. /// internal static void AssignMethodArgumentCodeNames( Parameter[] inputArguments, Parameter[] outputArguments, MethodArgumentCodeNameScope? scope = null, params string[] additionalReservedNames) { scope ??= s_defaultMethodArgumentCodeNameScope; lock (s_generatedCodeNamesLock) { ClearGeneratedCodeNames(inputArguments, scope); ClearGeneratedCodeNames(outputArguments, scope); var usedNames = new HashSet(StringComparer.OrdinalIgnoreCase); AddReservedMethodArgumentNames(usedNames, scope.ReservedNames); AddReservedMethodArgumentNames(usedNames, additionalReservedNames); AssignUniqueGeneratedCodeNames( inputArguments, usedNames, output: false, scope); AssignUniqueGeneratedCodeNames( outputArguments, usedNames, output: true, scope); } } private static void AddReservedMethodArgumentNames( HashSet usedNames, IEnumerable reservedNames) { if (reservedNames == null) { return; } foreach (string reservedName in reservedNames) { if (!string.IsNullOrWhiteSpace(reservedName)) { usedNames.Add(GetIdentifierCollisionKey( reservedName.ToCSharpIdentifier())); } } } private static void AssignUniqueGeneratedCodeNames( Parameter[] arguments, HashSet usedNames, bool output, MethodArgumentCodeNameScope scope) { if (arguments == null) { return; } foreach (Parameter argument in arguments) { if (argument == null) { continue; } string identifier = argument.Name.ToCSharpIdentifier(); if (TryReserveGeneratedIdentifier( identifier, output, scope, usedNames)) { SetGeneratedCodeName(argument, identifier, scope); continue; } string stem = identifier.TrimStart('@'); int suffix = output ? 1 : 2; while (true) { string suffixText; if (output) { suffixText = suffix == 1 ? "Out" : "Out" + suffix.ToString(CultureInfo.InvariantCulture); } else { suffixText = suffix.ToString(CultureInfo.InvariantCulture); } string candidate = (stem + suffixText).ToCSharpIdentifier(); suffix++; if (TryReserveGeneratedIdentifier( candidate, output, scope, usedNames)) { SetGeneratedCodeName(argument, candidate, scope); break; } } } } private static bool TryReserveGeneratedIdentifier( string identifier, bool output, MethodArgumentCodeNameScope scope, HashSet usedNames) { string[] collisionKeys = [ .. scope.GetCollisionIdentifiers(identifier, output) .Where(name => !string.IsNullOrWhiteSpace(name)) .Select(GetIdentifierCollisionKey) .Distinct(StringComparer.OrdinalIgnoreCase) ]; if (collisionKeys.Any(usedNames.Contains)) { return false; } foreach (string collisionKey in collisionKeys) { usedNames.Add(collisionKey); } return true; } private static void ClearGeneratedCodeNames( Parameter[] arguments, MethodArgumentCodeNameScope scope) { if (arguments == null) { return; } foreach (Parameter argument in arguments) { if (argument != null && s_generatedCodeNames.TryGetValue( argument, out GeneratedCodeNameState? state)) { state.ClearName(scope); } } } private static string GetIdentifierCollisionKey(string identifier) { return identifier.TrimStart('@'); } private static void SetGeneratedCodeName( Parameter argument, string name, MethodArgumentCodeNameScope scope) { s_generatedCodeNames.GetOrCreateValue(argument).SetName(scope, name); } private sealed class GeneratedCodeNameState { public bool TryGetName( MethodArgumentCodeNameScope scope, out string name) { return m_names.TryGetValue(scope, out name!); } public void SetName( MethodArgumentCodeNameScope scope, string name) { m_names[scope] = name; } public void ClearName(MethodArgumentCodeNameScope scope) { m_names.Remove(scope); } private readonly Dictionary m_names = []; } private static readonly MethodArgumentCodeNameScope s_defaultMethodArgumentCodeNameScope = new(); private static readonly System.Threading.Lock s_generatedCodeNamesLock = new(); private static readonly ConditionalWeakTable s_generatedCodeNames = new(); /// /// Returns the field name of a child node. /// public static string GetChildFieldName(this Parameter field) { if (string.IsNullOrEmpty(field?.Name)) { return string.Empty; } // Derived from the property name, which is sanitized and already // disambiguated against its siblings. Mapping the authored name // directly is lossy in a way the property mapping is not: "Value Id" // and "ValueId" both collapse onto "m_valueId" (CS0102, even though // their properties differ), and characters that are legal in a // BrowseName but not in an identifier survive into the field name - // "$Value" became "m_$Value", which does not compile at all. return field.GetPropertyName().TrimStart('@').ToSafeSymbolName(true, "m_"); } /// /// Returns the C# identifier of the property a structure field is /// generated as. Structure field names are authored data, so they can be /// C# keywords or collide with the members every generated data type /// carries; neither would compile when used verbatim. The wire name is /// unaffected - it is emitted from . /// public static string GetPropertyName(this Parameter field) { return GetGeneratedName(field, s_propertyNames, BuildPropertyNames); } /// /// Looks a field's generated name up in the map computed once for its /// whole structure. Deciding one field at a time cannot work: three /// authored names that sanitize alike ("A-", "A?", "A!" all give "A_") /// each see only the others' pre-disambiguation names, so the second and /// third both pick the same replacement. /// private static string GetGeneratedName( Parameter field, ConditionalWeakTable cache, ConditionalWeakTable.CreateValueCallback build) { if (string.IsNullOrEmpty(field?.Name)) { return string.Empty; } if (field.Parent is not DataTypeDesign dataType || dataType.Fields == null) { return field.Name.ToCSharpIdentifierPreserveCase(); } int index = IndexOfField(dataType.Fields, field); if (index < 0) { return field.Name.ToCSharpIdentifierPreserveCase(); } return cache.GetValue(dataType, build)[index]; } private static int IndexOfField(Parameter[] fields, Parameter field) { for (int ii = 0; ii < fields.Length; ii++) { if (ReferenceEquals(fields[ii], field)) { return ii; } } return -1; } /// /// Assigns every field of a structure its generated property name in one /// pass, in declaration order, so each name is unique within the class. /// A name is taken by an earlier property, by the backing field of an /// earlier property, by a member the templates declare, by the enclosing /// type's own name, or by an inherited property. /// private static string[] BuildPropertyNames(DataTypeDesign dataType) { Parameter[] fields = dataType.Fields; var names = new string[fields.Length]; var taken = new HashSet(StringComparer.Ordinal); taken.UnionWith(s_reservedDataTypeMembers); taken.UnionWith(s_reservedDataTypeFields); if (!string.IsNullOrEmpty(dataType.SymbolicName?.Name)) { // A member may not carry the name of its enclosing type. taken.Add(dataType.SymbolicName.Name); } // Inherited properties, keyed by generated name. A derived field // that carries the *same* authored name as an inherited one is a // deliberate redeclaration (the generator emits it as an override), // so only a different wire name landing on the same identifier is a // collision - that would be CS0108 on the generated class. Dictionary inherited = CollectInheritedPropertyNames(dataType); for (int ii = 0; ii < fields.Length; ii++) { Parameter field = fields[ii]; if (string.IsNullOrEmpty(field?.Name)) { names[ii] = string.Empty; continue; } string candidate = SanitizeFieldName(field.Name); string suffixed = candidate + "Field"; while (IsTaken(taken, inherited, candidate, field.Name)) { candidate = suffixed; suffixed += "_"; } taken.Add(candidate); // Reserve the backing field the property is stored in, so two // properties cannot share one ("Value" and "value" both map to // "m_value", and a field literally named "m_value" would take // the name of another field's store). taken.Add(ToBackingFieldName(candidate)); // Re-apply the keyword escape: the replacement may no longer be // one, and the original may still be. names[ii] = candidate.ToCSharpIdentifierPreserveCase(); } return names; } private static bool IsTaken( HashSet taken, Dictionary inherited, string candidate, string authoredName) { return taken.Contains(candidate) || taken.Contains(ToBackingFieldName(candidate)) || (inherited.TryGetValue(candidate, out string? inheritedFrom) && !string.Equals(inheritedFrom, authoredName, StringComparison.Ordinal)); } /// /// The generated property names of every field inherited through the /// base type chain, mapped to the authored name each came from. /// private static Dictionary CollectInheritedPropertyNames( DataTypeDesign dataType) { var result = new Dictionary(StringComparer.Ordinal); var visited = new HashSet(); for (var baseType = dataType.BaseTypeNode as DataTypeDesign; baseType?.Fields != null; baseType = baseType.BaseTypeNode as DataTypeDesign) { if (baseType.SymbolicId != null && !visited.Add(baseType.SymbolicId)) { // A cyclic base chain is reported elsewhere; do not hang. break; } foreach (Parameter field in baseType.Fields) { if (string.IsNullOrEmpty(field?.Name)) { continue; } string name = field.GetPropertyName().TrimStart('@'); if (!result.ContainsKey(name)) { result[name] = field.Name; } } } return result; } private static string SanitizeFieldName(string name) { return name.ToCSharpIdentifierPreserveCase().TrimStart('@'); } private static string ToBackingFieldName(string propertyName) { return propertyName.ToSafeSymbolName(true, "m_"); } private static readonly ConditionalWeakTable s_propertyNames = new(); private static readonly ConditionalWeakTable s_fieldsEnumMemberNames = new(); /// /// Backing fields the data type templates declare themselves, which a /// generated property therefore cannot be named after. /// private static readonly HashSet s_reservedDataTypeFields = new(StringComparer.Ordinal) { "m_FieldNames", "m_pooledSentinel" }; /// /// Returns the C# identifier of the member a structure field is given in /// the generated {ClassName}Fields enumeration. Same reasoning as /// , but the only name already /// taken in that scope is None. /// public static string GetFieldsEnumMemberName(this Parameter field) { return GetGeneratedName( field, s_fieldsEnumMemberNames, BuildFieldsEnumMemberNames); } /// /// Assigns every field its member name in the generated /// {ClassName}Fields enumeration, in one pass for the same reason /// as the property names. The enumeration is its own scope, so the only /// name taken up front is None. /// private static string[] BuildFieldsEnumMemberNames(DataTypeDesign dataType) { Parameter[] fields = dataType.Fields; var names = new string[fields.Length]; var taken = new HashSet(StringComparer.Ordinal) { "None" }; for (int ii = 0; ii < fields.Length; ii++) { Parameter field = fields[ii]; if (string.IsNullOrEmpty(field?.Name)) { names[ii] = string.Empty; continue; } string candidate = SanitizeFieldName(field.Name); string suffixed = candidate + "Field"; while (taken.Contains(candidate)) { candidate = suffixed; suffixed += "_"; } taken.Add(candidate); names[ii] = candidate.ToCSharpIdentifierPreserveCase(); } return names; } private static readonly HashSet s_reservedDataTypeMembers = new(StringComparer.Ordinal) { "SwitchField", "EncodingMask", "EncodingMaskFieldNames", "Reuse", "ResetForReuse", "TypeId", "BinaryEncodingId", "XmlEncodingId", "JsonEncodingId", "Encode", "Decode", "IsEqual", "Equals", "GetHashCode", "GetType", "ToString", "Clone", "MemberwiseClone", "Initialize" }; /// /// Returns the field name of a child node. /// public static string GetChildFieldName(this InstanceDesign instance) { if (string.IsNullOrEmpty(instance?.SymbolicName?.Name)) { return string.Empty; } string name = instance.SymbolicName.Name.ToSafeSymbolName(true, "m_"); if (instance is MethodDesign) { return CoreUtils.Format("{0}Method", name); } return name; } /// /// Fixes class names for nodes. /// /// /// public static string GetClassName(this TypeDesign node, Namespace[] namespaces) { if (node is null) { throw new ArgumentNullException(nameof(node)); } if (node is DataTypeDesign) { return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); } if (node is ObjectTypeDesign objectType && objectType.ClassName == "ObjectSource") { return "global::Opc.Ua.BaseObject"; } if (node is VariableTypeDesign variableType && variableType.ClassName is "DataVariable" or "BaseVariable") { return "global::Opc.Ua.BaseDataVariable"; } if (!string.IsNullOrEmpty(node.ClassName)) { return node.SymbolicName.AsFullyQualifiedTypeSymbol( namespaces, node.ClassName); } return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); } /// /// Returns the NodeClass of a Node /// public static string GetNodeClassAsString(this NodeDesign node) { if (node is VariableDesign) { return "Variable"; } if (node is VariableTypeDesign) { return "VariableType"; } if (node is ObjectDesign) { return "Object"; } if (node is ObjectTypeDesign) { return "ObjectType"; } if (node is ReferenceTypeDesign) { return "ReferenceType"; } if (node is DataTypeDesign) { return "DataType"; } if (node is MethodDesign) { return "Method"; } if (node is ViewDesign) { return "View"; } return "Node"; } /// /// Maps the event notifier flag onto a string. /// /// public static string GetEventNotifierAsCode(this ObjectTypeDesign objectType) { if (objectType is null) { throw new ArgumentNullException(nameof(objectType)); } if (objectType.SupportsEvents) { return "global::Opc.Ua.EventNotifiers.SubscribeToEvents"; } return "global::Opc.Ua.EventNotifiers.None"; } /// /// Maps the value rank enumeration onto a string. /// public static string GetValueRankAsCode(this ValueRank valueRank, string arrayDimensions) { switch (valueRank) { case ValueRank.Array: return "global::Opc.Ua.ValueRanks.OneDimension"; case ValueRank.Scalar: return "global::Opc.Ua.ValueRanks.Scalar"; case ValueRank.Any: case ValueRank.ScalarOrArray: return "global::Opc.Ua.ValueRanks.Any"; case ValueRank.ScalarOrOneDimension: return "global::Opc.Ua.ValueRanks.ScalarOrOneDimension"; case ValueRank.OneOrMoreDimensions: if (string.IsNullOrWhiteSpace(arrayDimensions)) { return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions"; } // The number of entries in ArrayDimensions is the number of // dimensions, which equals the ValueRank for a fixed-size // multi-dimensional array (e.g. "3,3" is a two-dimensional // array with ValueRank 2). string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries); int dims = dimensions.Length; if (dims == 0) { return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions"; } if (dims == 1) { return "global::Opc.Ua.ValueRanks.OneDimension"; } if (dims == 2) { return "global::Opc.Ua.ValueRanks.TwoDimensions"; } return CoreUtils.Format("{0}", dims); } return "global::Opc.Ua.ValueRanks.Any"; } /// /// The bit an OptionSet field mask (the design Identifier) selects. /// An OptionSet field names exactly one bit (OPC 10000-3 8.40, 8.52); /// false for a zero mask (the "no bits set" value) or a mask of /// several bits. /// public static bool TryGetOptionSetBit(decimal mask, out int bit) { bit = 0; if (mask <= 0 || decimal.Truncate(mask) != mask) { return false; } while (decimal.Remainder(mask, 2) == 0) { mask /= 2; bit++; } return mask == 1; } /// /// The bit an OptionSet field names: its explicit /// (any position, a subtype of /// the OptionSet structure has no upper bit, OPC 10000-3 8.40) or /// else the bit of its Identifier mask. False for the zero ("no bits /// set") value and a mask of several bits. /// public static bool TryGetOptionSetBit(this Parameter field, out int bit) { if (field?.OptionSetBit is int explicitBit && explicitBit >= 0) { bit = explicitBit; return true; } bit = 0; return field != null && TryGetOptionSetBit(field.Identifier, out bit); } /// /// The decimal mask of an OptionSet bit, or 0 when the bit is beyond /// what a decimal represents exactly (bit 96 and above); such a bit /// is carried by . Computed in /// decimal since a signed shift turns bit 63 into a negative mask. /// public static decimal GetOptionSetMask(int bit) { if (bit is < 0 or >= kMaxDecimalMaskBits) { return 0; } decimal mask = 1m; for (int ii = 0; ii < bit; ii++) { mask *= 2; } return mask; } /// /// The hexadecimal BitMask of an OptionSet bit (at least 8 digits), /// for any bit position. /// public static string GetOptionSetBitMask(int bit) { string mask = "1248"[bit % 4] + new string('0', bit / 4); return mask.Length < 8 ? mask.PadLeft(8, '0') : mask; } /// /// The bit a hexadecimal BitMask of any length selects; false when /// the mask is not a single bit. /// public static bool TryGetOptionSetBitFromBitMask(string bitMask, out int bit) { bit = 0; string? mask = bitMask?.Trim().TrimStart('0'); if (string.IsNullOrEmpty(mask)) { return false; } int zeros = 0; for (int ii = mask.Length - 1; ii > 0; ii--) { if (mask[ii] != '0') { return false; } zeros++; } int lead = mask[0] switch { '1' => 0, '2' => 1, '4' => 2, '8' => 3, _ => -1 }; if (lead < 0) { return false; } bit = (zeros * 4) + lead; return true; } /// /// The bits a decimal OptionSet mask can represent exactly. /// private const int kMaxDecimalMaskBits = 96; /// /// The ArrayDimensions a StructureField publishes for the field. A /// StructureField ValueRank is -1 or >= 1, never 0 (OPC 10000-3 /// 8.51), and a multi-dimensional field has at least two dimensions /// (OPC 10000-6 5.2.5): a /// field without ArrayDimensions is a two dimensional matrix of /// unknown lengths. (The validator already normalizes design fields /// that way; this also covers fields it did not see.) /// public static string GetStructureFieldArrayDimensions(this Parameter field) { if (field.ValueRank == ValueRank.OneOrMoreDimensions && string.IsNullOrWhiteSpace(field.ArrayDimensions)) { return "0,0"; } return field.ArrayDimensions; } /// /// Maps the MinimumSamplingInterval onto a constant. /// public static string GetMinimumSamplingIntervalAsCode(this VariableTypeDesign variableType) { return variableType.MinimumSamplingInterval switch { -1 => "global::Opc.Ua.MinimumSamplingIntervals.Indeterminate", 0 => "global::Opc.Ua.MinimumSamplingIntervals.Continuous", _ => variableType.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture) }; } /// /// Whether this is a reference type and implicitly nullable /// public static bool IsDotNetReferenceType( this DataTypeDesign dataType, ValueRank valueRank) { return !IsDotNetValueType(dataType, valueRank); } /// /// Whether the data type supports equality comparison in .NET. /// public static bool IsDotNetEqualityComparable( this DataTypeDesign dataType, ValueRank valueRank) { if (IsDotNetValueType(dataType, valueRank)) { return true; } if (valueRank == ValueRank.Scalar) { switch (dataType.BasicDataType) { case BasicDataType.String: // Use equality for strings return true; default: return false; } } return false; } /// /// If the data type is a value type in .NET. /// /// public static bool IsDotNetValueType( this DataTypeDesign dataType, ValueRank valueRank) { if (dataType is null) { throw new ArgumentNullException(nameof(dataType)); } if (valueRank != ValueRank.Scalar) { return true; // ArrayOf/MatrixOf/Variant; } switch (dataType.BasicDataType) { case BasicDataType.Boolean: case BasicDataType.SByte: case BasicDataType.Byte: case BasicDataType.Int16: case BasicDataType.UInt16: case BasicDataType.Int32: case BasicDataType.UInt32: case BasicDataType.Int64: case BasicDataType.UInt64: case BasicDataType.Float: case BasicDataType.Double: case BasicDataType.DateTime: case BasicDataType.Guid: case BasicDataType.NodeId: case BasicDataType.ExpandedNodeId: case BasicDataType.QualifiedName: case BasicDataType.LocalizedText: case BasicDataType.StatusCode: case BasicDataType.Structure: // Extension object case BasicDataType.Number: case BasicDataType.Integer: case BasicDataType.UInteger: case BasicDataType.BaseDataType: // Variant case BasicDataType.Enumeration when !dataType.IsOptionSet: case BasicDataType.UserDefined when dataType.IsEnumeration: case BasicDataType.ByteString: case BasicDataType.XmlElement: return true; default: return false; } } /// /// If the type needs to be cloned /// /// public static bool NeedsCloning(this DataTypeDesign dataType) { if (dataType is null) { throw new ArgumentNullException(nameof(dataType)); } switch (dataType.BasicDataType) { case BasicDataType.DataValue: case BasicDataType.Structure: // Extension object case BasicDataType.BaseDataType: // Variant case BasicDataType.UserDefined when !dataType.IsEnumeration: case BasicDataType.Enumeration when dataType.IsOptionSet: return true; default: return false; } } /// /// Whether the data type can be expressed as a typed /// in generated code, i.e. whether /// Variant.From(MatrixOf<T>) / /// Variant.GetXxxMatrix() round-trip APIs exist. /// All BasicDataType values except /// (which is not a valid structure field per OPC UA Part 5) are /// supported. /// /// public static bool SupportsMatrixOf(this DataTypeDesign dataType) { if (dataType is null) { throw new ArgumentNullException(nameof(dataType)); } return dataType.BasicDataType != BasicDataType.DiagnosticInfo; } /// /// True if a field or a nested field of the structure (including the /// fields inherited from its base types) is a multi-dimensional array, /// i.e. encoded with the inline matrix representation. Such a /// structure "is not compatible with the deprecated DataTypeDictionary /// mechanism, and shall not be included in a DataTypeDictionary" /// (OPC 10000-6 5.2.5): the dictionary has no way to describe values /// whose count is the product of the dimensions. A field that allows /// subtypes is an ExtensionObject in the dictionary and does not nest. /// public static bool HasInlineMatrixField(this DataTypeDesign dataType) { return HasInlineMatrixField(dataType, []); static bool HasInlineMatrixField(DataTypeDesign dataType, HashSet visited) { if (dataType == null || !visited.Add(dataType)) { return false; } for (DataTypeDesign? type = dataType; type != null; type = type.BaseTypeNode as DataTypeDesign) { if (type.Fields == null || type.IsOptionSet || type.IsEnumeration) { continue; } foreach (Parameter field in type.Fields) { if (field == null) { continue; } if (field.ValueRank == ValueRank.OneOrMoreDimensions) { return true; } if (field.ValueRank is ValueRank.Scalar or ValueRank.Array && !field.AllowSubTypes && field.DataTypeNode is DataTypeDesign fieldType && fieldType.BasicDataType == BasicDataType.UserDefined && !fieldType.IsEnumeration && HasInlineMatrixField(fieldType, visited)) { return true; } } } return false; } } /// /// Emits the default value as a C# expression. /// /// The data type of the value. /// The value rank. /// The authored value, decoded here when /// is null. /// The already decoded value. /// Wrap the expression in a Variant. /// The target namespace. /// The design's namespaces. /// The context used to decode /// . /// Fallback for values that have no /// expression form. /// The table the namespace /// indexes in refer to (the NodeSet's); /// null for a value authored in the design. /// A NamespaceTable expression in /// scope where the code is emitted (e.g. context.NamespaceUris), /// or null when there is none. public static string GetValueAsCode( this DataTypeDesign dataType, ValueRank valueRank, System.Xml.XmlElement? defaultValue, object? decodedValue, bool valueAsVariant, string targetNamespace, Namespace[] namespaces, IServiceMessageContext context, Func? onUnknownElement = null, NamespaceTable? decodedValueNamespaceUris = null, string? namespaceTableVariable = null) { TypeInfo decodedValueType = default; string defaultString = valueAsVariant ? "global::Opc.Ua.Variant.Null" : "default"; if (decodedValue == null && defaultValue != null) { // // true // // hand-written designs may write empty strings as layout whitespace. using var decoder = new XmlDecoder((XmlElement)defaultValue, context) { TreatWhitespaceOnlyStringsAsEmpty = true }; Variant variant = decoder.ReadVariantValue(null, default); decodedValueType = variant.TypeInfo; decodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy); // Authored in the design: its indexes are the design's own. decodedValueNamespaceUris = null; } if (valueRank == ValueRank.Array) { return dataType.GetArrayValueAsCode( defaultValue, decodedValue, valueAsVariant, targetNamespace, namespaces, onUnknownElement) ?? defaultString; } if (valueRank == ValueRank.Scalar && dataType.BasicDataType != BasicDataType.BaseDataType) { return dataType.GetScalarValueAsCode( defaultValue, decodedValue, decodedValueType, valueAsVariant, targetNamespace, namespaces, onUnknownElement, decodedValueNamespaceUris, namespaceTableVariable) ?? defaultString; } return onUnknownElement?.Invoke() ?? defaultString; } /// /// Get default for a scalar type /// internal static string? GetScalarValueAsCode( this DataTypeDesign dataType, System.Xml.XmlElement? defaultValue, object? decodedValue, TypeInfo decodedValueType, bool valueAsVariant, string targetNamespace, Namespace[] namespaces, Func? onUnknownElement = null, NamespaceTable? decodedValueNamespaceUris = null, string? namespaceTableVariable = null) { // Note that we return a typed value since we initialize a object/variant switch (dataType.BasicDataType) { case BasicDataType.Boolean: if (decodedValue is not bool boolValue) { boolValue = false; } return MakeReturnType(boolValue ? "true" : "false"); case BasicDataType.SByte: if (decodedValue is not sbyte sbyteValue) { sbyteValue = 0; } return MakeReturnType(CoreUtils.Format("(sbyte){0}", sbyteValue)); case BasicDataType.Byte: if (decodedValue is not byte byteValue) { byteValue = 0; } return MakeReturnType(CoreUtils.Format("(byte){0}", byteValue)); case BasicDataType.Int16: if (decodedValue is not short shortValue) { shortValue = 0; } return MakeReturnType(CoreUtils.Format("(short){0}", shortValue)); case BasicDataType.UInt16: if (decodedValue is not ushort ushortValue) { ushortValue = 0; } return MakeReturnType(CoreUtils.Format("(ushort){0}", ushortValue)); case BasicDataType.Int32: if (decodedValue is not int intValue) { intValue = 0; } return MakeReturnType(CoreUtils.Format("(int){0}", intValue)); case BasicDataType.UInt32: if (decodedValue is not uint uintValue) { uintValue = 0; } return MakeReturnType(CoreUtils.Format("(uint){0}", uintValue)); case BasicDataType.Integer: case BasicDataType.Int64: if (decodedValue is not long longValue) { longValue = 0; } return MakeReturnType(CoreUtils.Format("(long){0}", longValue)); case BasicDataType.UInteger: case BasicDataType.UInt64: if (decodedValue is not ulong ulongValue) { ulongValue = 0; } return MakeReturnType(CoreUtils.Format("(ulong){0}", ulongValue)); case BasicDataType.Float: if (decodedValue is not float floatValue) { floatValue = 0; } // NaN and the infinities have no literal form in C# - they // format as "NaN" / "Infinity", which would not compile. if (float.IsNaN(floatValue)) { return MakeReturnType("float.NaN"); } if (float.IsPositiveInfinity(floatValue)) { return MakeReturnType("float.PositiveInfinity"); } if (float.IsNegativeInfinity(floatValue)) { return MakeReturnType("float.NegativeInfinity"); } return MakeReturnType("(float)" + FormatFloatLiteral(floatValue)); case BasicDataType.Number: case BasicDataType.Double: if (decodedValue is not double doubleValue) { doubleValue = 0; } if (double.IsNaN(doubleValue)) { return MakeReturnType("double.NaN"); } if (double.IsPositiveInfinity(doubleValue)) { return MakeReturnType("double.PositiveInfinity"); } if (double.IsNegativeInfinity(doubleValue)) { return MakeReturnType("double.NegativeInfinity"); } return MakeReturnType("(double)" + FormatDoubleLiteral(doubleValue)); case BasicDataType.String: if (decodedValue is not string stringValue) { return MakeReturnType("default(string)"); } return MakeReturnType(stringValue.AsStringLiteral()); case BasicDataType.DateTime: if (decodedValue is DateTimeUtc dtutc) { if (dtutc.IsNull) { return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue"); } decodedValue = (DateTime)dtutc; } if (decodedValue is not DateTime dateTimeValue || dateTimeValue == DateTime.MinValue) { return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue"); } return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.DateTimeUtc.From(\"{0:yyyy-MM-dd HH:mm:ss}\")", dateTimeValue)); case BasicDataType.Guid: if (decodedValue is Uuid uuid) { decodedValue = uuid.Guid; } if (decodedValue is not Guid guidValue || guidValue == Guid.Empty) { return MakeReturnType("global::Opc.Ua.Uuid.Empty"); } return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.Uuid.Parse(\"{0}\")", guidValue)); case BasicDataType.ByteString: if (decodedValue is ByteString byteStringValue) { decodedValue = byteStringValue.ToArray(); } if (decodedValue is not byte[] byteArray) { return MakeReturnType("global::Opc.Ua.ByteString.Empty"); } return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.ByteString.FromHexString(\"{0}\")", CoreUtils.ToHexString(byteArray))); case BasicDataType.NodeId: if (decodedValue is not NodeId nodeId || nodeId.IsNull) { return MakeReturnType("global::Opc.Ua.NodeId.Null"); } // The namespace index is one of the table the value was // decoded with, not of the server the code runs in: resolve // it to its URI and look that up at run time. Without a // namespace table in scope (a structure field initializer) // or for an index nothing declares, the literal id is all // that can be emitted. string? nodeIdNamespaceUri = nodeId.NamespaceIndex == 0 ? null : ResolveDecodedNamespaceUri( nodeId.NamespaceIndex, decodedValueNamespaceUris, namespaces); if (nodeIdNamespaceUri == null || namespaceTableVariable == null) { return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.NodeId.Parse({0})", nodeId.ToString().AsStringLiteral())); } return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.NodeId.Parse({0}).WithNamespaceIndex({1}.GetIndexOrAppend({2}))", nodeId.WithNamespaceIndex(0).ToString().AsStringLiteral(), namespaceTableVariable, nodeIdNamespaceUri.AsStringLiteral())); case BasicDataType.ExpandedNodeId: if (decodedValue is not ExpandedNodeId expandedNodeId || expandedNodeId.IsNull) { return MakeReturnType("global::Opc.Ua.ExpandedNodeId.Null"); } // An ExpandedNodeId can carry the namespace URI itself, so an // index-only value is emitted in its absolute "nsu=" form - // the index would otherwise be read against the server's // namespace table. if (string.IsNullOrEmpty(expandedNodeId.NamespaceUri) && expandedNodeId.NamespaceIndex != 0) { string? expandedNamespaceUri = ResolveDecodedNamespaceUri( expandedNodeId.NamespaceIndex, decodedValueNamespaceUris, namespaces); if (expandedNamespaceUri != null) { expandedNodeId = new ExpandedNodeId( expandedNodeId.InnerNodeId, expandedNamespaceUri, expandedNodeId.ServerIndex); } } // A server index is one of the NodeSet's ServerUris table // (OPC 10000-6 F.2, F.14), not of the server the code runs // in, and the ExpandedNodeId text has no server URI form: // map the URI through the context's ServerUris at run time // (the literal index stays the fallback without a table). string? serverUri = expandedNodeId.ServerIndex == 0 || decodedValueNamespaceUris is not DecodedValueNamespaceTable decodedTable ? null : decodedTable.ServerUris?.GetString(expandedNodeId.ServerIndex); string? serverTableVariable = GetServerTableVariable(namespaceTableVariable); if (serverUri != null && serverTableVariable != null) { return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.ExpandedNodeId.Parse({0}).WithServerIndex({1}?.GetIndexOrAppend({2}) ?? {3}u)", expandedNodeId.WithServerIndex(0).ToString().AsStringLiteral(), serverTableVariable, serverUri.AsStringLiteral(), expandedNodeId.ServerIndex)); } return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.ExpandedNodeId.Parse({0})", expandedNodeId.ToString().AsStringLiteral())); case BasicDataType.QualifiedName: if (decodedValue is not QualifiedName qualifiedName || qualifiedName.IsNull) { return MakeReturnType("global::Opc.Ua.QualifiedName.Null"); } string? qualifiedNameNamespaceUri = qualifiedName.NamespaceIndex == 0 ? null : ResolveDecodedNamespaceUri( qualifiedName.NamespaceIndex, decodedValueNamespaceUris, namespaces); if (qualifiedNameNamespaceUri == null || namespaceTableVariable == null) { return MakeReturnType(CoreUtils.Format( "global::Opc.Ua.QualifiedName.Parse({0})", qualifiedName.ToString().AsStringLiteral())); } return MakeReturnType(CoreUtils.Format( "new global::Opc.Ua.QualifiedName({0}, {1}.GetIndexOrAppend({2}))", qualifiedName.Name.AsStringLiteral(), namespaceTableVariable, qualifiedNameNamespaceUri.AsStringLiteral())); case BasicDataType.LocalizedText: if (decodedValue is not Ua.LocalizedText localizedText || localizedText.IsNullOrEmpty) { return MakeReturnType("global::Opc.Ua.LocalizedText.Null"); } return MakeReturnType(CoreUtils.Format( "new global::Opc.Ua.LocalizedText({0}, {1})", localizedText.Locale.AsStringLiteral(), localizedText.Text.AsStringLiteral())); case BasicDataType.StatusCode: if (decodedValue is not StatusCode statusCode || statusCode.Code == 0) { return MakeReturnType("default(global::Opc.Ua.StatusCode)"); } // The numeric code, not StatusCode.ToString(): that yields // "BadNodeIdUnknown [0x80340000]", which is no C# expression. return MakeReturnType(CoreUtils.Format( "new global::Opc.Ua.StatusCode(0x{0:X8}u)", statusCode.Code)); case BasicDataType.Enumeration: if (dataType.BaseTypeNode?.SymbolicId == new XmlQualifiedName("OptionSet", Namespaces.OpcUa)) { // OptionSet subtypes inherit from Opc.Ua.OptionSet (a // class implementing IEncodeable), so Variant.From // (where T : struct, Enum) does not apply. Route them // through Variant.FromStructure (where T : IEncodeable). return MakeReturnType( CoreUtils.Format("new {0}()", dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)), "Structure"); } if (!dataType.IsOptionSet && dataType.Fields?.Length > 0) { if (decodedValue is int enumValue) { return MakeReturnType(CoreUtils.Format("({0})({1})", dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces), enumValue)); } if (decodedValue is null) { // Enum type declared - use first field as default return MakeReturnType(CoreUtils.Format("{0}.{1}", dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces), dataType.Fields[0].Name)); } // TODO: change to initialize enum with the value return onUnknownElement?.Invoke() ?? MakeReturnType(CoreUtils.Format("default({0})", dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces))); } // Default to enum with value 0 which should be "none" for option set return onUnknownElement?.Invoke(); case BasicDataType.DataValue: return MakeReturnType("new global::Opc.Ua.DataValue()"); case BasicDataType.UserDefined: if (defaultValue == null) { return MakeReturnType(CoreUtils.Format( "new {0}()", GetDotNetTypeName( dataType, ValueRank.Scalar, targetNamespace, namespaces, nullable: NullableAnnotation.NonNullable)), "Structure"); } return onUnknownElement?.Invoke(); default: return onUnknownElement?.Invoke(); } string MakeReturnType(string value, string? prefix = null) { if (valueAsVariant) { return CoreUtils.Format( "global::Opc.Ua.Variant.From{0}({1})", prefix ?? string.Empty, value); } return value; } } /// /// The run-time server URI table that sits next to a namespace table /// expression: "context.NamespaceUris" pairs with "context.ServerUris". /// Null when the expression is not a member access on a context. /// private static string? GetServerTableVariable(string? namespaceTableVariable) { const string suffix = ".NamespaceUris"; if (namespaceTableVariable == null || !namespaceTableVariable.EndsWith(suffix, StringComparison.Ordinal)) { return null; } return namespaceTableVariable[..(namespaceTableVariable.Length - suffix.Length)] + ".ServerUris"; } /// /// Resolves a namespace index found inside a decoded default value to /// its namespace URI. A value read from a NodeSet carries the table of /// that NodeSet; a value authored in a ModelDesign numbers OPC UA 0 and /// then the design's namespaces in declaration order. Returns null when /// the index is not in the table. /// internal static string? ResolveDecodedNamespaceUri( ushort namespaceIndex, NamespaceTable? decodedValueNamespaceUris, Namespace[] namespaces) { if (namespaceIndex == 0) { return Namespaces.OpcUa; } NamespaceTable table = decodedValueNamespaceUris ?? CreateDesignNamespaceTable(namespaces); return table.GetString(namespaceIndex); } /// /// The table the namespace indexes of a value authored in a ModelDesign /// refer to: OPC UA 0 and then the design's namespaces in declaration /// order. /// internal static NamespaceTable CreateDesignNamespaceTable(Namespace[] namespaces) { var table = new NamespaceTable(); foreach (Namespace ns in namespaces ?? []) { if (!string.IsNullOrEmpty(ns?.Value)) { table.GetIndexOrAppend(ns.Value); } } return table; } /// /// Formats a double as the digits of a C# literal that evaluates to /// exactly the same value. The default "G" format depends on the /// runtime the generator runs in: 15 significant digits on .NET /// Framework (Visual Studio), the shortest round-trip form on .NET - /// so the generated code differed between the two, lost precision on /// .NET Framework and turned double.MaxValue into an out-of-range /// literal (CS0594). The shortest of G15..G17 that parses back to the /// value is the same on every runtime. /// internal static string FormatDoubleLiteral(double value) { if (value == 0) { // "(double)-0" would be integer zero cast to +0. return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0"; } for (int precision = 15; precision < 17; precision++) { string text = value.ToString( "G" + precision.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); if (double.TryParse( text, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) && parsed.Equals(value)) { return text; } } return value.ToString("G17", CultureInfo.InvariantCulture); } /// /// Formats a float as the digits of a literal that, cast to float, /// evaluates to exactly the same value. The generated expression is /// (float)digits, i.e. the digits are parsed as a double and then /// converted, so the round trip is checked the same way. /// See for why "G" is not used. /// internal static string FormatFloatLiteral(float value) { if (value == 0) { return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0"; } for (int precision = 6; precision < 9; precision++) { string text = value.ToString( "G" + precision.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); if (double.TryParse( text, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) && ((float)parsed).Equals(value)) { return text; } } return value.ToString("G9", CultureInfo.InvariantCulture); } /// /// Get default for a array type /// internal static string? GetArrayValueAsCode( this DataTypeDesign dataType, System.Xml.XmlElement? defaultValue, object? decodedValue, bool valueAsVariant, string targetNamespace, Namespace[] namespaces, Func? onUnknownElement = null) { if ((decodedValue is not null && (decodedValue is not IList l || l.Count != 0) && (decodedValue is not Array a || a.Length != 0)) || (decodedValue is null && defaultValue is not null)) { // TODO: change to initialize simple arrays with values return onUnknownElement?.Invoke(); } // Emit Array.Empty() for empty arrays switch (dataType.BasicDataType) { case BasicDataType.Boolean: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.SByte: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Byte: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Int16: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.UInt16: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Int32: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.UInt32: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Int64: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.UInt64: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Float: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Double: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.String: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Guid: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.DateTime: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.ByteString: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.XmlElement: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.NodeId: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.ExpandedNodeId: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.QualifiedName: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.LocalizedText: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.StatusCode: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.DataValue: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Structure: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Enumeration: case BasicDataType.UserDefined: return MakeReturnType( CoreUtils.Format( "global::Opc.Ua.ArrayOf.Empty<{0}>()", GetDotNetTypeName( dataType, ValueRank.Scalar, targetNamespace, namespaces, nullable: NullableAnnotation.NonNullable)), dataType.BasicDataType == BasicDataType.UserDefined ? "Structure" : null); case BasicDataType.DiagnosticInfo: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); case BasicDataType.Number: case BasicDataType.Integer: case BasicDataType.UInteger: case BasicDataType.BaseDataType: return MakeReturnType("global::Opc.Ua.ArrayOf.Empty()"); default: return null; } string MakeReturnType(string value, string? prefix = null) { if (valueAsVariant) { return CoreUtils.Format( "global::Opc.Ua.Variant.From{0}({1})", prefix ?? string.Empty, value); } return value; } } /// /// Get code string to load a variant from XML resource stream. /// /// public static string GetVariantValueFromXmlAsCode( this DataTypeDesign dataType, ValueRank valueRank, string targetNamespace, Namespace[] namespaces, string xmlStreamResource, string? contextVariable = null) { contextVariable ??= "context"; switch (dataType.BasicDataType) { case BasicDataType.BaseDataType: return CoreUtils.Format( "global::Opc.Ua.Variant.FromXml({0}AsStream, {1})", xmlStreamResource, contextVariable); case BasicDataType.UserDefined: case BasicDataType.Structure: case BasicDataType.Enumeration: string dataTypeString = dataType.GetDotNetTypeName( targetNamespace, namespaces, NullableAnnotation.NonNullable, false); return CoreUtils.Format( "global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}<{4}>()", xmlStreamResource, contextVariable, dataType.BasicDataType == BasicDataType.Enumeration ? "Enumeration" : "Structure", valueRank == ValueRank.Array ? "Array" : string.Empty, dataTypeString); default: return CoreUtils.Format( "global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}()", xmlStreamResource, contextVariable, dataType.BasicDataType.ToString(), valueRank == ValueRank.Array ? "Array" : string.Empty); } } /// /// Returns the data type for a method argument. /// public static string GetMethodArgumentTypeAsCode( this DataTypeDesign datatype, ValueRank valueRank, string targetNamespace, Namespace[] namespaces, bool isOptional) { string typeName = GetDotNetTypeName( datatype, targetNamespace, namespaces, // Make scalar optional nullable, but array/matrix elements non nullable // TODO: Revisit this and make input non nullable but output parameter T // nullable when we get rid of collections nullable: isOptional && valueRank == ValueRank.Scalar ? NullableAnnotation.Nullable : NullableAnnotation.NonNullable, // Use raw arguments false); if (typeName is "global::Opc.Ua.IEncodeable" or "global::Opc.Ua.IEncodeable?") { typeName = "global::Opc.Ua.ExtensionObject"; } if (typeName is "object" or "object?") { typeName = "global::Opc.Ua.Variant"; } switch (valueRank) { case ValueRank.Scalar: return typeName; case ValueRank.Array: return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName); case ValueRank.OneOrMoreDimensions: return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName); default: return "global::Opc.Ua.Variant"; } } /// /// Returns true if the type is nullable /// /// /// public static bool IsXmlNillable(this BasicDataType type) { switch (type) { case BasicDataType.Boolean: case BasicDataType.SByte: case BasicDataType.Byte: case BasicDataType.Int16: case BasicDataType.UInt16: case BasicDataType.Int32: case BasicDataType.UInt32: case BasicDataType.Int64: case BasicDataType.UInt64: case BasicDataType.Float: case BasicDataType.Double: case BasicDataType.StatusCode: case BasicDataType.Enumeration: return false; } return true; } /// /// Returns system type for a basic data type. /// /// public static string GetDotNetTypeName( this DataTypeDesign? datatype, string targetNamespace, Namespace[] namespaces, NullableAnnotation nullable = NullableAnnotation.NonNullable, bool useBuiltInWrapperTypes = false) { switch (datatype!.BasicDataType) { case BasicDataType.Boolean: return "bool"; case BasicDataType.SByte: return "sbyte"; case BasicDataType.Byte: return "byte"; case BasicDataType.Int16: return "short"; case BasicDataType.UInt16: return "ushort"; case BasicDataType.Int32: return "int"; case BasicDataType.UInt32: return "uint"; case BasicDataType.Int64: return "long"; case BasicDataType.UInt64: return "ulong"; case BasicDataType.Float: return "float"; case BasicDataType.Double: return "double"; case BasicDataType.String: return nullable == NullableAnnotation.NonNullable ? "string" : "string?"; case BasicDataType.DateTime: return "global::Opc.Ua.DateTimeUtc"; case BasicDataType.Guid: return "global::Opc.Ua.Uuid"; case BasicDataType.ByteString: return "global::Opc.Ua.ByteString"; case BasicDataType.XmlElement: return "global::Opc.Ua.XmlElement"; case BasicDataType.NodeId: return "global::Opc.Ua.NodeId"; case BasicDataType.ExpandedNodeId: return "global::Opc.Ua.ExpandedNodeId"; case BasicDataType.StatusCode: return "global::Opc.Ua.StatusCode"; case BasicDataType.DiagnosticInfo: return nullable == NullableAnnotation.NonNullable ? "global::Opc.Ua.DiagnosticInfo" : "global::Opc.Ua.DiagnosticInfo?"; case BasicDataType.QualifiedName: return "global::Opc.Ua.QualifiedName"; case BasicDataType.LocalizedText: return "global::Opc.Ua.LocalizedText"; case BasicDataType.DataValue: return "global::Opc.Ua.DataValue"; case BasicDataType.Structure: if (!useBuiltInWrapperTypes) { // return nullable == NullableAnnotation.NonNullable ? // "global::Opc.Ua.IEncodeable" : // "global::Opc.Ua.IEncodeable?"; } return "global::Opc.Ua.ExtensionObject"; case BasicDataType.Enumeration: if (datatype.SymbolicId == new XmlQualifiedName("Enumeration", Namespaces.OpcUa)) { return "int"; } if (datatype.IsOptionSet) { return GetDotNetTypeName( (DataTypeDesign)datatype.BaseTypeNode!, targetNamespace, namespaces, nullable, // Should it always be non nullable? useBuiltInWrapperTypes); } return datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); case BasicDataType.UserDefined: string typeName = datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces); if (datatype.IsEnumeration) { return typeName; } // All of these are always set to default type in properties when null // is passed therefore we want to maintain non nullability no matter return nullable != NullableAnnotation.Nullable ? typeName : typeName + "?"; } if (!useBuiltInWrapperTypes) { return nullable == NullableAnnotation.NonNullable ? "object" : "object?"; } return "global::Opc.Ua.Variant"; } /// /// Returns the type name for the data type with the provided value rank. /// public static string GetDotNetTypeName( this DataTypeDesign dataType, ValueRank valueRank, string targetNamespace, Namespace[] namespaces, NullableAnnotation nullable = NullableAnnotation.NonNullable, bool useMatrixTypeInsteadOfVariant = false) { string typeName = dataType.GetDotNetTypeName( targetNamespace, namespaces, // Make scalar optional nullable, but array/matrix elements non nullable // TODO: Revisit this when we get rid of generated collections valueRank == ValueRank.Scalar ? nullable : NullableAnnotation.NonNullable, true); if (typeName != null) { switch (valueRank) { case ValueRank.Scalar: return typeName; case ValueRank.Array: return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName); case ValueRank.OneOrMoreDimensions when useMatrixTypeInsteadOfVariant: return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName); } } return "global::Opc.Ua.Variant"; } /// /// Whether a template parameter is required. /// public static bool IsTemplateParameterRequired( this DataTypeDesign dataType, ValueRank valueRank) { // DiagnosticInfo is not a valid Variant value (OPC UA Part 6) and // therefore has no IVariantBuilder. A typed // State (scalar or ArrayOf/MatrixOf) cannot be // emitted, so fall back to the non-generic, Variant-backed state. // Mirrors the DiagnosticInfo exclusion in SupportsMatrixOf. if (dataType.BasicDataType == BasicDataType.DiagnosticInfo) { return false; } if (dataType.BasicDataType is not BasicDataType.BaseDataType and not BasicDataType.Number and not BasicDataType.UInteger and not BasicDataType.Integer && valueRank is not ValueRank.OneOrMoreDimensions and not ValueRank.ScalarOrOneDimension and not ValueRank.ScalarOrArray) { return true; } if (valueRank == ValueRank.Array) { return true; } // Matrix-rank with a concrete data type that supports // `MatrixOf` is fully determinate (the generator produces // `State>` rather than `State`/`State`), // so the template parameter is in fact required to specialize // the inherited State class. if (valueRank == ValueRank.OneOrMoreDimensions && dataType.SupportsMatrixOf() && dataType.BasicDataType is not BasicDataType.BaseDataType and not BasicDataType.Number and not BasicDataType.UInteger and not BasicDataType.Integer) { return true; } return false; } /// /// Is overridden instance. /// /// public static bool IsOverridden(this InstanceDesign instance) { if (instance is null) { throw new ArgumentNullException(nameof(instance)); } return instance.OveriddenNode != null && instance.ModellingRule != ModellingRule.None && instance.ModellingRule != ModellingRule.ExposesItsArray && instance.ModellingRule != ModellingRule.MandatoryPlaceholder && instance.ModellingRule != ModellingRule.OptionalPlaceholder; } /// /// Determines whether a placeholder redeclares one its base type already /// declares. /// /// /// A placeholder is not overridden in the sense /// means, because it names a slot rather than /// a child, and merging it with the base declaration would be wrong. It /// does however emit an Add method named after the slot, so a subtype /// that redeclares the slot hides its base method and has to say so. /// This method detects that redeclaration so the generated member can /// be marked appropriately. /// public static bool HidesBaseTypePlaceholder(this InstanceDesign instance) { if (instance is null) { throw new ArgumentNullException(nameof(instance)); } if (instance.ModellingRule != ModellingRule.MandatoryPlaceholder && instance.ModellingRule != ModellingRule.OptionalPlaceholder) { return false; } // OveriddenNode is not linked for a placeholder, so the base chain // is walked for a slot of the same name. if (instance.Parent is not TypeDesign declaringType) { return false; } for (TypeDesign? baseType = declaringType.BaseTypeNode; baseType != null; baseType = baseType.BaseTypeNode) { InstanceDesign[]? children = baseType.Children?.Items; if (children == null) { continue; } foreach (InstanceDesign child in children) { if (child.SymbolicName == instance.SymbolicName) { return true; } } } return false; } /// /// Returns the merged instance for an overriden node. /// public static InstanceDesign GetMergedInstance(this InstanceDesign instance) { for (NodeDesign? parent = instance.Parent; parent != null; parent = parent.Parent) { if (parent.Parent == null && parent.Hierarchy != null) { // The hierarchy is keyed by the path relative to the root, so // the root's own symbolic name has to come off the front. // Strip that exact prefix rather than everything up to the // first '_', which mangles the path whenever the root type's // name itself contains an underscore. string relativePath = instance.SymbolicId.Name; string rootPrefix = parent.SymbolicId?.Name + "_"; if (rootPrefix.Length > 1 && relativePath.StartsWith(rootPrefix, StringComparison.Ordinal)) { relativePath = relativePath[rootPrefix.Length..]; } else { int index = relativePath.IndexOf('_', StringComparison.Ordinal); if (index != -1) { relativePath = relativePath[(index + 1)..]; } } if (parent.Hierarchy.Nodes.TryGetValue(relativePath, out HierarchyNode? hierarchyNode) && hierarchyNode.Instance is InstanceDesign instanceDesign) { return instanceDesign; } break; } } return instance; } /// /// Returns a name qualified with a namespace prefix. /// /// public static string GetPrefixedName(this XmlQualifiedName qname, List namespaceUris) { if (qname.IsNull()) { return string.Empty; } if (qname.Namespace == Namespaces.OpcUaBuiltInTypes) { return CoreUtils.Format("ua:{0}", qname.Name); } if (namespaceUris is null) { throw new ArgumentNullException(nameof(namespaceUris)); } int index = namespaceUris.IndexOf(qname.Namespace); if (index > 0) { return CoreUtils.Format("s{0}:{1}", index - 1, qname.Name); } return qname.Name; } /// /// Returns the data type to use for the value of a variable or the argument of a method. /// public static string GetBinaryDataType( this DataTypeDesign? dataType, string targetNamespace, Namespace[] namespaces) { switch (dataType!.BasicDataType) { case BasicDataType.Boolean: return "opc:Boolean"; case BasicDataType.SByte: return "opc:SByte"; case BasicDataType.Byte: return "opc:Byte"; case BasicDataType.Int16: return "opc:Int16"; case BasicDataType.UInt16: return "opc:UInt16"; case BasicDataType.Int32: return "opc:Int32"; case BasicDataType.UInt32: return "opc:UInt32"; case BasicDataType.Int64: return "opc:Int64"; case BasicDataType.UInt64: return "opc:UInt64"; case BasicDataType.Float: return "opc:Float"; case BasicDataType.Double: return "opc:Double"; case BasicDataType.String: return "opc:CharArray"; case BasicDataType.DateTime: return "opc:DateTime"; case BasicDataType.Guid: return "opc:Guid"; case BasicDataType.ByteString: return "opc:ByteString"; case BasicDataType.XmlElement: return "ua:XmlElement"; case BasicDataType.NodeId: return "ua:NodeId"; case BasicDataType.ExpandedNodeId: return "ua:ExpandedNodeId"; case BasicDataType.StatusCode: return "ua:StatusCode"; case BasicDataType.DiagnosticInfo: return "ua:DiagnosticInfo"; case BasicDataType.QualifiedName: return "ua:QualifiedName"; case BasicDataType.LocalizedText: return "ua:LocalizedText"; case BasicDataType.DataValue: return "ua:DataValue"; case BasicDataType.Number: case BasicDataType.Integer: case BasicDataType.UInteger: case BasicDataType.BaseDataType: return "ua:Variant"; default: if (dataType.SymbolicName == new XmlQualifiedName("Structure", Namespaces.OpcUa)) { return CoreUtils.Format("ua:ExtensionObject"); } if (dataType.SymbolicName == new XmlQualifiedName("Enumeration", Namespaces.OpcUa)) { if (dataType.IsOptionSet) { return GetBinaryDataType( (DataTypeDesign)dataType.BaseTypeNode!, targetNamespace, namespaces); } return CoreUtils.Format("opc:Int32"); } string? prefix = "tns"; if (dataType.SymbolicName.Namespace != targetNamespace) { if (dataType.SymbolicName.Namespace == Namespaces.OpcUa) { prefix = "ua"; } else { prefix = GetXmlNamespacePrefix( namespaces, dataType.SymbolicName.Namespace); } } return CoreUtils.Format("{0}:{1}", prefix, dataType.SymbolicName.Name); } } /// /// Returns the data type to use for the value of a variable or the argument of a method. /// public static string GetXmlDataType( this DataTypeDesign? dataType, ValueRank valueRank, string targetNamespace, Namespace[] namespaces) { if (valueRank != ValueRank.Scalar) { switch (dataType!.BasicDataType) { case BasicDataType.Boolean: return "ua:ListOfBoolean"; case BasicDataType.SByte: return "ua:ListOfSByte"; case BasicDataType.Byte: return "ua:ListOfByte"; case BasicDataType.Int16: return "ua:ListOfInt16"; case BasicDataType.UInt16: return "ua:ListOfUInt16"; case BasicDataType.Int32: return "ua:ListOfInt32"; case BasicDataType.UInt32: return "ua:ListOfUInt32"; case BasicDataType.Int64: return "ua:ListOfInt64"; case BasicDataType.UInt64: return "ua:ListOfUInt64"; case BasicDataType.Float: return "ua:ListOfFloat"; case BasicDataType.Double: return "ua:ListOfDouble"; case BasicDataType.String: return "ua:ListOfString"; case BasicDataType.DateTime: return "ua:ListOfDateTime"; case BasicDataType.Guid: return "ua:ListOfGuid"; case BasicDataType.ByteString: return "ua:ListOfByteString"; case BasicDataType.XmlElement: return "ua:ListOfXmlElement"; case BasicDataType.NodeId: return "ua:ListOfNodeId"; case BasicDataType.ExpandedNodeId: return "ua:ListOfExpandedNodeId"; case BasicDataType.StatusCode: return "ua:ListOfStatusCode"; case BasicDataType.DiagnosticInfo: return "ua:ListOfDiagnosticInfo"; case BasicDataType.QualifiedName: return "ua:ListOfQualifiedName"; case BasicDataType.LocalizedText: return "ua:ListOfLocalizedText"; case BasicDataType.DataValue: return "ua:ListOfDataValue"; case BasicDataType.Number: case BasicDataType.Integer: case BasicDataType.UInteger: case BasicDataType.BaseDataType: return "ua:ListOfVariant"; default: if (dataType.SymbolicName == new XmlQualifiedName("Structure", Namespaces.OpcUa)) { return CoreUtils.Format("ua:ListOfExtensionObject"); } if (dataType.SymbolicName == new XmlQualifiedName("Enumeration", Namespaces.OpcUa)) { if (dataType.IsOptionSet) { return GetXmlDataType( (DataTypeDesign)dataType.BaseTypeNode!, valueRank, targetNamespace, namespaces); } return CoreUtils.Format("ua:ListOfInt32"); } string? prefix = "tns"; if (dataType.SymbolicName.Namespace != targetNamespace) { if (dataType.SymbolicName.Namespace == Namespaces.OpcUa) { if (dataType.SymbolicName.Name == "Enumeration") { if (dataType.IsOptionSet) { return GetXmlDataType( (DataTypeDesign)dataType.BaseTypeNode!, valueRank, targetNamespace, namespaces); } return CoreUtils.Format("ua:ListOfInt32"); } prefix = "ua"; } else { prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace); } } return CoreUtils.Format("{0}:ListOf{1}", prefix, dataType.SymbolicName.Name); } } switch (dataType!.BasicDataType) { case BasicDataType.Boolean: return "xs:boolean"; case BasicDataType.SByte: return "xs:byte"; case BasicDataType.Byte: return "xs:unsignedByte"; case BasicDataType.Int16: return "xs:short"; case BasicDataType.UInt16: return "xs:unsignedShort"; case BasicDataType.Int32: return "xs:int"; case BasicDataType.UInt32: return "xs:unsignedInt"; case BasicDataType.Int64: return "xs:long"; case BasicDataType.UInt64: return "xs:unsignedLong"; case BasicDataType.Float: return "xs:float"; case BasicDataType.Double: return "xs:double"; case BasicDataType.String: return "xs:string"; case BasicDataType.DateTime: return "xs:dateTime"; case BasicDataType.Guid: return "ua:Guid"; case BasicDataType.ByteString: return "xs:base64Binary"; case BasicDataType.XmlElement: return "ua:XmlElement"; case BasicDataType.NodeId: return "ua:NodeId"; case BasicDataType.ExpandedNodeId: return "ua:ExpandedNodeId"; case BasicDataType.StatusCode: return "ua:StatusCode"; case BasicDataType.DiagnosticInfo: return "ua:DiagnosticInfo"; case BasicDataType.QualifiedName: return "ua:QualifiedName"; case BasicDataType.LocalizedText: return "ua:LocalizedText"; case BasicDataType.DataValue: return "ua:DataValue"; case BasicDataType.Number: case BasicDataType.Integer: case BasicDataType.UInteger: case BasicDataType.BaseDataType: return "ua:Variant"; default: if (dataType.SymbolicName == new XmlQualifiedName("Structure", Namespaces.OpcUa)) { return CoreUtils.Format("ua:ExtensionObject"); } if (dataType.SymbolicName == new XmlQualifiedName("Enumeration", Namespaces.OpcUa)) { if (dataType.IsOptionSet) { return GetXmlDataType( (DataTypeDesign)dataType.BaseTypeNode!, valueRank, targetNamespace, namespaces); } return CoreUtils.Format("xs:int"); } string? prefix = null; if (dataType.SymbolicName.Namespace != targetNamespace) { if (dataType.SymbolicName.Namespace == Namespaces.OpcUa) { if (dataType.SymbolicName.Name == "Enumeration") { if (dataType.IsOptionSet) { return GetXmlDataType( (DataTypeDesign)dataType.BaseTypeNode!, valueRank, targetNamespace, namespaces); } return CoreUtils.Format("xs:int"); } prefix = "ua"; } else { prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace); } } return CoreUtils.Format("{0}:{1}", prefix ?? "tns", dataType.SymbolicName.Name); } } /// /// Returns a constant for the namespace uri. /// public static Namespace? GetNamespace(this Namespace[] namespaces, string namespaceUri) { if (namespaces != null) { foreach (Namespace ns in namespaces) { if (ns.Value == namespaceUri) { return ns; } } } return null; } /// /// Returns a constant for the namespace uri. /// public static string? GetConstantForXmlNamespace(this Namespace[] namespaces, string namespaceUri) { Namespace? ns = GetNamespace(namespaces, namespaceUri); if (ns != null) { // The "...Xsd" companion constant only exists when the XML // namespace differs from the namespace URI - ConstantsGenerator // emits it under exactly that condition. Referencing it whenever // XmlNamespace is merely non-empty names a constant that was // never declared (CS0117 in the generated sources). if (!string.IsNullOrEmpty(ns.XmlNamespace) && !string.Equals(ns.XmlNamespace, ns.Value, StringComparison.Ordinal)) { return CoreUtils.Format("{1}.Namespaces.{0}Xsd", ns.Name, ns.Prefix); } return CoreUtils.Format("{1}.Namespaces.{0}", ns.Name, ns.Prefix); } return null; } /// /// Returns the XML prefix for the specified namespace. /// public static string? GetXmlNamespacePrefix(this Namespace[] namespaces, string namespaceUri) { if (namespaceUri == null) { return null; } if (namespaces != null) { for (int ii = 0; ii < namespaces.Length; ii++) { if (namespaces[ii].Value == namespaceUri) { if (string.IsNullOrEmpty(namespaces[ii].XmlPrefix)) { return CoreUtils.Format("s{0}", ii); } return namespaces[ii].XmlPrefix; } } } return null; } public static bool IsMethodTypeDesign([NotNullWhen(true)] this NodeDesign? node) { if (node?.SymbolicId is null) { return false; } if (node is not MethodDesign) { return false; } string symbol = node.SymbolicId.Name; int index = symbol.IndexOf('_', StringComparison.Ordinal); if (index > 0) { symbol = symbol[..index]; } return symbol.EndsWith("MethodType", StringComparison.Ordinal); } public static bool IsPartOfOpcUaTypesLibrary(this DataTypeDesign dataType) { if (dataType != null && dataType.SymbolicId.Namespace == Namespaces.OpcUa) { switch (dataType.NumericId) { case DataTypes.AccessRestrictionType: case DataTypes.ReferenceDescription: case DataTypes.AttributeWriteMask: case DataTypes.Argument: case DataTypes.IdType: case DataTypes.RolePermissionType: case DataTypes.PermissionType: case DataTypes.ViewDescription: case DataTypes.BrowseDescription: case DataTypes.StructureDefinition: case DataTypes.StructureType: case DataTypes.StructureField: case DataTypes.InstanceNode: case DataTypes.ReferenceTypeNode: case DataTypes.ReferenceNode: case DataTypes.DataTypeDefinition: case DataTypes.EnumDefinition: case DataTypes.EnumField: case DataTypes.EnumValueType: case DataTypes.RelativePath: case DataTypes.BrowseDirection: case DataTypes.RelativePathElement: case DataTypes.NodeClass: case DataTypes.Node: case DataTypes.ViewNode: case DataTypes.ObjectNode: case DataTypes.MethodNode: case DataTypes.TypeNode: case DataTypes.ObjectTypeNode: case DataTypes.DataTypeNode: case DataTypes.VariableTypeNode: case DataTypes.VariableNode: return true; } } return false; } public static Export.ReleaseStatus ToNodeSetReleaseStatus( this ReleaseStatus releaseStatus) { switch (releaseStatus) { case ReleaseStatus.Deprecated: return Export.ReleaseStatus.Deprecated; case ReleaseStatus.RC: case ReleaseStatus.Draft: return Export.ReleaseStatus.Draft; default: return Export.ReleaseStatus.Released; } } /// /// Maps the access level enumeration onto a code string. /// public static string GetAccessLevelAsCode(this AccessLevel accessLevel) { return accessLevel switch { AccessLevel.Read => "global::Opc.Ua.AccessLevels.CurrentRead", AccessLevel.Write => "global::Opc.Ua.AccessLevels.CurrentWrite", AccessLevel.ReadWrite => "global::Opc.Ua.AccessLevels.CurrentReadOrWrite", AccessLevel.HistoryRead => "global::Opc.Ua.AccessLevels.HistoryRead", AccessLevel.HistoryWrite => "global::Opc.Ua.AccessLevels.HistoryWrite", AccessLevel.HistoryReadWrite => "global::Opc.Ua.AccessLevels.HistoryReadOrWrite", _ => "global::Opc.Ua.AccessLevels.None" }; } /// /// Maps the AccessLevel of a variable onto code. NodeSet2-sourced /// designs carry the verbatim bitmask, which is preferred because /// the ModelDesign enumeration cannot /// represent combinations such as CurrentRead | HistoryRead. /// public static string GetAccessLevelAsCode(this VariableDesign? variable) { uint? rawAccessLevel = variable?.RawAccessLevel; return rawAccessLevel != null ? GetAccessLevelBitsAsCode(rawAccessLevel.Value) : GetAccessLevelAsCode(variable?.AccessLevel ?? AccessLevel.None); } /// /// Maps the UserAccessLevel of a variable onto code. NodeSet2-sourced /// designs carry the verbatim UserAccessLevel bitmask when the /// attribute is explicitly present and it is preferred; otherwise the /// UserAccessLevel is derived from the AccessLevel, matching the /// runtime NodeSet2 importer in UANodeSetHelpers. /// public static string GetUserAccessLevelAsCode(this VariableDesign variable) { uint? rawUserAccessLevel = variable?.RawUserAccessLevel; return rawUserAccessLevel != null ? GetAccessLevelBitsAsCode(rawUserAccessLevel.Value) : GetAccessLevelAsCode(variable); } /// /// Maps a verbatim OPC UA AccessLevel bitmask onto code, emitting /// the named AccessLevels constants for every set bit. /// private static string GetAccessLevelBitsAsCode(uint accessLevel) { byte bits = (byte)accessLevel; if (bits == 0) { return "global::Opc.Ua.AccessLevels.None"; } var names = new List(); AppendBit(names, bits, 0x1, "CurrentRead"); AppendBit(names, bits, 0x2, "CurrentWrite"); AppendBit(names, bits, 0x4, "HistoryRead"); AppendBit(names, bits, 0x8, "HistoryWrite"); AppendBit(names, bits, 0x10, "SemanticChange"); AppendBit(names, bits, 0x20, "StatusWrite"); AppendBit(names, bits, 0x40, "TimestampWrite"); if (names.Count == 0) { return CoreUtils.Format("(byte)0x{0:X2}", bits); } if (names.Count == 1) { return names[0]; } return CoreUtils.Format("(byte)({0})", string.Join(" | ", names)); } private static void AppendBit(List names, byte bits, byte mask, string name) { if ((bits & mask) != 0) { names.Add("global::Opc.Ua.AccessLevels." + name); } } /// /// Maps the WriteAccess (WriteMask) of a node onto code. The verbatim /// OPC UA WriteMask bitmask is emitted so that attributes advertised as /// writable in the NodeSet (for example DisplayName and Description) /// stay writable at runtime instead of defaulting to None. /// public static string GetWriteMaskAsCode(this NodeDesign node) { uint writeMask = node?.WriteAccess ?? 0; if (writeMask == 0) { return "global::Opc.Ua.AttributeWriteMask.None"; } return CoreUtils.Format( "(global::Opc.Ua.AttributeWriteMask){0}", writeMask); } public static string GetLocalizedTextAsCode(this string localizedText) { return GetLocalizedTextAsCode(new LocalizedText { Value = localizedText }, noAutoGen: false); } public static string GetLocalizedTextAsCode(this LocalizedText? localizedText, bool noAutoGen = false) { if (localizedText?.Value != null && (!noAutoGen || !localizedText.IsAutogenerated)) { return CoreUtils.Format( "new global::Opc.Ua.LocalizedText({0}, string.Empty, {1})", localizedText.Key.AsStringLiteral(), localizedText.Value.Trim().AsStringLiteral()); } return "global::Opc.Ua.LocalizedText.Null"; } /// /// Maps the array dimensions onto code that creates the array. /// public static string? GetArrayDimensionsAsCode(this ValueRank valueRank, string arrayDimensions) { // Part 3 5.6.2: ArrayDimensions is null unless the ValueRank names an // array of a specific dimension. ModelDesign's ValueRank is the XSD // enum (Scalar = 0 ... Any = 5), not the numeric OPC UA rank, so a // "< 0" test never fired and a scalar declared with // ArrayDimensions="0" (as NodeSet exporters write it) got them. if (valueRank is ValueRank.Scalar or ValueRank.ScalarOrArray or ValueRank.ScalarOrOneDimension or ValueRank.Any) { return null; } if (string.IsNullOrEmpty(arrayDimensions)) { if (valueRank == ValueRank.Array) { return "new uint[] { 0 }"; } return null; } var tokens = arrayDimensions .Split([','], StringSplitOptions.RemoveEmptyEntries) .Select(t => t.Trim()) .Where(t => t.Length > 0) .ToList(); if (tokens.Count == 0) { return null; } return CoreUtils.Format( "new uint[] {{ {0} }}", string.Join(", ", tokens.Select(t => { if (uint.TryParse(t, out uint val)) { return val.ToString(CultureInfo.InvariantCulture); } return "0"; }))); } public static string? GetAccessRestrictionsAsCode( this AccessRestrictions restrictions, bool restrictionsSpecified) { if (!restrictionsSpecified) { return null; } return restrictions switch { AccessRestrictions.SigningRequired => "global::Opc.Ua.AccessRestrictionType.SigningRequired", AccessRestrictions.EncryptionRequired => "global::Opc.Ua.AccessRestrictionType.EncryptionRequired", AccessRestrictions.SessionRequired => "global::Opc.Ua.AccessRestrictionType.SessionRequired", AccessRestrictions.SessionWithSigningRequired => "global::Opc.Ua.AccessRestrictionType.SigningRequired | " + "global::Opc.Ua.AccessRestrictionType.SessionRequired", AccessRestrictions.SessionWithEncryptionRequired => "global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " + "global::Opc.Ua.AccessRestrictionType.SessionRequired", AccessRestrictions.SigningAndApplyToBrowseRequired => "global::Opc.Ua.AccessRestrictionType.SigningRequired | " + "global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse", AccessRestrictions.EncryptionAndApplyToBrowseRequired => "global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " + "global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse", AccessRestrictions.SessionAndApplyToBrowseRequired => "global::Opc.Ua.AccessRestrictionType.SessionRequired | " + "global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse", AccessRestrictions.SessionWithSigningAndApplyToBrowseRequired => "global::Opc.Ua.AccessRestrictionType.SigningRequired | " + "global::Opc.Ua.AccessRestrictionType.SessionRequired | " + "global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse", AccessRestrictions.SessionWithEncryptionAndApplyToBrowseRequired => "global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " + "global::Opc.Ua.AccessRestrictionType.SessionRequired | " + "global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse", _ => null }; } public static string GetPermissionTypeAsCode(this Permissions[] permissions) { if (permissions == null || permissions.Length == 0) { return "global::Opc.Ua.PermissionType.None"; } var parts = new HashSet(); foreach (Permissions p in permissions) { switch (p) { case Permissions.All: AddPermissionStrings(Permissions.WriteAttribute, parts); AddPermissionStrings(Permissions.WriteRolePermissions, parts); AddPermissionStrings(Permissions.WriteHistorizing, parts); AddPermissionStrings(Permissions.Write, parts); AddPermissionStrings(Permissions.InsertHistory, parts); AddPermissionStrings(Permissions.ModifyHistory, parts); AddPermissionStrings(Permissions.DeleteHistory, parts); AddPermissionStrings(Permissions.Call, parts); AddPermissionStrings(Permissions.AddReference, parts); AddPermissionStrings(Permissions.RemoveReference, parts); AddPermissionStrings(Permissions.DeleteNode, parts); AddPermissionStrings(Permissions.AddNode, parts); goto case Permissions.AllRead; case Permissions.AllRead: AddPermissionStrings(Permissions.Browse, parts); AddPermissionStrings(Permissions.Read, parts); AddPermissionStrings(Permissions.ReadHistory, parts); AddPermissionStrings(Permissions.ReceiveEvents, parts); AddPermissionStrings(Permissions.ReadRolePermissions, parts); break; default: AddPermissionStrings(p, parts); break; } } return parts.Count > 0 ? string.Join(" | ", parts) : "global::Opc.Ua.PermissionType.None"; static void AddPermissionStrings(Permissions p, HashSet parts) { string? part = p switch { Permissions.Browse => "global::Opc.Ua.PermissionType.Browse", Permissions.Read => "global::Opc.Ua.PermissionType.Read", Permissions.Write => "global::Opc.Ua.PermissionType.Write", Permissions.Call => "global::Opc.Ua.PermissionType.Call", Permissions.ReadHistory => "global::Opc.Ua.PermissionType.ReadHistory", Permissions.InsertHistory => "global::Opc.Ua.PermissionType.InsertHistory", Permissions.ModifyHistory => "global::Opc.Ua.PermissionType.ModifyHistory", Permissions.DeleteHistory => "global::Opc.Ua.PermissionType.DeleteHistory", Permissions.ReceiveEvents => "global::Opc.Ua.PermissionType.ReceiveEvents", Permissions.AddNode => "global::Opc.Ua.PermissionType.AddNode", Permissions.DeleteNode => "global::Opc.Ua.PermissionType.DeleteNode", Permissions.AddReference => "global::Opc.Ua.PermissionType.AddReference", Permissions.RemoveReference => "global::Opc.Ua.PermissionType.RemoveReference", Permissions.ReadRolePermissions => "global::Opc.Ua.PermissionType.ReadRolePermissions", Permissions.WriteRolePermissions => "global::Opc.Ua.PermissionType.WriteRolePermissions", Permissions.WriteAttribute => "global::Opc.Ua.PermissionType.WriteAttribute", Permissions.WriteHistorizing => "global::Opc.Ua.PermissionType.WriteHistorizing", _ => null }; if (part != null) { parts.Add(part); } } } /// /// Returns the identifier assigned to the node as one of the four /// identifier types defined by OPC 10000-3 5.2.2, or null when /// the node does not carry an explicit identifier. /// public static object? GetIdentifier(this NodeDesign? node) { if (node == null) { return null; } if (node.NumericIdSpecified) { return node.NumericId; } if (node.StringId != null) { return node.StringId; } if (node.GuidIdSpecified) { return node.GuidId; } if (node.OpaqueId != null) { return ByteString.From(node.OpaqueId); } return null; } /// /// Assigns the identifier of the node, clearing the identifiers of all /// other identifier types. Passing null clears the identifier. /// public static void SetIdentifier(this NodeDesign? node, object? identifier) { if (node == null) { return; } node.NumericId = 0; node.NumericIdSpecified = false; node.StringId = null; node.GuidId = Guid.Empty; node.GuidIdSpecified = false; node.OpaqueId = null; switch (identifier) { case uint numericId: node.NumericId = numericId; node.NumericIdSpecified = true; break; case string stringId: node.StringId = stringId; break; case Guid guidId: node.GuidId = guidId; node.GuidIdSpecified = true; break; case ByteString opaqueId: node.OpaqueId = opaqueId.ToArray(); break; } } /// /// Returns true when the node carries an explicit identifier of any of /// the four identifier types defined by OPC 10000-3 5.2.2. /// public static bool HasIdentifier(this NodeDesign node) { return node.GetIdentifier() != null; } /// /// Returns true when the node carries a Guid or Opaque identifier. /// These cannot be emitted as a C# const and are not part of the /// generated identifier reflection tables. /// public static bool HasNonConstantIdentifier(this NodeDesign node) { return node != null && !node.NumericIdSpecified && string.IsNullOrEmpty(node.StringId) && (node.GuidIdSpecified || node.OpaqueId != null); } /// /// Returns the C# literal for an identifier together with the C# type /// the literal has. /// public static string? GetIdentifierAsCode(object identifier, out string? type) { switch (identifier) { case uint numericId: type = "uint"; return CoreUtils.Format("{0}u", numericId); case string stringId: type = "string"; return stringId.AsStringLiteral(); case Guid guidId: type = "global::System.Guid"; return CoreUtils.Format( "new global::System.Guid({0})", guidId.ToString("D", CultureInfo.InvariantCulture).AsStringLiteral()); case ByteString opaqueId: type = "global::Opc.Ua.ByteString"; return CoreUtils.Format( "global::Opc.Ua.ByteString.FromBase64({0})", opaqueId.ToBase64().AsStringLiteral()); default: type = null; return null; } } public static string GetNodeIdAsCode( this NodeDesign? node, Namespace[] namespaces, string namespaceTableVariable) { string? identifier; if (node == null) { return "global::Opc.Ua.NodeId.Null"; } if (node.NumericIdSpecified) { identifier = CoreUtils.Format("{0}u", node.NumericId); } else if (!string.IsNullOrEmpty(node.StringId)) { identifier = node.StringId.AsStringLiteral(); } else if (node.HasNonConstantIdentifier()) { identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _); } else { uint? numericId = FindNumericIdentifier(node); if (numericId.HasValue) { identifier = CoreUtils.Format("{0}u", numericId.Value); } else { if (string.IsNullOrEmpty(node.SymbolicId.Name)) { return "global::Opc.Ua.NodeId.Null"; } identifier = node.SymbolicId.Name.AsStringLiteral(); } } return CoreUtils.Format( "global::Opc.Ua.NodeId.Create({0}, {1}, {2})", identifier, namespaces.GetNamespaceUriAsCode( node.SymbolicId.Namespace), namespaceTableVariable); } public static string GetExpandedNodeIdAsCode( this NodeDesign node, Namespace[] namespaces) { string? identifier; if (node == null) { return "global::Opc.Ua.ExpandedNodeId.Null"; } if (node.NumericIdSpecified) { identifier = CoreUtils.Format("{0}u", node.NumericId); } else if (!string.IsNullOrEmpty(node.StringId)) { identifier = node.StringId.AsStringLiteral(); } else if (node.HasNonConstantIdentifier()) { identifier = GetIdentifierAsCode(node.GetIdentifier()!, out _); } else { uint? numericId = FindNumericIdentifier(node); if (numericId.HasValue) { identifier = CoreUtils.Format("{0}u", numericId.Value); } else { if (string.IsNullOrEmpty(node.SymbolicId.Name)) { return "global::Opc.Ua.ExpandedNodeId.Null"; } identifier = node.SymbolicId.Name.AsStringLiteral(); } } return CoreUtils.Format( "new global::Opc.Ua.ExpandedNodeId({0}, {1})", identifier, namespaces.GetNamespaceUriAsCode( node.SymbolicId.Namespace)); } public static uint? FindNumericIdentifier(this NodeDesign node) { uint? numericId = node.NumericIdSpecified ? node.NumericId : null; for (NodeDesign? parent = node.Parent; !numericId.HasValue && parent != null; parent = parent.Parent) { if (parent.Hierarchy == null) { continue; } string browsePath = node.SymbolicId.Name; string parentPath = parent.SymbolicId.Name; if (browsePath.StartsWith(parentPath, StringComparison.InvariantCulture) && browsePath[parentPath.Length] == NodeDesign.PathChar) { browsePath = browsePath[(parentPath.Length + 1)..]; } if (parent.Hierarchy.Nodes.TryGetValue(browsePath, out HierarchyNode? instance) && instance.Instance!.NumericId != 0) { numericId = instance.Instance.NumericId; } } return numericId; } public static string GetNodeIdConstant( this IModelDesign design, XmlQualifiedName? symbolidId, string type, string namespaceTableVariable) { if (symbolidId == null) { return "global::Opc.Ua.NodeId.Null"; } NodeDesign node = design.FindNode( symbolidId, symbolidId.Name, type); if (node == null) { return "global::Opc.Ua.NodeId.Null"; } return node.GetNodeIdAsCode(design.Namespaces, namespaceTableVariable); } public static void SetDefaultValue( this VariableTypeDesign node, object value, IServiceMessageContext context) { if (value != null) { node.DefaultValue = AsXmlElement(value, context); node.DecodedValue = value; node.DecodedValueNamespaceUris = null; } else { node.DefaultValue = null; node.DecodedValue = null; node.DecodedValueNamespaceUris = null; } } public static void SetDefaultValue( this VariableDesign node, object? value, IServiceMessageContext context) { if (value != null) { node.DefaultValue = AsXmlElement(value, context); node.DecodedValue = value; node.DecodedValueNamespaceUris = null; } else { node.DefaultValue = null; node.DecodedValue = null; node.DecodedValueNamespaceUris = null; } } private static System.Xml.XmlElement? AsXmlElement( object value, IServiceMessageContext context) { if (VariantHelper.TryCastFromWithReflectionFallback(value, out Variant variant) && !variant.IsNull) { using var encoder = new XmlEncoder(context); encoder.WriteVariantValue(null, variant); var xmlDoc = new XmlDocument(); xmlDoc.LoadInnerXml(encoder.CloseAndReturnText()!); return xmlDoc.DocumentElement; } return null; } private static bool IsBasicDataType( this DataTypeDesign dataType, out BasicDataType basicDataType) { if (dataType.SymbolicName.Namespace == "http://opcfoundation.org/UA/") { #if NET8_0_OR_GREATER foreach (string name in Enum.GetNames()) #else foreach (string name in Enum.GetNames(typeof(BasicDataType))) #endif { if (name == dataType.SymbolicName.Name) { #if NET8_0_OR_GREATER basicDataType = Enum.Parse( dataType.SymbolicName.Name); #else basicDataType = (BasicDataType)Enum.Parse( typeof(BasicDataType), dataType.SymbolicName.Name); #endif return true; } } } basicDataType = default; return false; } } }