/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using Microsoft.Extensions.Logging;
using Opc.Ua.Schema.Model;
using Opc.Ua.Types;
namespace Opc.Ua.SourceGeneration
{
///
/// Generates data types and node state classes.
///
internal sealed class NodeStateGenerator : IGenerator
{
///
/// Create node state generator
///
public NodeStateGenerator(IGeneratorContext context)
{
m_context = context ?? throw new ArgumentNullException(nameof(context));
m_messageContext = ServiceMessageContext.CreateEmpty(context.Telemetry);
m_logger = context.Telemetry.CreateLogger();
CollectNodesToGenerate();
}
///
public IEnumerable Emit()
{
m_initializers.Clear();
if (m_instances.Count + m_nodes.Count == 0)
{
return [];
}
List resources =
[
EmitNodeStateClasses(),
EmitExtensions()
];
Resource initializers = EmbedInitializers();
if (initializers != null)
{
resources.Add(initializers);
}
return resources;
}
///
/// Create the nodestates classes
///
private TextFileResource EmitNodeStateClasses()
{
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format(
"{0}.NodeStates.g.cs",
nsPrefix));
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
using var templateWriter = new TemplateWriter(writer);
var template = new Template(templateWriter, NodeStateTemplates.File);
template.AddReplacement(Tokens.NamespacePrefix, nsPrefix);
template.AddReplacement(
Tokens.Namespace,
nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal));
template.AddReplacement(
Tokens.NamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
m_context.ModelDesign.TargetNamespace.Value));
// TODO: Switch to "instance" list and get "type" from entry.IsInstanceOfType
template.AddReplacement(
Tokens.ListOfTypes,
m_nodes.Values,
LoadTemplate_ListOfNodeStateClasses,
WriteTemplate_ListOfNodeStateClasses);
template.AddReplacement(
Tokens.ListOfTypeActivators,
NodeStateTemplates.ActivatorClass,
m_nodes.Values,
LoadTemplate_ListOfNodeStateActivators,
WriteTemplate_ListOfNodeStateActivators);
template.Render();
return fileName.AsTextFileResource();
}
///
/// Create extensions
///
private TextFileResource EmitExtensions()
{
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
string fileName = Path.Combine(m_context.OutputFolder, CoreUtils.Format(
"{0}.NodeStates.ex.g.cs",
nsPrefix));
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
using var templateWriter = new TemplateWriter(writer);
var template = new Template(
templateWriter,
NodeStateTemplates.Extensions_File);
template.AddReplacement(Tokens.NamespacePrefix, nsPrefix);
template.AddReplacement(
Tokens.Namespace,
nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal));
template.AddReplacement(
Tokens.NamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
m_context.ModelDesign.TargetNamespace.Value));
template.AddReplacement(
Tokens.ListOfNodeStateInitializers,
m_nodes.Values,
LoadTemplate_ListOfNodeStateInitializers,
WriteTemplate_ListOfNodeStateInitializers);
template.AddReplacement(
Tokens.ListOfNodeStateTypeFactories,
m_nodes.Values,
LoadTemplate_ListOfNodeStateFactories,
WriteTemplate_ListOfNodeStateFactories);
template.AddReplacement(
Tokens.ListOfNodeStateInstanceFactories,
m_instances.Values,
LoadTemplate_ListOfNodeStateFactories,
WriteTemplate_ListOfNodeStateFactories);
// TODO: Adopt to "instance" list and get "type" from entry.IsInstanceOfType
template.AddReplacement(
Tokens.ListOfActivatorRegistrations,
NodeStateTemplates.ActivatorRegistration,
m_nodes.Values,
LoadTemplate_ListOfNodeStateActivators,
WriteTemplate_ListOfNodeStateActivators);
template.Render();
return fileName.AsTextFileResource();
}
private TemplateString LoadTemplate_ListOfNodeStateActivators(ILoadContext context)
{
if (context.Target is not NodeToGenerate node)
{
return null;
}
if (ExcludeNodeStateClassGeneration(node))
{
return null;
}
switch (node.Design)
{
case ObjectTypeDesign:
case VariableTypeDesign:
case MethodDesign method
when method.HasArguments && method.IsMethodTypeDesign():
return context.TemplateString;
default:
return null;
}
}
private bool WriteTemplate_ListOfNodeStateActivators(IWriteContext context)
{
if (context.Target is not NodeToGenerate node)
{
return false;
}
string nodeClass = node.Design.GetNodeClassAsString();
context.Template.AddReplacement(Tokens.NodeClass, nodeClass);
context.Template.AddReplacement(Tokens.BrowseName, node.Design.SymbolicName.Name);
context.Template.AddReplacement(Tokens.SymbolicId, node.Design.SymbolicId.Name);
if (node.Design is TypeDesign type)
{
context.Template.AddReplacement(
Tokens.StateClassName,
CoreUtils.Format("{0}State", type.ClassName));
}
else if (node.Design is MethodDesign method)
{
context.Template.AddReplacement(
Tokens.StateClassName,
method.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value, []));
}
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfNodeStateClasses(ILoadContext context)
{
if (context.Target is not NodeToGenerate node)
{
return null;
}
if (ExcludeNodeStateClassGeneration(node))
{
return null;
}
return node.Design switch
{
ObjectTypeDesign
=> NodeStateTemplates.ObjectType_Class,
VariableTypeDesign
=> NodeStateTemplates.VariableType_Class,
MethodDesign method when method.HasArguments
=> NodeStateTemplates.MethodType_Class,
_ => null
};
}
private bool WriteTemplate_ListOfNodeStateClasses(IWriteContext context)
{
if (context.Target is not NodeToGenerate node)
{
return false;
}
NodeDesign root = node.Design;
context.Template.AddReplacement(Tokens.NodeClass, root.GetNodeClassAsString());
context.Template.AddReplacement(Tokens.TypeName, root.SymbolicName.Name);
context.Template.AddReplacement(Tokens.SymbolicId, root.SymbolicId.Name);
context.Template.AddReplacement(Tokens.BrowseName, root.SymbolicName.Name);
context.Template.AddReplacement(
Tokens.Description,
root.Description != null ? root.Description.Value : string.Empty);
context.Template.AddReplacement(
Tokens.NamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
root.SymbolicName.Namespace));
context.Template.AddReplacement(
Tokens.NamespacePrefix,
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
root.SymbolicId.Namespace));
context.Template.AddReplacement(
Tokens.XmlNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantForXmlNamespace(
root.SymbolicId.Namespace));
context.Template.AddReplacement(
Tokens.BrowseNameNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
root.SymbolicName.Namespace));
switch (root)
{
case MethodDesign method:
AddNodeStateClassMethodTypeReplacements(context, method);
break;
case ObjectTypeDesign objectType:
AddNodeStateClassObjectTypeReplacements(context, objectType);
break;
case VariableTypeDesign variableType:
AddNodeStateClassVariableTypeReplacements(context, variableType);
break;
default:
return false;
}
context.Template.AddReplacement(
Tokens.InitializeOptionalChildren,
NodeStateTemplates.InitializeOptionalChild,
node.Children.Values,
LoadTemplate_InitializeOptionalChildren,
WriteTemplate_InitializeOptionalChildren);
context.Template.AddReplacement(
Tokens.ListOfNonMandatoryChildren,
node.Children.Values,
LoadTemplate_ListOfNonMandatoryChildren,
WriteTemplate_ListOfNonMandatoryChildren);
context.Template.AddReplacement(
Tokens.ListOfFields,
node.Children.Values,
LoadTemplate_ListOfFields);
context.Template.AddReplacement(
Tokens.ListOfProperties,
node.Children.Values,
LoadTemplate_ListOfProperties,
WriteTemplate_ListOfProperties);
context.Template.AddReplacement(
Tokens.ListOfChildOperations,
NodeStateTemplates.ChildOperations,
[node],
LoadTemplate_FindChildMethods,
WriteTemplate_FindChildMethods);
List properties =
GetChildrenWithProperties(node, true);
context.Template.AddReplacement(
Tokens.ListOfChildCopies,
NodeStateTemplates.CloneChild,
properties,
WriteTemplate_ListOfChildren);
context.Template.AddReplacement(
Tokens.ListOfChildHashes,
NodeStateTemplates.HashChild,
properties,
WriteTemplate_ListOfChildren);
context.Template.AddReplacement(
Tokens.ListOfEqualityComparers,
NodeStateTemplates.CompareChild,
properties,
WriteTemplate_ListOfChildren);
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfNonMandatoryChildren(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return null;
}
if (node.IsNotExplicitlyDefined)
{
return null;
}
if (instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces))
{
return null;
}
switch (GetEffectiveModellingRule(node, instance))
{
case ModellingRule.Optional:
return NodeStateTemplates.OptionalMethod;
case ModellingRule.OptionalPlaceholder:
case ModellingRule.MandatoryPlaceholder:
return NodeStateTemplates.PlaceHolderMethod;
case ModellingRule.ExposesItsArray:
case ModellingRule.CardinalityRestriction:
case ModellingRule.MandatoryShared:
// TODO?
break;
}
return null;
}
private bool WriteTemplate_ListOfNonMandatoryChildren(IWriteContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return false;
}
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
if (!instance.IsOverridden())
{
if (!s_builtInMethodNames.Contains(instance.SymbolicName.Name))
{
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
}
}
else
{
instance = instance.GetMergedInstance();
}
context.Template.AddReplacement(Tokens.Description,
instance.Description != null ? instance.Description.Value : string.Empty);
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
return context.Template.Render();
}
private TemplateString LoadTemplate_TypedVariableType(ILoadContext context)
{
if (context.Target is not VariableTypeDesign variableType)
{
return null;
}
if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
{
return null;
}
return context.TemplateString;
}
private bool WriteTemplate_TypedVariableType(IWriteContext context)
{
if (context.Target is not VariableTypeDesign type)
{
return false;
}
context.Template.AddReplacement(Tokens.NodeClass, type.GetNodeClassAsString());
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
context.Template.AddReplacement(Tokens.TypeName, type.SymbolicName.Name);
context.Template.AddReplacement(Tokens.BrowseName, type.SymbolicName.Name);
return context.Template.Render();
}
private TemplateString LoadTemplate_VariableTypeValue(ILoadContext context)
{
if (context.Target is not VariableTypeDesign variableType)
{
return null;
}
Dictionary fields = [];
CollectMatchingFields(variableType, fields);
if (fields.Count == 0)
{
return null;
}
return context.TemplateString;
}
private bool WriteTemplate_VariableTypeValue(IWriteContext context)
{
if (context.Target is not VariableTypeDesign type)
{
return false;
}
Dictionary fields = [];
CollectMatchingFields(type, fields);
if (fields.Count == 0)
{
return false;
}
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
context.Template.AddReplacement(Tokens.DataType, type.DataTypeNode.GetDotNetTypeName(
ValueRank.Scalar,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
nullable: NullableAnnotation.NonNullable));
context.Template.AddReplacement(
Tokens.ListOfChildInitializers,
fields,
LoadTemplate_VariableTypeValueInitializers);
context.Template.AddReplacement(
Tokens.ListOfUpdateChildrenChangeMasks,
fields,
LoadTemplate_VariableTypeValueChangeMasks);
context.Template.AddReplacement(
Tokens.ListOfChildMethods,
NodeStateTemplates.VariableType_ValueMethods,
fields,
WriteTemplate_VariableTypeValueField);
return context.Template.Render();
}
private TemplateString LoadTemplate_VariableTypeValueInitializers(ILoadContext context)
{
if (context.Target is not KeyValuePair field || field.Value == null)
{
return null;
}
string name = field.Key;
string path = field.Key;
context.Out.WriteLine("instance = m_variable.{0};", path);
context.Out.WriteLine("if (instance != null)");
context.Out.WriteLine("{");
context.Out.WriteLine(" instance.OnReadValue = OnRead_{0};", name);
context.Out.WriteLine(" instance.OnWriteValue = OnWrite_{0};", name);
context.Out.WriteLine(" updateList.Add(instance);");
context.Out.WriteLine("}");
return null;
}
private TemplateString LoadTemplate_VariableTypeValueChangeMasks(ILoadContext context)
{
if (context.Target is not KeyValuePair field ||
field.Value == null)
{
return null;
}
var dataType = field.Value.Parent as DataTypeDesign;
string path = field.Key;
if (dataType.IsDotNetEqualityComparable(field.Value.ValueRank))
{
context.Out.WriteLine("if (m_value.{0} != newValue.{0})", path);
}
else
{
context.Out.WriteLine(
"if (!global::Opc.Ua.CoreUtils.IsEqual(m_value.{0}, newValue.{0}))",
path);
}
context.Out.WriteLine("{");
context.Out.WriteLine(
" UpdateChildVariableStatus(m_variable.{0}, ref statusCode, ref timestamp);",
path);
context.Out.WriteLine("}");
return null;
}
private bool WriteTemplate_VariableTypeValueField(IWriteContext context)
{
if (context.Target is not KeyValuePair field ||
field.Value == null)
{
return false;
}
context.Template.AddReplacement(Tokens.ChildName, field.Key);
context.Template.AddReplacement(Tokens.ChildPath, field.Key);
string childDataType = field.Value.DataTypeNode.GetDotNetTypeName(
field.Value.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
nullable: NullableAnnotation.NonNullable);
context.Template.AddReplacement(Tokens.ChildDataType, childDataType);
if (field.Value.DataTypeNode.NeedsCloning())
{
context.Template.AddReplacement(
Tokens.ValueWrite,
CoreUtils.Format(
"CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue",
childDataType));
}
else
{
context.Template.AddReplacement(Tokens.ValueWrite, "newValue");
}
AddVariantAccessor(context, field.Value.DataTypeNode);
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfFields(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return null;
}
if (node.IsNotExplicitlyDefined || instance.IsOverridden())
{
return null;
}
ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance);
if (effectiveRule
is ModellingRule.ExposesItsArray
or ModellingRule.MandatoryPlaceholder
or ModellingRule.OptionalPlaceholder)
{
return null;
}
if (effectiveRule == ModellingRule.None)
{
return null;
}
if (instance is MethodDesign method &&
GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory &&
GetEffectiveModellingRule(node, method) != ModellingRule.Optional)
{
return null;
}
if (IsBuiltInProperty(node))
{
return null;
}
context.Out.WriteLine(
"private {0}? {1};",
instance.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces),
instance.GetChildFieldName());
return null;
}
private TemplateString LoadTemplate_ListOfInputArguments(ILoadContext context)
{
if (context.Target is not Parameter field)
{
return null;
}
string fieldName = field.GetChildFieldName()[2..];
string typeName = field.DataTypeNode.GetMethodArgumentTypeAsCode(
field.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
false);
switch (field.DataTypeNode.BasicDataType)
{
case BasicDataType.UserDefined:
context.Out.WriteLine(
"_inputArguments[{2}].TryGetStructure(out {1} {0});",
fieldName,
typeName,
context.Index);
break;
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
context.Out.WriteLine(
"{1} {0} = _inputArguments[{2}];",
fieldName,
typeName,
context.Index);
break;
default:
context.Out.WriteLine(
"_inputArguments[{2}].TryGetValue(out {1} {0});",
fieldName,
typeName,
context.Index);
break;
}
return null;
}
private TemplateString LoadTemplate_ListOfOutputDeclarations(ILoadContext context)
{
if (context.Target is not Parameter field)
{
return null;
}
string fieldName = field.GetChildFieldName()[2..];
string typeName = field.DataTypeNode.GetMethodArgumentTypeAsCode(
field.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
field.IsOptional);
switch (field.DataTypeNode.BasicDataType)
{
case BasicDataType.UserDefined:
context.Out.WriteLine(
"_outputArguments[{2}].TryGetStructure(out {1} {0});",
fieldName,
typeName,
context.Index);
break;
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
context.Out.WriteLine(
"{1} {0} = _outputArguments[{2}];",
fieldName,
typeName,
context.Index);
break;
default:
context.Out.WriteLine(
"_outputArguments[{2}].TryGetValue(out {1} {0});",
fieldName,
typeName,
context.Index);
break;
}
return null;
}
private TemplateString LoadTemplate_ListOfOutputArguments(ILoadContext context)
{
if (context.Target is not Parameter field)
{
return null;
}
string fieldName = field.GetChildFieldName()[2..];
switch (field.DataTypeNode.BasicDataType)
{
case BasicDataType.UserDefined:
context.Out.WriteLine(
"_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure({0});",
fieldName,
context.Index);
break;
case BasicDataType.BaseDataType:
context.Out.WriteLine(
"_outputArguments[{1}] = {0};",
fieldName,
context.Index);
break;
default:
context.Out.WriteLine(
"_outputArguments[{1}] = global::Opc.Ua.Variant.From({0});",
fieldName,
context.Index);
break;
}
return null;
}
private TemplateString LoadTemplate_OnCallDeclaration(ILoadContext context)
{
if (context.Target is not MethodDesign method)
{
return null;
}
context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,");
context.Out.WriteLine("global::Opc.Ua.MethodState _method,");
context.Out.Write("global::Opc.Ua.NodeId _objectId");
if (method.InputArguments != null)
{
for (int ii = 0; ii < method.InputArguments.Length; ii++)
{
Parameter argument = method.InputArguments[ii];
context.Out.WriteLine(",");
context.Out.Write("{1} {0}", argument.GetChildFieldName()[2..],
argument.DataTypeNode.GetMethodArgumentTypeAsCode(
argument.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
argument.IsOptional));
}
}
if (method.OutputArguments != null)
{
for (int ii = 0; ii < method.OutputArguments.Length; ii++)
{
Parameter argument = method.OutputArguments[ii];
context.Out.WriteLine(",");
context.Out.Write("ref {1} {0}", argument.GetChildFieldName()[2..],
argument.DataTypeNode.GetMethodArgumentTypeAsCode(
argument.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
argument.IsOptional));
}
}
context.Out.WriteLine(");");
return null;
}
private TemplateString LoadTemplate_OnCallAsyncDeclaration(ILoadContext context)
{
if (context.Target is not MethodDesign method)
{
return null;
}
context.Out.WriteLine("global::Opc.Ua.ISystemContext _context,");
context.Out.WriteLine("global::Opc.Ua.MethodState _method,");
context.Out.Write("global::Opc.Ua.NodeId _objectId");
if (method.InputArguments != null)
{
for (int ii = 0; ii < method.InputArguments.Length; ii++)
{
Parameter argument = method.InputArguments[ii];
context.Out.WriteLine(",");
context.Out.Write("{1} {0}", argument.GetChildFieldName()[2..],
argument.DataTypeNode.GetMethodArgumentTypeAsCode(
argument.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
argument.IsOptional));
}
}
context.Out.WriteLine(",");
context.Out.Write(
"global::System.Threading.CancellationToken cancellationToken");
context.Out.WriteLine(");");
return null;
}
private TemplateString LoadTemplate_ListOfOutputArgumentsFromResult(ILoadContext context)
{
if (context.Target is not Parameter field)
{
return null;
}
string fieldName = field.GetChildFieldName()[2..].ToUpperCamelCase();
switch (field.DataTypeNode.BasicDataType)
{
case BasicDataType.UserDefined:
context.Out.WriteLine(
"_outputArguments[{1}] = global::Opc.Ua.Variant.FromStructure(_result.{0});",
fieldName,
context.Index);
break;
case BasicDataType.BaseDataType when field.ValueRank == ValueRank.Scalar:
context.Out.WriteLine(
"_outputArguments[{1}] = _result.{0};",
fieldName,
context.Index);
break;
default:
context.Out.WriteLine(
"_outputArguments[{1}] = global::Opc.Ua.Variant.From(_result.{0});",
fieldName,
context.Index);
break;
}
return null;
}
private TemplateString LoadTemplate_ListOfResultProperties(ILoadContext context)
{
if (context.Target is not Parameter field)
{
return null;
}
string fieldName = field.GetChildFieldName()[2..].ToUpperCamelCase();
context.Out.WriteLine(
"public {1} {0} {{ get; set; }}",
fieldName,
field.DataTypeNode.GetMethodArgumentTypeAsCode(
field.ValueRank,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
field.IsOptional));
return null;
}
private TemplateString LoadTemplate_OnCallImplementation(ILoadContext context)
{
if (context.Target is not MethodDesign method)
{
return null;
}
context.Out.WriteLine("_result = OnCall(");
context.Out.WriteLine(" _context,");
context.Out.WriteLine(" this,");
context.Out.Write(" _objectId");
if (method.InputArguments != null)
{
for (int ii = 0; ii < method.InputArguments.Length; ii++)
{
context.Out.WriteLine(",");
context.Out.Write(" {0}", method.InputArguments[ii].GetChildFieldName()[2..]);
}
}
if (method.OutputArguments != null)
{
for (int ii = 0; ii < method.OutputArguments.Length; ii++)
{
context.Out.WriteLine(",");
context.Out.Write(" ref {0}", method.OutputArguments[ii].GetChildFieldName()[2..]);
}
}
context.Out.WriteLine(");");
return null;
}
private TemplateString LoadTemplate_OnCallAsyncImplementation(ILoadContext context)
{
if (context.Target is not MethodDesign method)
{
return null;
}
context.Out.WriteLine("_result = await OnCallAsync(");
context.Out.WriteLine(" _context,");
context.Out.WriteLine(" this,");
context.Out.Write(" _objectId");
if (method.InputArguments != null)
{
for (int ii = 0; ii < method.InputArguments.Length; ii++)
{
context.Out.WriteLine(",");
context.Out.Write(" {0}", method.InputArguments[ii].GetChildFieldName()[2..]);
}
}
context.Out.WriteLine(",");
context.Out.WriteLine(" cancellationToken).ConfigureAwait(false);");
return null;
}
private TemplateString LoadTemplate_InitializeOptionalChildren(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return null;
}
if (node.IsNotExplicitlyDefined)
{
return null;
}
if (GetEffectiveModellingRule(node, instance) != ModellingRule.Optional)
{
return null;
}
if (IsBuiltInProperty(node))
{
return null;
}
return context.TemplateString;
}
private bool WriteTemplate_InitializeOptionalChildren(IWriteContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return context.Template.Render();
}
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
if (instance.Parent is MethodDesign method)
{
context.Template.AddReplacement(Tokens.ClassName, method.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
[]));
}
else if (instance.Parent is TypeDesign type)
{
context.Template.AddReplacement(
Tokens.ClassName,
type.ClassName);
}
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfProperties(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return null;
}
if (node.IsNotExplicitlyDefined)
{
return null;
}
ModellingRule effectiveRule = GetEffectiveModellingRule(node, instance);
if (effectiveRule
is ModellingRule.ExposesItsArray
or ModellingRule.MandatoryPlaceholder
or ModellingRule.OptionalPlaceholder)
{
return null;
}
if (effectiveRule == ModellingRule.None)
{
return null;
}
if (instance is MethodDesign method &&
GetEffectiveModellingRule(node, method) != ModellingRule.Mandatory &&
GetEffectiveModellingRule(node, method) != ModellingRule.Optional)
{
return null;
}
if (instance.IsOverridden())
{
if (instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces))
{
return null;
}
return NodeStateTemplates.PropertyOverride;
}
if (IsBuiltInProperty(node))
{
return null;
}
return NodeStateTemplates.Property;
}
private bool WriteTemplate_ListOfProperties(IWriteContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return false;
}
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
if (!instance.IsOverridden())
{
if (!s_builtInPropertyNames.Contains(instance.SymbolicName.Name) ||
(instance is VariableDesign && instance.SymbolicName.Name == "Value"))
{
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
}
}
else
{
instance = instance.GetMergedInstance();
}
context.Template.AddReplacement(Tokens.Description,
instance.Description != null ? instance.Description.Value : string.Empty);
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
return context.Template.Render();
}
private TemplateString LoadTemplate_FindChildMethods(ILoadContext context)
{
if (context.Target is not NodeToGenerate node)
{
return null;
}
int count = 0;
foreach (NodeToGenerate child in node.Children.Values)
{
if (child.Design is not InstanceDesign instance)
{
continue;
}
if (child.IsNotExplicitlyDefined)
{
continue;
}
ModellingRule childEffectiveRule =
GetEffectiveModellingRule(child, instance);
if (childEffectiveRule is
ModellingRule.ExposesItsArray or
ModellingRule.MandatoryPlaceholder or
ModellingRule.OptionalPlaceholder)
{
continue;
}
if (childEffectiveRule is ModellingRule.None)
{
continue;
}
if (childEffectiveRule is
ModellingRule.OptionalPlaceholder or
ModellingRule.MandatoryPlaceholder)
{
continue;
}
if (instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces))
{
continue;
}
count++;
}
if (count == 0)
{
return null;
}
return context.TemplateString;
}
private bool WriteTemplate_FindChildMethods(IWriteContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not TypeDesign type)
{
return false;
}
List childrenWithProperties = GetChildrenWithProperties(node);
context.Template.AddReplacement(
Tokens.ListOfFindChildCase,
NodeStateTemplates.FindChildCase,
childrenWithProperties,
WriteTemplate_ListOfChildren);
context.Template.AddReplacement(
Tokens.ListOfCreateOrReplaceChild,
NodeStateTemplates.CreateOrReplaceChild,
childrenWithProperties,
WriteTemplate_ListOfChildren);
List additionalChildren = GetAdditionalChildren(node);
context.Template.AddReplacement(
Tokens.ListOfFindChildren,
NodeStateTemplates.FindChildren,
additionalChildren,
WriteTemplate_ListOfChildren);
context.Template.AddReplacement(
Tokens.ListOfRemoveChild,
NodeStateTemplates.RemoveChild,
additionalChildren,
WriteTemplate_ListOfChildren);
return context.Template.Render();
}
private bool WriteTemplate_ListOfChildren(IWriteContext context)
{
if (context.Target is not InstanceDesign instance)
{
return false;
}
context.Template.AddReplacement(Tokens.AccessorSymbol, "public new");
if (!instance.IsOverridden())
{
context.Template.AddReplacement(Tokens.AccessorSymbol, "public");
}
context.Template.AddReplacement(Tokens.ClassName, instance.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(Tokens.ClassFactory, instance.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
asFactory: true));
context.Template.AddReplacement(Tokens.SymbolicId, instance.SymbolicId.Name);
context.Template.AddReplacement(Tokens.ChildName, instance.SymbolicName.Name);
// The runtime BrowseName.Name may differ from the symbolic name
// (the design's attribute can override it, e.g.
// Boiler's "InputPipe" InstanceDesign with BrowseName "PipeX001").
// FindChild switches on the runtime browse name, so the case
// label must use that string verbatim.
context.Template.AddReplacement(Tokens.ChildBrowseName,
!string.IsNullOrEmpty(instance.BrowseName)
? instance.BrowseName
: instance.SymbolicName.Name);
context.Template.AddReplacement(Tokens.FieldName, instance.GetChildFieldName());
context.Template.AddReplacement(Tokens.NodeClass, instance.GetNodeClassAsString());
context.Template.AddReplacement(Tokens.BrowseName, instance.SymbolicName.Name);
context.Template.AddReplacement(
Tokens.BrowseNameNamespacePrefix,
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
instance.SymbolicName.Namespace));
context.Template.AddReplacement(
Tokens.BrowseNameNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
instance.SymbolicName.Namespace));
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfNodeStateInitializers(ILoadContext context)
{
if (context.Target is not NodeToGenerate node)
{
return null;
}
if (node.Design.IsMethodTypeDesign() ||
node.InstanceOf != null ||
!IsInAddressSpace(node) ||
node.Design.Purpose == DataTypePurpose.Testing)
{
return null;
}
if (node.Parent != null)
{
// No registering of children
return null;
}
return NodeStateTemplates.Add;
}
private bool WriteTemplate_ListOfNodeStateInitializers(IWriteContext context)
{
if (context.Target is not NodeToGenerate node)
{
return false;
}
context.Template.AddReplacement(Tokens.SymbolicId, node.Design.SymbolicId.Name);
context.Template.AddReplacement(Tokens.SymbolicName, node.Design.SymbolicName.Name);
return context.Template.Render();
}
private TemplateString LoadTemplate_ListOfNodeStateFactories(ILoadContext context)
{
if (context.Target is not NodeToGenerate node)
{
return null;
}
if (context.Token == Tokens.ListOfNodeStateInstanceFactories)
{
if (node.Parent != null)
{
return null;
}
return node.Design switch
{
ObjectDesign => NodeStateTemplates.Create_InstanceOfObjectType,
VariableDesign => NodeStateTemplates.Create_InstanceOfVariableType,
MethodDesign => NodeStateTemplates.Create_InstanceOfMethodType,
_ => null
};
}
if (node.Parent != null)
{
if (node.Design is not InstanceDesign instance)
{
return null;
}
if (GetEffectiveModellingRule(node, instance) is
not ModellingRule.MandatoryPlaceholder and
not ModellingRule.OptionalPlaceholder)
{
return node.Design switch
{
ObjectDesign => NodeStateTemplates.Create_ChildObject,
VariableDesign => NodeStateTemplates.Create_ChildVariable,
MethodDesign => NodeStateTemplates.Create_ChildMethod,
_ => null
};
}
return node.Design switch
{
ObjectDesign => NodeStateTemplates.Create_ChildObject_Placeholder,
VariableDesign => NodeStateTemplates.Create_ChildVariable_Placeholder,
MethodDesign => NodeStateTemplates.Create_ChildMethod_Placeholder,
_ => null
};
}
return node.Design switch
{
ObjectTypeDesign => NodeStateTemplates.Create_ObjectType,
VariableTypeDesign => NodeStateTemplates.Create_VariableType,
ReferenceTypeDesign => NodeStateTemplates.Create_ReferenceType,
DataTypeDesign => NodeStateTemplates.Create_DataType,
ObjectDesign => NodeStateTemplates.Create_Object,
VariableDesign => NodeStateTemplates.Create_Variable,
ViewDesign => NodeStateTemplates.Create_View,
_ => null
};
}
private bool WriteTemplate_ListOfNodeStateFactories(IWriteContext context)
{
if (context.Target is not NodeToGenerate node)
{
return false;
}
NodeDesign root = node.Design;
// Common replacements for all node types
context.Template.AddReplacement(
Tokens.SymbolicId,
root.SymbolicId.Name);
context.Template.AddReplacement(
Tokens.SymbolicName,
root.SymbolicName.Name);
context.Template.AddReplacement(Tokens.BrowseName, root.SymbolicName.Name);
context.Template.AddReplacement(Tokens.NumericIdValue, root.FindNumericIdentifier() ?? 0);
context.Template.AddReplacement(
Tokens.TypeName,
node.InstanceOf?.Design.SymbolicName.Name);
context.Template.AddReplacement(
Tokens.NodeIdConstant,
root.GetNodeIdAsCode(m_context.ModelDesign.Namespaces, kNamespaceTableContextVariable));
// .net efficiently interns constant string, so usage of the BrowseName constants is not needed.
string symbolicNameSymbol = root.SymbolicName.Name.AsStringLiteral();
context.Template.AddReplacement(Tokens.BrowseNameSymbol,
string.IsNullOrEmpty(root.BrowseName) ? symbolicNameSymbol : root.BrowseName.AsStringLiteral());
context.Template.AddReplacement(Tokens.SymbolicNameSymbol, symbolicNameSymbol);
context.Template.AddReplacement(Tokens.DisplayName, GetDisplayNameValue(root) ?? symbolicNameSymbol);
context.Template.AddReplacement(
Tokens.BrowseNameNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
root.SymbolicName.Namespace));
context.Template.AddReplacement(
Tokens.DescriptionValue,
GetDescriptionValue(root));
context.Template.AddReplacement(
Tokens.WriteMaskValue,
"global::Opc.Ua.AttributeWriteMask.None");
context.Template.AddReplacement(
Tokens.UserWriteMaskValue,
"global::Opc.Ua.AttributeWriteMask.None");
// Add Children
context.Template.AddReplacement(
Tokens.ListOfChildNodeStates,
node.AllChildren,
LoadTemplate_ReplaceChild);
context.Template.AddReplacement(
Tokens.ListOfOptionalChildNodeStates,
node.AllChildren,
LoadTemplate_ReplaceChild);
// Add References
HashSet references = GetReferences(node);
context.Template.AddReplacement(
Tokens.ListOfReferences,
references,
LoadTemplate_AddReference);
// Node type-specific replacements
switch (root)
{
case ObjectTypeDesign objectType:
AddObjectTypeStateFactoryReplacements(context, objectType);
break;
case VariableTypeDesign variableType:
AddVariableTypeStateFactoryReplacements(context, variableType);
break;
case ReferenceTypeDesign referenceType:
AddReferenceTypeStateFactoryReplacements(context, referenceType);
break;
case DataTypeDesign dataType:
AddDataTypeStateFactoryReplacements(context, dataType);
break;
case ObjectDesign objectDesign:
AddObjectReplacements(context, node, objectDesign, references);
break;
case VariableDesign variableDesign:
AddVariableStateFactoryReplacements(context, node, variableDesign, references);
break;
case MethodDesign methodDesign:
AddMethodStateFactoryReplacements(context, node, methodDesign);
break;
case ViewDesign viewDesign:
AddViewStateFactoryReplacements(context, viewDesign);
break;
}
// Release status
Export.ReleaseStatus releaseStatus =
root.ReleaseStatus.ToNodeSetReleaseStatus();
context.Template.AddReplacement(
Tokens.ReleaseStatusValue,
releaseStatus != Export.ReleaseStatus.Released
? CoreUtils.Format(
"state.ReleaseStatus = global::Opc.Ua.Export.ReleaseStatus.{0};",
releaseStatus)
: null);
// Categories
context.Template.AddReplacement(
Tokens.CategoriesValue,
!string.IsNullOrEmpty(root.Category)
? CoreUtils.Format(
"state.Categories = new string[] {{ {0} }};",
string.Join(
", ",
root.Category
.Split([','])
.Select(c => CoreUtils.Format("\"{0}\"", c.Trim()))))
: null);
// Specification
context.Template.AddReplacement(
Tokens.SpecificationValue,
root.PartNo != 0
? CoreUtils.Format("state.Specification = \"Part{0}\";", root.PartNo)
: null);
// Access restrictions — emit on all nodes (type and instance).
// Type hierarchy nodes carry restrictions as metadata but the server
// bypasses enforcement via IsPartOfTypeHierarchy at runtime.
string accessRestrictions =
root.AccessRestrictions.GetAccessRestrictionsAsCode(
root.AccessRestrictionsSpecified) ?? root.DefaultAccessRestrictions.GetAccessRestrictionsAsCode(
root.DefaultAccessRestrictionsSpecified);
context.Template.AddReplacement(
Tokens.AccessRestrictionsValue,
accessRestrictions != null
? CoreUtils.Format("state.AccessRestrictions = {0};", accessRestrictions)
: null);
// Role permissions
HashSet rolePermissions = GetRolePermissions(root);
context.Template.AddReplacement(
Tokens.ListOfRolePermissions,
NodeStateTemplates.ListOfRolePermissions,
[rolePermissions],
WriteTemplate_RolePermissions);
return context.Template.Render();
}
private TemplateString LoadTemplate_AddReference(ILoadContext context)
{
if (context.Target is not ReferenceToGenerate reference)
{
return null;
}
context.Out.WriteLine(
"state.AddReference({0}, {1}, {2});",
m_context.ModelDesign.GetNodeIdConstant(
reference.ReferenceTypeId,
"",
kNamespaceTableContextVariable),
reference.IsInverse ? "true" : "false",
reference.TargetNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
return null;
}
private TemplateString LoadTemplate_ReplaceChild(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
{
return null;
}
// Factory methods use the instance's own modelling rule for the
// mandatory/optional list classification so that children the
// instance explicitly marks Mandatory are always created. The
// effective (type-definition) rule is used elsewhere for the
// type-class structure (fields, properties, optional init).
if (context.Token == Tokens.ListOfOptionalChildNodeStates)
{
if (instance.ModellingRule == ModellingRule.Mandatory)
{
return null;
}
// Real instance children of a top-level (non-typed) parent belong in
// the always-emitted list so they materialize for the actual instance,
// not only when the node is treated as a type template.
if (node.Parent != null && node.Parent.Parent == null && node.Parent.InstanceOf == null)
{
return null;
}
}
// Otherwise only add mandatory children - all others are created on demand
else if (instance.ModellingRule != ModellingRule.Mandatory)
{
// Exception: real instance children of a top-level (non-typed) parent.
if (!(node.Parent != null && node.Parent.Parent == null && node.Parent.InstanceOf == null))
{
return null;
}
}
string forInstanceVariableValue =
node.RootIsTypeDefinition ? "forInstance" :
node.Parent?.InstanceOf != null || node.Parent?.Parent == null ? "true" : "forInstance";
if (node.Parent != null && IsInAddressSpace(node.Parent))
{
switch (node.Parent.Design)
{
case TypeDesign:
context.Out.WriteLine(
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
instance.SymbolicId.Name,
forInstanceVariableValue);
return null;
case InstanceDesign parentInstance:
if (HasChildDefined(parentInstance.TypeDefinitionNode, instance.SymbolicName.Name) ||
IsBuiltInProperty(node))
{
// Children whose SymbolicName ends in "_Placeholder"
// come from OptionalPlaceholder/MandatoryPlaceholder
// slots on the type, which emit Add*_Placeholder
// methods (cardinality 0..N) instead of
// CreateOrReplace*_Placeholder methods (which only
// exist for fixed Mandatory/Optional slots).
// Fall through to AddChild for these.
bool isPlaceholderSlot = instance.SymbolicName.Name
.EndsWith("_Placeholder", StringComparison.Ordinal);
if (!isPlaceholderSlot)
{
switch (instance.ModellingRule)
{
case ModellingRule.Mandatory:
case ModellingRule.Optional:
context.Out.WriteLine(
"state.CreateOrReplace{0}(context, Create{1}(context, state, forInstance: {2}));",
instance.SymbolicName.Name,
instance.SymbolicId.Name,
forInstanceVariableValue);
return null;
case ModellingRule.OptionalPlaceholder:
case ModellingRule.MandatoryPlaceholder:
// TODO
break;
case ModellingRule.ExposesItsArray:
case ModellingRule.None:
case ModellingRule.CardinalityRestriction:
case ModellingRule.MandatoryShared:
break;
}
}
}
break;
}
}
context.Out.WriteLine(
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
instance.SymbolicId.Name,
forInstanceVariableValue);
return null;
}
private bool WriteTemplate_RolePermissions(IWriteContext context)
{
if (context.Target is not HashSet rolePermissions ||
rolePermissions.Count == 0)
{
return false;
}
context.Template.AddReplacement(
Tokens.ListOfRolePermissions,
NodeStateTemplates.RolePermission,
rolePermissions,
LoadTemplate_RolePermissionEntry,
WriteTemplate_RolePermissionEntry);
return context.Template.Render();
}
private bool WriteTemplate_ArgumentCollection(IWriteContext context)
{
switch (context.Target)
{
case IList arguments:
// The parameter that is packed into the Argument value
context.Template.AddReplacement(
Tokens.ListOfValues,
NodeStateTemplates.ArgumentValue,
arguments,
WriteTemplate_Argument);
break;
case Parameter[] parameters:
context.Template.AddReplacement(
Tokens.ListOfValues,
NodeStateTemplates.ArgumentValue,
parameters,
WriteTemplate_Argument);
break;
default:
return false;
}
context.Template.AddReplacement(Tokens.DataType, "global::Opc.Ua.Argument");
return context.Template.Render();
}
private bool WriteTemplate_Argument(IWriteContext context)
{
Argument argument = null;
Parameter parameter;
switch (context.Target)
{
case Argument arg:
argument = arg;
parameter = argument.Value as Parameter;
break;
case Parameter parm:
parameter = parm;
break;
default:
return false;
}
context.Template.AddReplacement(
Tokens.Name,
parameter == null ?
argument.Name.AsStringLiteral() :
parameter.Name.AsStringLiteral());
context.Template.AddReplacement(
Tokens.DataType,
m_context.ModelDesign.GetNodeIdConstant(
parameter?.DataType,
"",
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.ValueRank,
parameter?.ValueRank.GetValueRankAsCode(parameter.ArrayDimensions));
context.Template.AddReplacement(
Tokens.ArrayDimensions,
parameter?.ValueRank.GetArrayDimensionsAsCode(parameter.ArrayDimensions) ?? "default");
context.Template.AddReplacement(
Tokens.Description,
(parameter?.Description).GetLocalizedTextAsCode(true));
return context.Template.Render();
}
private TemplateString LoadTemplate_RolePermissionEntry(ILoadContext context)
{
if (context.Target is not RolePermission)
{
return null;
}
return NodeStateTemplates.RolePermission;
}
private bool WriteTemplate_RolePermissionEntry(IWriteContext context)
{
if (context.Target is not RolePermission rolePermission)
{
return false;
}
if (!m_context.ModelDesign.TryFindNode(
rolePermission.Role,
rolePermission.Role.Name,
"RoleType",
out ObjectDesign roleNode))
{
return false;
}
context.Template.AddReplacement(
Tokens.RoleIdConstant,
roleNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.PermissionsValue,
CoreUtils.Format(
"(uint)({0})",
rolePermission.Permission.GetPermissionTypeAsCode()));
return context.Template.Render();
}
private void AddNodeStateClassVariableTypeReplacements(
IWriteContext context,
VariableTypeDesign variableType)
{
AddNodeStateClassTypeReplacements(context, variableType);
BasicDataType basicType = variableType.DataTypeNode.BasicDataType;
if (variableType.SymbolicName.Name == "TwoStateDiscreteType")
{
variableType.ValueRank = ValueRank.Scalar;
}
if (!variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
{
// Overrides a variable type that does not require a template parameter so we
// need to add it
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
}
else
{
// Overrides a typed variable type where the base class requires a template
// parameter, so the same template parameter is needed plus the Variant builder
// to access the variant content of the type T.
string parameter = GetTemplateParameter(variableType, out string variantBuilder);
if (parameter == "" && variableType.ValueRank != ValueRank.Scalar)
{
parameter = "";
variantBuilder = "global::Opc.Ua.VariantBuilder";
}
if (string.IsNullOrEmpty(parameter))
{
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
}
else
{
// The class extends from the generic one through its inner
// Implementation class to provide generic variant
// value accessors
context.Template.AddReplacement(
Tokens.BaseT,
CoreUtils.Format("{0}.Implementation<{1}>", parameter, variantBuilder));
}
}
string valueRank = variableType.ValueRank.GetValueRankAsCode(
variableType.ArrayDimensions);
if (variableType.ValueRank == ValueRank.ScalarOrArray)
{
for (TypeDesign baseType = variableType.BaseTypeNode;
baseType != null;
baseType = baseType.BaseTypeNode)
{
if (baseType.SymbolicId ==
new XmlQualifiedName("DataItemType", Namespaces.OpcUa))
{
valueRank = $"global::Opc.Ua.ValueRanks.{ValueRank.Scalar}";
}
}
}
AddVariantAccessor(context, variableType.DataTypeNode);
if (variableType.DataTypeNode.NeedsCloning())
{
context.Template.AddReplacement(
Tokens.ValueWrite,
CoreUtils.Format(
"CopyOnWrite ? ({0})global::Opc.Ua.CoreUtils.Clone(newValue) : newValue",
variableType.DataTypeNode.SymbolicName.Name));
}
else
{
context.Template.AddReplacement(Tokens.ValueWrite, "newValue");
}
if (variableType.DataTypeNode.IsDotNetEqualityComparable(variableType.ValueRank))
{
context.Template.AddReplacement(Tokens.ValueComparison, "m_value != newValue");
}
else
{
context.Template.AddReplacement(Tokens.ValueComparison,
"!global::Opc.Ua.CoreUtils.IsEqual(m_value, newValue)");
}
context.Template.AddReplacement(
Tokens.DefaultValue,
variableType.DataTypeNode.GetValueAsCode(
variableType.ValueRank,
variableType.DefaultValue,
variableType.DecodedValue,
true,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
m_messageContext,
() => AddXmlInitializerForComplexValue(
variableType,
variableType.DataTypeNode,
variableType.DefaultValue)));
context.Template.AddReplacement(Tokens.ValueRank, valueRank);
context.Template.AddReplacement(
Tokens.ArrayDimensions,
variableType.ValueRank.GetArrayDimensionsAsCode(
variableType.ArrayDimensions) ??
"default");
context.Template.AddReplacement(Tokens.IsAbstract, variableType.IsAbstract);
context.Template.AddReplacement(
Tokens.AccessLevelValue,
variableType.AccessLevel.GetAccessLevelAsCode());
context.Template.AddReplacement(
Tokens.MinimumSamplingIntervalValue,
variableType.GetMinimumSamplingIntervalAsCode());
context.Template.AddReplacement(
Tokens.Historizing,
variableType.Historizing);
context.Template.AddReplacement(
Tokens.DataType,
variableType.DataTypeNode.SymbolicName.Name);
context.Template.AddReplacement(
Tokens.DataTypeNamespacePrefix,
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
variableType.DataTypeNode.SymbolicId.Namespace));
context.Template.AddReplacement(
Tokens.DataTypeNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
variableType.DataTypeNode.SymbolicId.Namespace));
context.Template.AddReplacement(
Tokens.TypedVariableType,
NodeStateTemplates.VariableTypeWithTypedValue_Class,
[variableType],
LoadTemplate_TypedVariableType,
WriteTemplate_TypedVariableType);
context.Template.AddReplacement(
Tokens.VariableTypeValue,
NodeStateTemplates.VariableTypeValue_Class,
[variableType],
LoadTemplate_VariableTypeValue,
WriteTemplate_VariableTypeValue);
}
private void AddNodeStateClassObjectTypeReplacements(
IWriteContext context,
ObjectTypeDesign objectType)
{
AddNodeStateClassTypeReplacements(context, objectType);
context.Template.AddReplacement(Tokens.BaseT, string.Empty);
context.Template.AddReplacement(Tokens.IsAbstract, objectType.IsAbstract);
context.Template.AddReplacement(Tokens.EventNotifier,
objectType.GetEventNotifierAsCode());
}
private void AddNodeStateClassMethodTypeReplacements(
IWriteContext context,
MethodDesign method)
{
context.Template.AddReplacement(
Tokens.ClassName,
method.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
[]));
context.Template.AddReplacement(
Tokens.ListOfInputArguments,
method.InputArguments,
LoadTemplate_ListOfInputArguments);
context.Template.AddReplacement(
Tokens.OnCallDeclaration,
[method],
LoadTemplate_OnCallDeclaration);
context.Template.AddReplacement(
Tokens.OnCallAsyncDeclaration,
[method],
LoadTemplate_OnCallAsyncDeclaration);
context.Template.AddReplacement(
Tokens.OnCallImplementation,
[method],
LoadTemplate_OnCallImplementation);
context.Template.AddReplacement(
Tokens.OnCallAsyncImplementation,
[method],
LoadTemplate_OnCallAsyncImplementation);
context.Template.AddReplacement(
Tokens.ListOfOutputDeclarations,
method.OutputArguments,
LoadTemplate_ListOfOutputDeclarations);
context.Template.AddReplacement(
Tokens.ListOfOutputArgumentsFromResult,
method.OutputArguments,
LoadTemplate_ListOfOutputArgumentsFromResult);
context.Template.AddReplacement(
Tokens.ListOfOutputArguments,
method.OutputArguments,
LoadTemplate_ListOfOutputArguments);
context.Template.AddReplacement(
Tokens.ListOfResultProperties,
method.OutputArguments,
LoadTemplate_ListOfResultProperties);
}
private void AddNodeStateClassTypeReplacements(
IWriteContext context,
TypeDesign type)
{
context.Template.AddReplacement(Tokens.ClassName, type.ClassName);
context.Template.AddReplacement(
Tokens.BaseClassName,
type.BaseTypeNode.GetClassName(m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(
Tokens.BaseTypeNamespacePrefix,
m_context.ModelDesign.Namespaces.GetNamespacePrefix(
type.BaseTypeNode.SymbolicId.Namespace));
context.Template.AddReplacement(
Tokens.BaseTypeNamespaceUri,
m_context.ModelDesign.Namespaces.GetConstantSymbolForNamespace(
type.BaseTypeNode.SymbolicId.Namespace));
}
private void AddObjectTypeStateFactoryReplacements(
IWriteContext context,
ObjectTypeDesign node)
{
context.Template.AddReplacement(
Tokens.SuperTypeId,
node.BaseTypeNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
context.Template.AddReplacement(
Tokens.StateClassName,
"global::Opc.Ua.BaseObjectTypeState");
context.Template.AddReplacement(
Tokens.StateClassFactory,
"new global::Opc.Ua.BaseObjectTypeState");
}
private void AddVariableTypeStateFactoryReplacements(
IWriteContext context,
VariableTypeDesign node)
{
context.Template.AddReplacement(Tokens.SuperTypeId,
node.BaseTypeNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
context.Template.AddReplacement(
Tokens.StateClassName,
"global::Opc.Ua.BaseDataVariableTypeState");
context.Template.AddReplacement(
Tokens.StateClassFactory,
"new global::Opc.Ua.BaseDataVariableTypeState");
context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format(
"state.WrappedValue = {0};",
node.DataTypeNode.GetValueAsCode(
node.ValueRank,
node.DefaultValue,
node.DecodedValue,
true,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
m_messageContext,
() => AddXmlInitializerForComplexValue(
node,
node.DataTypeNode,
node.DefaultValue))));
string dataTypeId =
GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces);
context.Template.AddReplacement(
Tokens.DataTypeIdConstant,
dataTypeId ?? "global::Opc.Ua.NodeId.Null");
string valueRank = node.ValueRank.GetValueRankAsCode(
node.ArrayDimensions);
context.Template.AddReplacement(Tokens.ValueRank, valueRank);
string arrayDims = node.ValueRank.GetArrayDimensionsAsCode(
node.ArrayDimensions);
context.Template.AddReplacement(
Tokens.ArrayDimensions,
!string.IsNullOrEmpty(arrayDims)
? CoreUtils.Format("state.ArrayDimensions = {0};", arrayDims)
: null);
}
private void AddVariableStateFactoryReplacements(
IWriteContext context,
NodeToGenerate nodeToGenerate,
VariableDesign node,
HashSet references)
{
context.Template.AddReplacement(
Tokens.StateClassName,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(
Tokens.StateClassFactory,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
asFactory: true));
context.Template.AddReplacement(
Tokens.TypeDefinitionId,
node.TypeDefinitionNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.ReferenceTypeId,
m_context.ModelDesign.GetNodeIdConstant(
node.ReferenceType,
"",
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.ModellingRuleId,
GetModellingRuleReplacement(
GetEffectiveModellingRule(nodeToGenerate, node)));
context.Template.AddReplacement(
Tokens.DataTypeIdConstant,
GetNodeIdConstantForDataType(node, m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(
Tokens.ValueRank,
node.ValueRank.GetValueRankAsCode(node.ArrayDimensions));
string arrayDims = node.ValueRank.GetArrayDimensionsAsCode(node.ArrayDimensions);
context.Template.AddReplacement(
Tokens.ArrayDimensions,
!string.IsNullOrEmpty(arrayDims)
? CoreUtils.Format("state.ArrayDimensions = {0};", arrayDims)
: null);
context.Template.AddReplacement(
Tokens.AccessLevelValue,
node.AccessLevel.GetAccessLevelAsCode());
context.Template.AddReplacement(
Tokens.UserAccessLevelValue,
node.AccessLevel.GetAccessLevelAsCode());
context.Template.AddReplacement(
Tokens.MinimumSamplingIntervalValue,
node.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture));
context.Template.AddReplacement(
Tokens.HistorizingValue,
node.Historizing);
// set dictionary to point to embedded schemas
if (node.TypeDefinitionNode.SymbolicId ==
new XmlQualifiedName("DataTypeDictionaryType", Namespaces.OpcUa))
{
NodeDesign typeSystemReference = references.FirstOrDefault()?.TargetNode;
switch (typeSystemReference?.SymbolicId.Name)
{
case "XmlSchema_TypeSystem":
context.Template.AddReplacement(
Tokens.ValueCode,
"state.WrappedValue = global::Opc.Ua.Variant.From(global::Opc.Ua.ByteString.From(XmlSchemas.TypesXsd.ToArray()));");
return;
case "OPCBinarySchema_TypeSystem":
context.Template.AddReplacement(
Tokens.ValueCode,
"state.WrappedValue = global::Opc.Ua.Variant.From(global::Opc.Ua.ByteString.From(XmlSchemas.TypesBsd.ToArray()));");
return;
}
// unknown type system
context.Template.AddReplacement(
Tokens.ValueCode,
"state.WrappedValue = global::Opc.Ua.Variant.Null;");
return;
}
if (node.DecodedValue is IList args)
{
context.Template.AddReplacement(
Tokens.ValueCode,
NodeStateTemplates.VariantArrayOfValue,
[args],
WriteTemplate_ArgumentCollection);
}
else if (node.DecodedValue is ArrayOf arguments)
{
context.Template.AddReplacement(
Tokens.ValueCode,
NodeStateTemplates.VariantArrayOfValue,
[arguments.ToList()],
WriteTemplate_ArgumentCollection);
}
else
{
context.Template.AddReplacement(Tokens.ValueCode, CoreUtils.Format(
"state.WrappedValue = {0};",
node.DataTypeNode.GetValueAsCode(
node.ValueRank,
node.DefaultValue,
node.DecodedValue,
true,
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
m_messageContext,
() => AddXmlInitializerForComplexValue(
node,
node.DataTypeNode,
node.DefaultValue))));
}
}
private void AddReferenceTypeStateFactoryReplacements(
IWriteContext context,
ReferenceTypeDesign node)
{
context.Template.AddReplacement(Tokens.SuperTypeId,
node.BaseTypeNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
context.Template.AddReplacement(Tokens.SymmetricValue, node.Symmetric);
context.Template.AddReplacement(Tokens.InverseNameValue, GetInverseNameValue(node));
}
private void AddDataTypeStateFactoryReplacements(
IWriteContext context,
DataTypeDesign node)
{
context.Template.AddReplacement(Tokens.SuperTypeId,
node.BaseTypeNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(Tokens.IsAbstract, node.IsAbstract);
context.Template.AddReplacement(Tokens.Purpose,
(Export.DataTypePurpose)(int)(node.Purpose == DataTypePurpose.Testing ?
DataTypePurpose.CodeGenerator :
node.Purpose));
if (node.BasicDataType is not BasicDataType.Enumeration and not BasicDataType.UserDefined)
{
context.Template.AddReplacement(
Tokens.DataTypeDefinition,
"global::Opc.Ua.ExtensionObject.Null");
}
else
{
context.Template.AddReplacement(
Tokens.DataTypeDefinition,
CoreUtils.Format(
"new global::Opc.Ua.ExtensionObject({0}.DataTypeDefinitions.Create{1}({2}))",
m_context.ModelDesign.Namespaces.GetNamespacePrefix(node.SymbolicName.Namespace),
node.SymbolicName.Name,
kNamespaceTableContextVariable));
}
}
private void AddObjectReplacements(
IWriteContext context,
NodeToGenerate nodeToGenerate,
ObjectDesign node,
HashSet references)
{
context.Template.AddReplacement(
Tokens.StateClassName,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(
Tokens.StateClassFactory,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
asFactory: true));
context.Template.AddReplacement(
Tokens.TypeDefinitionId,
node.TypeDefinitionNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.ReferenceTypeId,
m_context.ModelDesign.GetNodeIdConstant(
node.ReferenceType,
"",
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.ModellingRuleId,
GetModellingRuleReplacement(
GetEffectiveModellingRule(nodeToGenerate, node)));
context.Template.AddReplacement(
Tokens.EventNotifier,
node.SupportsEvents || HasForwardEventReferences(references)
? "global::Opc.Ua.EventNotifiers.SubscribeToEvents"
: "global::Opc.Ua.EventNotifiers.None");
}
private void AddMethodStateFactoryReplacements(
IWriteContext context,
NodeToGenerate nodeToGenerate,
MethodDesign node)
{
context.Template.AddReplacement(
Tokens.ReferenceTypeId,
m_context.ModelDesign.GetNodeIdConstant(
node.ReferenceType,
"",
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.StateClassName,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces));
context.Template.AddReplacement(
Tokens.StateClassFactory,
node.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
asFactory: true));
context.Template.AddReplacement(
Tokens.ModellingRuleId,
GetModellingRuleReplacement(
GetEffectiveModellingRule(nodeToGenerate, node)));
bool executable = !node.NonExecutable;
context.Template.AddReplacement(Tokens.ExecutableValue, executable);
context.Template.AddReplacement(
Tokens.MethodDeclarationId,
node.MethodDeclarationNode != null
? CoreUtils.Format(
"state.MethodDeclarationId = {0};",
node.MethodDeclarationNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable))
: null);
}
private static void AddViewStateFactoryReplacements(
IWriteContext context,
ViewDesign node)
{
context.Template.AddReplacement(
Tokens.EventNotifier,
node.SupportsEvents
? "global::Opc.Ua.EventNotifiers.SubscribeToEvents"
: "global::Opc.Ua.EventNotifiers.None");
context.Template.AddReplacement(Tokens.ContainsNoLoopsValue, node.ContainsNoLoops);
}
///
/// Returns true if the references include a forward HasEventSource
/// (i=36) or HasNotifier (i=48) reference. Per OPC UA Part 3, the
/// EventNotifier attribute must be set on the source node of these
/// references to indicate that events can be subscribed to.
///
private static bool HasForwardEventReferences(HashSet references)
{
if (references == null)
{
return false;
}
foreach (ReferenceToGenerate reference in references)
{
if (!reference.IsInverse &&
reference.ReferenceTypeId != null &&
(reference.ReferenceTypeId.Name == "HasEventSource" ||
reference.ReferenceTypeId.Name == "HasNotifier"))
{
return true;
}
}
return false;
}
private void CollectNodesToGenerate()
{
foreach (NodeDesign node in m_context.ModelDesign.Nodes)
{
if (m_context.ModelDesign.IsExcluded(node))
{
continue;
}
NodeToGenerate entry = null;
if (node is not InstanceDesign)
{
entry = new NodeToGenerate(
Parent: null,
Path: string.Empty,
Hierarchy: node.Hierarchy,
Design: node,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: true,
InstanceOf: null);
}
else
{
entry = new NodeToGenerate(
Parent: null,
Path: string.Empty,
Hierarchy: node.Hierarchy,
Design: node.Hierarchy.NodeList[0].Instance,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: false,
InstanceOf: null);
}
if (!m_nodes.TryAdd(entry.Design.SymbolicId, entry))
{
m_logger.LogDebug(
"Removing duplicate entry for {Node}.",
entry.Design.SymbolicId.Name);
}
GetChildren(entry, m_nodes, false);
if ((node is not ObjectTypeDesign and not VariableTypeDesign) &&
!node.IsMethodTypeDesign())
{
continue;
}
// Add instances to generate
if (node.Hierarchy == null ||
!node.Hierarchy.Nodes.TryGetValue(
string.Empty,
out HierarchyNode hierarchyNode))
{
continue;
}
if (hierarchyNode.Identifier != null)
{
if (hierarchyNode.Identifier is uint numericId)
{
hierarchyNode.Instance.NumericId = numericId;
hierarchyNode.Instance.NumericIdSpecified = true;
}
else if (hierarchyNode.Identifier is string stringId)
{
hierarchyNode.Instance.StringId = stringId;
}
else
{
throw new InvalidOperationException(CoreUtils.Format(
"Invalid identifier {0}",
hierarchyNode.Identifier));
}
}
var instanceToGenerate = new NodeToGenerate(
Parent: null,
Path: string.Empty,
Hierarchy: node.Hierarchy,
Design: hierarchyNode.Instance,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: false,
InstanceOf: entry); // Mark as instance of a type design
entry.Instance = instanceToGenerate;
if (!m_instances.TryAdd(instanceToGenerate.Design.SymbolicId, instanceToGenerate))
{
m_logger.LogDebug(
"Removing duplicate entry for instance {Node}.",
instanceToGenerate.Design.SymbolicId.Name);
}
GetChildren(instanceToGenerate, m_instances, true);
}
// Collect DataType encoding nodes (Default Binary, Default XML, Default JSON).
// These are ObjectDesign instances with TypeDefinition=DataTypeEncodingType
// that are linked to DataTypes via HasEncoding references. They don't have
// a Hierarchy from the model validation, so we build a minimal one with
// their references to emit them into the address space.
CollectEncodingNodes();
}
private void CollectEncodingNodes()
{
foreach (NodeDesign node in m_context.ModelDesign.Nodes)
{
if (node is not ObjectDesign encoding)
{
continue;
}
if (encoding.TypeDefinition == null ||
encoding.TypeDefinition.Name != "DataTypeEncodingType")
{
continue;
}
if (encoding.NotInAddressSpace)
{
continue;
}
if (m_context.ModelDesign.IsExcluded(encoding))
{
continue;
}
// Skip if already collected
if (m_nodes.ContainsKey(encoding.SymbolicId))
{
continue;
}
// Build a synthetic Hierarchy with the encoding's references
var hierarchy = new Hierarchy();
if (encoding.References != null)
{
foreach (Reference reference in encoding.References)
{
if (reference.ReferenceType == null)
{
continue;
}
hierarchy.References.Add(new HierarchyReference
{
SourcePath = string.Empty,
ReferenceType = reference.ReferenceType,
IsInverse = reference.IsInverse,
TargetId = reference.TargetId,
TargetPath = null
});
}
}
encoding.Hierarchy = hierarchy;
var entry = new NodeToGenerate(
Parent: null,
Path: string.Empty,
Hierarchy: hierarchy,
Design: encoding,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: false,
InstanceOf: null);
m_nodes.TryAdd(encoding.SymbolicId, entry);
}
}
private bool ExcludeNodeStateClassGeneration(NodeToGenerate node)
{
// Filter unncessary nodes
if (!IsInAddressSpace(node))
{
m_logger.LogDebug(
"Excluded node {Node} as it is marked NotInAddressSpace.",
node.Design.SymbolicId.Name);
return true;
}
// Only process type designs and method types for definitions
// Also allow DataTypeEncodingType instances (Default Binary/XML/JSON)
if (node.Design is not VariableTypeDesign and not ObjectTypeDesign &&
!node.Design.IsMethodTypeDesign() &&
!IsDataTypeEncodingInstance(node.Design))
{
return true;
}
if (node.Design.SymbolicName.Namespace == Namespaces.OpcUa &&
node.Design.NumericId < 256)
{
switch (node.Design.SymbolicName.Name)
{
case "DataTypeDictionaryType":
case "DataTypeDescriptionType":
case "DataTypeSystemType":
case "DataTypeEncodingType":
case "ModellingRuleType":
break;
default:
m_logger.LogDebug(
"Skipping built-in node state class generation for {Node}.",
node.Design.SymbolicId.Name);
return true;
}
}
return false;
}
private static bool IsInAddressSpace(NodeToGenerate node)
{
bool isInAddressSpace = !node.Design.NotInAddressSpace;
if (node.Design is InstanceDesign instanceDesign &&
instanceDesign.TypeDefinition != null &&
instanceDesign.TypeDefinition.Name == "DataTypeEncodingType")
{
isInAddressSpace =
instanceDesign.Parent == null ||
!instanceDesign.Parent.NotInAddressSpace;
}
return isInAddressSpace;
}
private static bool IsDataTypeEncodingInstance(NodeDesign node)
{
return node is ObjectDesign objectDesign &&
objectDesign.TypeDefinition != null &&
objectDesign.TypeDefinition.Name == "DataTypeEncodingType";
}
///
/// Checks if the instance is a built in property of a base node state
/// implementation (variable/method) that should not be generated because
/// it is already defined on the root nodestate class.
///
private static bool IsBuiltInProperty(NodeToGenerate node)
{
switch (node.Parent?.Design)
{
case MethodDesign:
if (node.Design.SymbolicName ==
new XmlQualifiedName("InputArguments", Namespaces.OpcUa))
{
return true;
}
if (node.Design.SymbolicName ==
new XmlQualifiedName("OutputArguments", Namespaces.OpcUa))
{
return true;
}
break;
case VariableDesign:
case VariableTypeDesign:
if (node.Design.SymbolicName ==
new XmlQualifiedName("EnumStrings", Namespaces.OpcUa))
{
return true;
}
break;
}
return false;
}
private static bool HasChildDefined(TypeDesign typeDefinitionNode, string symbolicName)
{
if (typeDefinitionNode == null)
{
return false;
}
if (typeDefinitionNode.Children?.Items != null)
{
foreach (InstanceDesign child in typeDefinitionNode.Children.Items)
{
if (child.SymbolicName.Name == symbolicName)
{
return true;
}
}
}
return HasChildDefined(typeDefinitionNode.BaseTypeNode, symbolicName);
}
///
/// Returns the effective modelling rule for a .
/// When
/// is enabled (opt-in) and the node belongs to an instance hierarchy,
/// the type definition's modelling rule is used unconditionally.
/// When the option is off (default), the instance may only
/// promote the type-definition rule per OPC UA rules:
///
/// - Optional → Mandatory
/// - OptionalPlaceholder → Mandatory | MandatoryPlaceholder
/// - MandatoryPlaceholder → Mandatory
///
/// Any other instance override (demotion) is ignored and the
/// type-definition rule is returned instead.
///
private ModellingRule GetEffectiveModellingRule(
NodeToGenerate node,
InstanceDesign instance)
{
if (!node.RootIsTypeDefinition &&
node.TypeDefinitionModellingRule.HasValue)
{
ModellingRule typeDefRule = node.TypeDefinitionModellingRule.Value;
if (m_context.Options.UseTypeDefinitionModellingRules)
{
return typeDefRule;
}
// When not opt-in, only allow valid promotions per
// the OPC UA modelling rule standard.
if (!IsValidPromotion(typeDefRule, instance.ModellingRule))
{
return typeDefRule;
}
}
return instance.ModellingRule;
}
///
/// Returns the effective modelling rule for a hierarchy node.
/// Overload used in where the hierarchy
/// node and root-is-type-definition flag are available directly.
///
private ModellingRule GetEffectiveModellingRule(
HierarchyNode hierarchyNode,
InstanceDesign child,
bool rootIsTypeDefinition)
{
if (!rootIsTypeDefinition &&
hierarchyNode.TypeDefinitionModellingRule.HasValue)
{
ModellingRule typeDefRule =
hierarchyNode.TypeDefinitionModellingRule.Value;
if (m_context.Options.UseTypeDefinitionModellingRules)
{
return typeDefRule;
}
// When not opt-in, only allow valid promotions per
// the OPC UA modelling rule standard.
if (!IsValidPromotion(typeDefRule, child.ModellingRule))
{
return typeDefRule;
}
}
return child.ModellingRule;
}
///
/// Determines whether the instance modelling rule is a valid
/// promotion of the type-definition modelling rule per OPC UA:
///
/// - Optional → Mandatory
/// - OptionalPlaceholder → Mandatory | MandatoryPlaceholder
/// - MandatoryPlaceholder → Mandatory
///
/// Returns true when the instance rule is an allowed
/// promotion (or identical to the type-definition rule), and
/// false when it is a demotion that should be rejected.
///
private static bool IsValidPromotion(
ModellingRule typeDefRule,
ModellingRule instanceRule)
{
if (instanceRule == typeDefRule)
{
return true;
}
return typeDefRule switch
{
ModellingRule.Optional =>
instanceRule == ModellingRule.Mandatory,
ModellingRule.OptionalPlaceholder =>
instanceRule is ModellingRule.Mandatory
or ModellingRule.MandatoryPlaceholder,
ModellingRule.MandatoryPlaceholder =>
instanceRule == ModellingRule.Mandatory,
// Mandatory, None, ExposesItsArray, etc. cannot be promoted
_ => false
};
}
private void GetChildren(
NodeToGenerate node,
Dictionary children,
bool forInstance)
{
if (!forInstance)
{
if (node.Design.Children?.Items != null)
{
foreach (InstanceDesign child in node.Design.Children.Items)
{
if (m_context.ModelDesign.IsExcluded(child))
{
continue;
}
string childPath = child.SymbolicName.Name;
if (!string.IsNullOrEmpty(node.Path))
{
childPath = CoreUtils.Format(
"{0}{1}{2}",
node.Path,
NodeDesign.PathChar,
childPath);
}
node.Children[child.SymbolicName.Name] = new NodeToGenerate(
node,
childPath,
node.Hierarchy,
child,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: node.RootIsTypeDefinition,
InstanceOf: null);
}
}
}
if (node.Hierarchy == null)
{
return;
}
foreach (HierarchyNode current in node.Hierarchy.NodeList)
{
string childPath = current.RelativePath;
// only looking for nodes in the current tree.
if (!childPath.StartsWith(node.Path, StringComparison.Ordinal))
{
continue;
}
// ignore reference to the current base node.
if (childPath == node.Path)
{
continue;
}
// relative should always end in the name of the current instance.
if (!childPath.EndsWith(
current.Instance.SymbolicName.Name,
StringComparison.Ordinal))
{
continue;
}
// get the parent path.
if (childPath.Length <= current.Instance.SymbolicName.Name.Length)
{
if (!string.IsNullOrEmpty(node.Path))
{
continue;
}
}
else
{
int idx = childPath.Length - current.Instance.SymbolicName.Name.Length - 1;
string parentPath = current.RelativePath[..idx];
if (parentPath != node.Path)
{
continue;
}
}
if (!string.IsNullOrEmpty(node.Path))
{
childPath = childPath[(node.Path.Length + 1)..];
childPath = CoreUtils.Format(
"{0}{1}{2}",
node.Path,
NodeDesign.PathChar,
childPath);
}
if (current.Instance is not InstanceDesign child)
{
continue;
}
ModellingRule effectiveRule = GetEffectiveModellingRule(
current, child, node.RootIsTypeDefinition);
if (!node.RootIsTypeDefinition &&
!current.ExplicitlyDefined &&
effectiveRule != ModellingRule.Mandatory &&
effectiveRule != ModellingRule.Optional)
{
continue;
}
if (!current.ExplicitlyDefined &&
effectiveRule != ModellingRule.Mandatory &&
effectiveRule != ModellingRule.None &&
effectiveRule != ModellingRule.ExposesItsArray &&
effectiveRule != ModellingRule.OptionalPlaceholder &&
effectiveRule != ModellingRule.MandatoryPlaceholder)
{
continue;
}
if (node.RootIsTypeDefinition &&
!current.ExplicitlyDefined &&
current.Inherited &&
current.AdHocInstance)
{
// this assumes that ad-hoc instances are not more than one level deep.
// i.e. a type defines folder and adds a few instances but does not
// defined subfolders.
// need a better way to identify when to suppress inherited adhoc instances.
if (!node.Path.Contains(NodeDesign.PathChar, StringComparison.Ordinal))
{
continue;
}
}
bool add = false;
if (node.Design is DataTypeDesign or ViewDesign or ReferenceTypeDesign)
{
add = true;
}
else if (node.RootIsTypeDefinition)
{
if (effectiveRule == ModellingRule.Mandatory)
{
add = true;
}
else if (current.ExplicitlyDefined &&
effectiveRule == ModellingRule.Optional)
{
add = true;
}
else if (current.ExplicitlyDefined &&
(effectiveRule is
ModellingRule.ExposesItsArray or
ModellingRule.OptionalPlaceholder or
ModellingRule.MandatoryPlaceholder))
{
add = true;
}
else if (current.StaticValue && !current.Inherited)
{
add = true;
}
else if (effectiveRule == ModellingRule.None &&
current.ExplicitlyDefined)
{
// Include ModellingRule=None children that are explicitly defined
// on this type (e.g., state machine states/transitions, type-level
// methods like ConditionRefresh, and type-level properties like
// Creatable or SupportsFilteredRetain).
add = true;
}
else if (effectiveRule is not ModellingRule.None)
{
m_logger.LogDebug(
"Excluding child node {Node} from generation.",
current.Instance.SymbolicId.Name);
}
}
else if (effectiveRule == ModellingRule.Mandatory)
{
add = true;
}
else if (current.ExplicitlyDefined)
{
add = true;
}
else
{
m_logger.LogInformation(
"Excluding child node {Node} from generation.",
current.Instance.SymbolicId.Name);
}
if (add)
{
XmlQualifiedName symbolicId = current.Instance.SymbolicId;
var childNodeToGenerate = new NodeToGenerate(
node,
childPath,
node.Hierarchy,
child,
IsNotExplicitlyDefined: !current.ExplicitlyDefined,
RootIsTypeDefinition: node.RootIsTypeDefinition,
InstanceOf: null,
TypeDefinitionModellingRule:
current.TypeDefinitionModellingRule);
if (!children.TryAdd(symbolicId, childNodeToGenerate))
{
m_logger.LogInformation(
"Removing duplicate entry for child {Node}.",
symbolicId.Name);
}
node.AllChildren.Add(childNodeToGenerate);
GetChildren(childNodeToGenerate, children, forInstance);
}
}
}
private HashSet GetReferences(NodeToGenerate node)
{
if (node.Hierarchy == null)
{
return [];
}
Hierarchy hierarchy = node.Hierarchy;
NodeDesign root = node.Design;
HashSet references = [];
foreach (HierarchyReference reference in hierarchy.References)
{
if (reference.SourcePath != node.Path &&
reference.TargetPath != node.Path)
{
continue;
}
bool isInverse = reference.IsInverse;
if (reference.TargetId != null)
{
if (!m_context.ModelDesign.TryFindNode(
reference.TargetId,
root.SymbolicId.Name,
reference.ReferenceType.Name,
out NodeDesign targetNode))
{
continue;
}
if (!node.RootIsTypeDefinition &&
targetNode is InstanceDesign instance &&
(instance.ModellingRule is
ModellingRule.MandatoryPlaceholder or
ModellingRule.OptionalPlaceholder))
{
continue;
}
references.Add(new ReferenceToGenerate(
targetNode,
reference.ReferenceType,
isInverse));
continue;
}
if (reference.TargetPath != null &&
reference.TargetPath.Length == 0 &&
node.Parent?.Design != null)
{
references.Add(new ReferenceToGenerate(
node.Parent.Design,
reference.ReferenceType,
isInverse));
continue;
}
if (reference.SourcePath == node.Path)
{
if (!hierarchy.Nodes.TryGetValue(
reference.TargetPath,
out HierarchyNode target))
{
continue;
}
if (!target.ExplicitlyDefined && node.RootIsTypeDefinition)
{
continue;
}
references.Add(new ReferenceToGenerate(
target.Instance,
reference.ReferenceType,
isInverse));
continue;
}
if (!hierarchy.Nodes.TryGetValue(reference.SourcePath, out HierarchyNode source))
{
continue;
}
if (!source.ExplicitlyDefined && node.RootIsTypeDefinition)
{
continue;
}
references.Add(new ReferenceToGenerate(
source.Instance,
reference.ReferenceType,
!isInverse));
}
return references;
}
private List GetAdditionalChildren(NodeToGenerate node)
{
List additionalChildren = [];
foreach (NodeToGenerate child in node.Children.Values)
{
if (child.Design is not InstanceDesign instance)
{
continue;
}
if (child.IsNotExplicitlyDefined)
{
continue;
}
if (GetEffectiveModellingRule(child, instance) is
not ModellingRule.Mandatory and
not ModellingRule.Optional)
{
continue;
}
if (instance.IsOverridden())
{
continue;
}
additionalChildren.Add(instance);
}
return additionalChildren;
}
private List GetChildrenWithProperties(
NodeToGenerate node,
bool excludeBuiltIn = false)
{
List childrenWithProperties = [];
foreach (NodeToGenerate child in node.Children.Values)
{
if (child.Design is not InstanceDesign instance)
{
continue;
}
if (child.IsNotExplicitlyDefined)
{
continue;
}
if (GetEffectiveModellingRule(child, instance) is
not ModellingRule.Mandatory and
not ModellingRule.Optional)
{
continue;
}
if (instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces))
{
continue;
}
if (excludeBuiltIn && IsBuiltInProperty(child))
{
continue;
}
childrenWithProperties.Add(instance);
}
return childrenWithProperties;
}
private HashSet GetRolePermissions(NodeDesign node)
{
var rolePermissions = new HashSet();
// Emit RolePermissions on all nodes (type and instance).
// Type hierarchy nodes carry permissions as metadata but the server
// bypasses enforcement via IsPartOfTypeHierarchy at runtime.
RolePermission[] nodeRolePermissions =
(node.RolePermissions?.RolePermission) ?? (node.DefaultRolePermissions?.RolePermission);
if (nodeRolePermissions != null)
{
foreach (RolePermission rp in nodeRolePermissions)
{
ObjectDesign roleNode = m_context.ModelDesign.FindNode(
rp.Role,
rp.Role.Name,
"RoleType");
if (roleNode != null)
{
rolePermissions.Add(rp);
}
}
}
return rolePermissions;
}
private static string GetDisplayNameValue(NodeDesign node)
{
if (node.DisplayName != null)
{
return CoreUtils.Format(
"{0}, string.Empty, {1}",
node.DisplayName.Key.AsStringLiteral(),
(node.DisplayName.Value?.Trim()).AsStringLiteral());
}
return null;
}
private static string GetDescriptionValue(NodeDesign node)
{
if (node.Description != null && !node.Description.IsAutogenerated)
{
return CoreUtils.Format(
"state.Description = {0};",
node.Description.GetLocalizedTextAsCode());
}
return null;
}
private static string GetInverseNameValue(ReferenceTypeDesign node)
{
if (!node.Symmetric && node.InverseName != null)
{
return CoreUtils.Format(
"state.InverseName = {0};",
node.InverseName.GetLocalizedTextAsCode());
}
if (node.Symmetric)
{
return "state.InverseName = global::Opc.Ua.LocalizedText.Null;";
}
return null;
}
private static string GetModellingRuleReplacement(ModellingRule modellingRule)
{
string constant = modellingRule switch
{
ModellingRule.Mandatory => "global::Opc.Ua.Objects.ModellingRule_Mandatory",
ModellingRule.Optional => "global::Opc.Ua.Objects.ModellingRule_Optional",
ModellingRule.MandatoryPlaceholder => "global::Opc.Ua.Objects.ModellingRule_MandatoryPlaceholder",
ModellingRule.OptionalPlaceholder => "global::Opc.Ua.Objects.ModellingRule_OptionalPlaceholder",
ModellingRule.ExposesItsArray => "global::Opc.Ua.Objects.ModellingRule_ExposesItsArray",
_ => null
};
return constant != null
? CoreUtils.Format("state.ModellingRuleId = new global::Opc.Ua.NodeId({0});", constant)
: null;
}
private string GetNodeIdConstantForDataType(
VariableTypeDesign type,
Namespace[] namespaceUris)
{
if (!m_context.ModelDesign.UseAllowSubtypes)
{
DataTypeDesign dataType = m_context.ModelDesign.FindNode(
type.DataType,
type.SymbolicId.Name,
"DataType");
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
return type.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
private string GetNodeIdConstantForDataType(
VariableDesign instance,
Namespace[] namespaceUris)
{
if (!m_context.ModelDesign.UseAllowSubtypes)
{
DataTypeDesign dataType = m_context.ModelDesign.FindNode(
instance.DataType,
instance.SymbolicId.Name,
"DataType");
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
return instance.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
private string GetTemplateParameter(TypeDesign type, out string variantBuilder)
{
variantBuilder = "global::Opc.Ua.VariantBuilder";
if (type is not VariableTypeDesign variableType)
{
return string.Empty;
}
if (type.BaseTypeNode == null)
{
return "";
}
if (GetTemplateParameter(type.BaseTypeNode, out variantBuilder) != "")
{
return string.Empty;
}
BasicDataType basicType = variableType.DataTypeNode.BasicDataType;
if (basicType == BasicDataType.BaseDataType) // == Variant, so any
{
return "";
}
string scalarName;
switch (basicType)
{
case BasicDataType.UserDefined:
scalarName = variableType.DataTypeNode.GetClassName(
m_context.ModelDesign.Namespaces);
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
break;
default:
scalarName = variableType.DataTypeNode.GetDotNetTypeName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
nullable: NullableAnnotation.NonNullable,
true);
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
break;
}
if (variableType.ValueRank == ValueRank.Scalar)
{
return $"<{scalarName}>";
}
if (variableType.ValueRank == ValueRank.Array)
{
return $">";
}
if (variableType.ValueRank == ValueRank.OneOrMoreDimensions)
{
// TODO: matrixOf;
}
variantBuilder = "global::Opc.Ua.VariantBuilder";
return "";
}
private static void CollectMatchingFields(
VariableTypeDesign variableType,
Dictionary fields)
{
CollectFields(
variableType.DataTypeNode,
variableType.ValueRank,
string.Empty,
fields);
List availablePaths = [.. fields.Keys];
for (int ii = 0; ii < availablePaths.Count; ii++)
{
if (!variableType.Hierarchy.Nodes.ContainsKey(availablePaths[ii]))
{
fields.Remove(availablePaths[ii]);
}
}
}
private static void CollectFields(
DataTypeDesign dataType,
ValueRank valueRank,
string basePath,
Dictionary fields)
{
if (dataType.BasicDataType != BasicDataType.UserDefined ||
valueRank != ValueRank.Scalar)
{
return;
}
for (DataTypeDesign parent = dataType;
parent != null;
parent = parent.BaseTypeNode as DataTypeDesign)
{
if (parent.Fields != null)
{
for (int ii = 0; ii < parent.Fields.Length; ii++)
{
Parameter parameter = parent.Fields[ii];
string fieldPath = parameter.Name;
if (!string.IsNullOrEmpty(basePath))
{
fieldPath = CoreUtils.Format("{0}_{1}", basePath, parameter.Name);
}
fields[fieldPath] = parameter;
}
}
}
}
private string AddXmlInitializerForComplexValue(
NodeDesign node,
DataTypeDesign dataType,
System.Xml.XmlElement element)
{
string xml = element?.OuterXml;
if (string.IsNullOrEmpty(xml))
{
return null;
}
string resourceName = CoreUtils.Format("Values.{0}", node.SymbolicId.Name);
string uniqueName = resourceName;
for (int i = 0; i < 1000; i++)
{
if (m_initializers.TryAdd(uniqueName, new TextResource(uniqueName, xml)))
{
// Get code to create the variant from the XML resource reference
return CoreUtils.Format(
"global::Opc.Ua.Variant.FromXml({0}AsStream, context)",
uniqueName);
}
uniqueName = resourceName + i;
}
throw new InvalidOperationException("Unexpected duplicate resource names");
}
private static void AddVariantAccessor(IWriteContext context, DataTypeDesign dataType)
{
switch (dataType.BasicDataType)
{
case BasicDataType.UserDefined when !dataType.IsEnumeration:
context.Template.AddReplacement(Tokens.VariantFrom, "FromStructure");
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetStructure");
break;
default:
context.Template.AddReplacement(Tokens.VariantFrom, "From");
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetValue");
break;
}
}
///
/// Embed all initializers as source code
///
private Resource EmbedInitializers()
{
if (m_initializers.Count == 0)
{
return null;
}
var initializers = new ResourceGenerator(m_context);
return initializers.Embed(
m_context.ModelDesign.TargetNamespace.Prefix,
"NodeStates.i",
internalAccess: true,
[.. m_initializers.Values]);
}
///
/// Node state generation template
///
private record class NodeToGenerate(
NodeToGenerate Parent = null,
string Path = null,
Hierarchy Hierarchy = null,
NodeDesign Design = null,
bool IsNotExplicitlyDefined = false,
bool RootIsTypeDefinition = false,
NodeToGenerate InstanceOf = null,
ModellingRule? TypeDefinitionModellingRule = null)
{
///
/// Full inherited list of children
///
public List AllChildren { get; } = [];
///
/// Direclty defined
///
public Dictionary Children { get; } = [];
public NodeToGenerate Instance { get; set; }
///
public override string ToString()
{
return CoreUtils.Format("{0} (Parent: {1})", Design, Parent);
}
}
private record class ReferenceToGenerate(
NodeDesign TargetNode,
XmlQualifiedName ReferenceTypeId,
bool IsInverse);
private const string kNamespaceTableContextVariable = "context.NamespaceUris";
private static readonly string[] s_builtInPropertyNames =
[
"Description",
"Save",
"Handle",
"Specification",
"Update",
"Delete"
];
private static readonly string[] s_builtInMethodNames =
[
"Child"
];
private readonly Dictionary m_initializers = [];
private readonly Dictionary m_nodes = [];
private readonly Dictionary m_instances = [];
private readonly ILogger m_logger;
private readonly IServiceMessageContext m_messageContext;
private readonly IGeneratorContext m_context;
}
}