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