Files
Wolfgang Colsman 2de64d7d93 Source generator: keep NodeSet2 references to nodes of referenced models (#4796)
# Description

A reference in a NodeSet2 input whose target is a node of another model
was
dropped without a diagnostic when that model is supplied by a referenced
assembly rather than by an AdditionalFiles input. The dependency
metadata
of a referenced assembly carries types only, so the importer could not
resolve an instance such as DI's DeviceSet and skipped every reference
to
it: a device organized by DeviceSet (inverse Organizes) lost that
reference, and so did any non-hierarchical reference to such a node.

When the target of a reference is in another, non-zero namespace and no
loaded design defines it, the importer now creates a stand-in that
carries
only the NodeId the generated AddReference call needs. The stand-in is
not
generated. References whose reference type is unknown are still skipped.

Tests generate a DI-style NodeSet2 whose device is organized by
DeviceSet
against DI supplied as a referenced assembly and as an AdditionalFile,
and
compile the result against the stack.

## Related Issues

None.

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-16 20:09:14 +02:00

5155 lines
210 KiB
C#

/* ========================================================================
* 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
{
/// <summary>
/// Generates data types and node state classes.
/// </summary>
internal sealed class NodeStateGenerator : IGenerator
{
/// <summary>
/// Create node state generator
/// </summary>
public NodeStateGenerator(IGeneratorContext context)
{
m_context = context ?? throw new ArgumentNullException(nameof(context));
m_messageContext = ServiceMessageContext.CreateEmpty(context.Telemetry);
m_logger = context.Telemetry.CreateLogger<NodeStateGenerator>();
CollectNodesToGenerate();
}
/// <summary>
/// When <c>true</c>, emits the model's public NodeSet import factory
/// provider. Only set for models whose consumer references
/// <c>Opc.Ua.Server</c>.
/// </summary>
public bool GenerateNodeSetImportSupport { get; init; }
/// <summary>
/// When <c>true</c>, 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 <c>Opc.Ua.Server</c> contract that a
/// model-only assembly cannot reference, so it has to be emitted
/// beside the manager rather than beside the model.
/// </summary>
public bool ImportSupportOnly { get; init; }
/// <inheritdoc/>
public IEnumerable<Resource> Emit()
{
m_initializers.Clear();
if (ImportSupportOnly)
{
return GenerateNodeSetImportSupport
? [EmitNodeSetImportSupport()]
: [];
}
if (m_instances.Count + m_nodes.Count == 0)
{
return GenerateNodeSetImportSupport
? [EmitNodeSetImportSupport()]
: [];
}
List<Resource> resources =
[
EmitNodeStateClasses(),
EmitExtensions()
];
if (GenerateNodeSetImportSupport)
{
resources.Add(EmitNodeSetImportSupport());
}
Resource? initializers = EmbedInitializers();
if (initializers != null)
{
resources.Add(initializers);
}
return resources;
}
/// <summary>
/// Create the nodestates classes
/// </summary>
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();
}
/// <summary>
/// Create extensions
/// </summary>
/// <summary>
/// Create the model's NodeSet import factory provider
/// </summary>
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<ImportFactoryRegistration> registrations =
GetNodeSetImportFactoryRegistrations();
writer.WriteLine(" /// <summary>");
writer.WriteLine(
" /// Public registration provider for typed NodeSet imports.");
writer.WriteLine(" /// </summary>");
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(" /// <summary>");
writer.WriteLine(" /// Initializes a provider.");
writer.WriteLine(" /// </summary>");
writer.WriteLine(" public {0}()", className);
writer.WriteLine(" {");
writer.WriteLine(" }");
writer.WriteLine();
writer.WriteLine(" /// <summary>");
writer.WriteLine(" /// Gets the shared provider instance.");
writer.WriteLine(" /// </summary>");
writer.WriteLine(
" public static {0} Instance {{ get; }} = new {0}();",
className);
writer.WriteLine();
writer.WriteLine(" /// <inheritdoc/>");
writer.WriteLine(
" public global::Opc.Ua.ArrayOf<global::Opc.Ua.Server.Nodes.INodeSetImportFactory>");
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<ImportFactoryRegistration>
GetNodeSetImportFactoryRegistrations()
{
var registrations = new Dictionary<string, ImportFactoryRegistration>(
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;
}
/// <summary>
/// Gets whether a node belongs to a declaration - a type definition or
/// a method type - rather than to the model's address space.
/// </summary>
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<InstanceDesign> 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();
}
/// <summary>
/// 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.
/// </summary>
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<string, Parameter> 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<string, Parameter> 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<string, Parameter> 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<string, Parameter> 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<string, Parameter> 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<InstanceDesign> childrenWithProperties = GetChildrenWithProperties(node);
context.Template.AddReplacement(
Tokens.ListOfFindChildCase,
NodeStateTemplates.FindChildCase,
childrenWithProperties,
WriteTemplate_ListOfChildren);
context.Template.AddReplacement(
Tokens.ListOfCreateOrReplaceChild,
NodeStateTemplates.CreateOrReplaceChild,
childrenWithProperties,
WriteTemplate_ListOfChildren);
List<InstanceDesign> 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 <opc:BrowseName> 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<ReferenceToGenerate> 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<RolePermission> 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,
"<ReferenceType>",
kNamespaceTableContextVariable),
reference.IsInverse ? "true" : "false",
reference.TargetNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
if (placeholder)
{
context.Out.WriteLine("}");
}
return null;
}
/// <summary>
/// Whether <see cref="GeneratorOptions.EmitNodeSetNodesOnly"/> applies:
/// the option is set and the target model was read from a NodeSet2.
/// </summary>
private bool EmitsNodeSetNodesOnly =>
m_context.Options.EmitNodeSetNodesOnly &&
m_context.ModelDesign is ModelDesignValidator { IsSourceNodeSet: true };
/// <summary>
/// With <see cref="GeneratorOptions.EmitNodeSetNodesOnly"/>, 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.
/// <para>
/// The child factories of a type are also called by the
/// <c>CreateInstanceOf*</c> factories with <c>forInstance: true</c>, 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.
/// </para>
/// </summary>
private bool IsOmittedFromNodeSetAddressSpace(NodeToGenerate node)
{
return EmitsNodeSetNodesOnly &&
node.IsNotExplicitlyDefined &&
GetRoot(node).InstanceOf == null;
}
/// <summary>
/// Whether the predefined instance that <paramref name="node"/> 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
/// <c>CreateInstanceOf*</c> factories reuse at run time.
/// </summary>
private bool OmitsUndeclaredHierarchyNodes(NodeToGenerate node)
{
return EmitsNodeSetNodesOnly &&
!node.RootIsTypeDefinition &&
GetRoot(node).InstanceOf == null;
}
/// <summary>
/// Whether the predefined instance that <paramref name="node"/> belongs
/// to creates the node at <paramref name="path"/> of its hierarchy: the
/// node is generated and neither it nor any of its ancestors is
/// omitted (see <see cref="IsOmittedFromNodeSetAddressSpace"/>).
/// </summary>
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 <opc:Object> 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
// <see cref="Opc.Ua.Server.ConfigurationNodeManager"/>,
// RoleStateBinding looks up
// <see cref="Opc.Ua.ObjectIds.Server_ServerCapabilities_RoleSet"/>).
// 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
// <see cref="Opc.Ua.Server.Diagnostics.DiagnosticsNodeManager.
// AddOperationLimitsSdkOptionalChildren"/>).
//
// 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.
// <opc:Object SymbolicName="ServerConfiguration" ...> 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
// <see cref="GeneratorOptions.UseTypeDefinitionModellingRules"/>
// 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 <opc:Object>/<opc:Variable> 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;
}
/// <summary>
/// Emits the call that creates <paramref name="node"/> and adds it to
/// the <c>state</c> of its parent's factory.
/// </summary>
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);
}
/// <summary>
/// Emits the <c>state.CreateOrReplace{Name}(context, Create{Symbolic}(context, state, forInstance: …))</c>
/// call site for a child of an InstanceDesign parent. When the child
/// lives in a type-level factory body (<see cref="NodeToGenerate.RootIsTypeDefinition"/>
/// is <c>true</c>) AND the type has well-known singleton instances
/// whose corresponding child factories have been collected, also
/// dispatches the <c>forInstance: true</c> path into the appropriate
/// singleton-instance child factory. This ensures that lazy-added
/// methods on singleton owners (e.g.
/// <c>serverObject.AddGetMonitoredItems(context, MethodIds.Server_GetMonitoredItems)</c>)
/// produce synthesized children (<c>InputArguments</c>,
/// <c>OutputArguments</c>) at their well-known instance NodeIds
/// rather than the type-level placeholders.
/// </summary>
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
// <paramref name="forInstance"/> 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<RolePermission> 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<Argument> 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,
"<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 == "<T>" && variableType.ValueRank != ValueRank.Scalar)
{
parameter = "<global::Opc.Ua.Variant>";
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<TBuilder> 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));
}
/// <summary>
/// Emits the <c>ElementNamespaceUri</c> override for
/// <c>FiniteStateMachineType</c> subtypes. Part 5 qualifies the
/// numeric state and transition NodeIds a machine reports in
/// <c>CurrentState/Id</c> and <c>LastTransition/Id</c> 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 <c>null</c> (collapsing the
/// template line) for every other object type.
/// </summary>
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 <Namespaces> to fix this.",
namespaceUri,
objectType.SymbolicName?.Name);
return null;
}
return CoreUtils.Format(
"/// <inheritdoc/>{0}" +
" protected override string ElementNamespaceUri => {1};",
Environment.NewLine,
constant);
}
/// <summary>
/// The namespace of the nearest type in the hierarchy that
/// actually declares <c>StateType</c> / <c>TransitionType</c>
/// children. A subtype that only adds behaviour inherits its
/// elements — and therefore their namespace — from its base.
/// </summary>
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;
}
/// <summary>
/// Whether the type derives, however indirectly, from the
/// standard <c>FiniteStateMachineType</c>.
/// </summary>
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");
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<ReferenceToGenerate> 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,
"<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<Argument> args)
{
context.Template.AddReplacement(
Tokens.ValueCode,
NodeStateTemplates.VariantArrayOfValue,
[args],
WriteTemplate_ArgumentCollection);
}
else if (node.DecodedValue is ArrayOf<Argument> arguments)
{
context.Template.AddReplacement(
Tokens.ValueCode,
NodeStateTemplates.VariantArrayOfValue,
[arguments.ToList()],
WriteTemplate_ArgumentCollection);
}
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)));
}
}
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<ReferenceToGenerate> 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,
"<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,
"<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);
}
/// <summary>
/// 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 -
/// <see cref="MethodState.CreateOrReplaceInputArguments"/> 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
/// <see cref="RequiresArgumentValueOverride"/>). When no child factory
/// exists the whole property is created inline.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
private static bool RequiresArgumentValueOverride(Parameter[] arguments)
{
return Array.Exists(
arguments,
argument => argument.DataTypeNode != null &&
!string.Equals(
argument.DataTypeNode.SymbolicId.Namespace,
Namespaces.OpcUa,
StringComparison.Ordinal));
}
/// <summary>
/// The declaration node of a method is only usable as
/// <see cref="MethodState.MethodDeclarationId"/> 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.
/// </summary>
private static bool HasResolvableNodeId(NodeDesign node)
{
return node.NumericIdSpecified ||
!string.IsNullOrEmpty(node.StringId) ||
node.HasNonConstantIdentifier() ||
node.FindNumericIdentifier().HasValue;
}
/// <summary>
/// Indicates that the model generates a dedicated child node factory
/// for the InputArguments or OutputArguments property of a method.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
private static bool HasForwardEventReferences(HashSet<ReferenceToGenerate> 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();
}
/// <summary>
/// Populates <see cref="m_singletonsByType"/> with every singleton
/// instance root or nested well-known instance in
/// <see cref="m_nodes"/> grouped by its
/// <see cref="InstanceDesign.TypeDefinitionNode"/>'s SymbolicId.
/// Top-level entries correspond to declared
/// <c>&lt;opc:Object&gt;</c> / <c>&lt;opc:Variable&gt;</c>
/// singletons such as <c>Server</c> or
/// <c>WellKnownRole_Observer</c>; nested entries cover Mandatory
/// well-known children such as <c>Server_ServerCapabilities</c>
/// (an instance of <c>ServerCapabilitiesType</c>) so the type-level
/// factory's child-dispatch can produce the well-known instance
/// NodeIds at every depth.
/// </summary>
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<NodeToGenerate>? singletons))
{
singletons = [];
m_singletonsByType[typeDef.SymbolicId] = singletons;
}
singletons.Add(entry);
}
}
/// <summary>
/// Given the type-level child <paramref name="typeChild"/> that is
/// about to be emitted inside a type-level factory body (e.g. the
/// InputArguments slot inside <c>CreateServerType_GetMonitoredItems</c>),
/// 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.
/// </summary>
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<NodeToGenerate>? 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 "_<ChildSymbolicName>" 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;
}
/// <summary>
/// Registers the <see cref="Tokens.InstanceNodeIdOverride"/>
/// replacement for the type-level factory currently being emitted.
/// When the node has well-known singleton instances (e.g. a method
/// declared under <c>ServerType</c> whose <c>Server</c> singleton
/// has its own well-known <c>NodeId</c>), the wrapper template
/// renders a runtime dispatch that rebinds <c>nodeState.NodeId</c> to
/// the singleton-instance value when <c>forInstance: true</c> is
/// passed AND <c>parent</c> matches one of the known singleton
/// instances. The token expands to nothing when no singleton
/// dispatch is applicable.
/// </summary>
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);
}
/// <summary>
/// Wrapper writer for <see cref="NodeStateTemplates.InstanceNodeIdOverride"/>;
/// expands the inner <see cref="Tokens.ListOfInstanceNodeIdBranches"/>
/// token by iterating the collected per-singleton branches.
/// </summary>
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<object>()],
WriteTemplate_InstanceNodeIdBranch);
return context.Template.Render();
}
/// <summary>
/// Per-branch writer for <see cref="NodeStateTemplates.InstanceNodeIdBranch"/>;
/// the first branch emits <c>if</c>, subsequent ones <c>else if</c>.
/// </summary>
private bool WriteTemplate_InstanceNodeIdBranch(IWriteContext context)
{
if (context.Target is not ValueTuple<NodeToGenerate, NodeToGenerate, string> branch)
{
return false;
}
context.Template.AddReplacement(
Tokens.IfOrElseIf,
context.Index == 0 ? "if" : "else if");
context.Template.AddReplacement(
Tokens.ParentNodeIdConstant,
branch.Item1.Design.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Template.AddReplacement(
Tokens.NodeIdConstant,
branch.Item2.Design.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
return context.Template.Render();
}
private void CollectEncodingNodes()
{
foreach (NodeDesign node in m_context.ModelDesign.Nodes)
{
if (node is not ObjectDesign encoding)
{
continue;
}
if (encoding.TypeDefinition == null ||
encoding.TypeDefinition.Name != "DataTypeEncodingType")
{
continue;
}
if (encoding.NotInAddressSpace)
{
continue;
}
if (m_context.ModelDesign.IsExcluded(encoding))
{
continue;
}
// Skip if already collected
if (m_nodes.ContainsKey(encoding.SymbolicId))
{
continue;
}
// Build a synthetic Hierarchy with the encoding's references
var hierarchy = new Hierarchy();
if (encoding.References != null)
{
foreach (Reference reference in encoding.References)
{
if (reference.ReferenceType == null)
{
continue;
}
hierarchy.References.Add(new HierarchyReference
{
SourcePath = string.Empty,
ReferenceType = reference.ReferenceType,
IsInverse = reference.IsInverse,
TargetId = reference.TargetId,
TargetPath = null
});
}
}
encoding.Hierarchy = hierarchy;
var entry = new NodeToGenerate(
Parent: null,
Path: string.Empty,
Hierarchy: hierarchy,
Design: encoding,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: false,
InstanceOf: null);
m_nodes.TryAdd(encoding.SymbolicId, entry);
}
}
private bool ExcludeNodeStateClassGeneration(NodeToGenerate node)
{
// Filter unncessary nodes
if (!IsInAddressSpace(node))
{
if (m_logger.IsEnabled(LogLevel.Debug))
{
m_logger.LogDebug(
"Excluded node {Node} as it is marked NotInAddressSpace.",
node.Design!.SymbolicId.Name);
}
return true;
}
// Only process type designs and method types for definitions
// Also allow DataTypeEncodingType instances (Default Binary/XML/JSON)
if (node.Design is not VariableTypeDesign and not ObjectTypeDesign &&
!node.Design.IsMethodTypeDesign() &&
!IsDataTypeEncodingInstance(node.Design))
{
return true;
}
if (node.Design!.SymbolicName.Namespace == Namespaces.OpcUa &&
node.Design.NumericId < 256)
{
switch (node.Design.SymbolicName.Name)
{
case "DataTypeDictionaryType":
case "DataTypeDescriptionType":
case "DataTypeSystemType":
case "DataTypeEncodingType":
case "ModellingRuleType":
break;
default:
if (m_logger.IsEnabled(LogLevel.Debug))
{
m_logger.LogDebug(
"Skipping built-in node state class generation for {Node}.",
node.Design.SymbolicId.Name);
}
return true;
}
}
return false;
}
private static bool IsInAddressSpace(NodeToGenerate node)
{
bool isInAddressSpace = !node.Design!.NotInAddressSpace;
if (node.Design is InstanceDesign instanceDesign &&
instanceDesign.TypeDefinition != null &&
instanceDesign.TypeDefinition.Name == "DataTypeEncodingType")
{
isInAddressSpace =
instanceDesign.Parent == null ||
!instanceDesign.Parent.NotInAddressSpace;
}
return isInAddressSpace;
}
private static bool IsDataTypeEncodingInstance(NodeDesign? node)
{
return node is ObjectDesign objectDesign &&
objectDesign.TypeDefinition != null &&
objectDesign.TypeDefinition.Name == "DataTypeEncodingType";
}
/// <summary>
/// Checks if the instance is a built in property of a base node state
/// implementation (variable/method) that should not be generated because
/// it is already defined on the root nodestate class.
/// </summary>
private static bool IsBuiltInProperty(NodeToGenerate node)
{
switch (node.Parent?.Design)
{
case MethodDesign:
if (node.Design!.SymbolicName ==
new XmlQualifiedName("InputArguments", Namespaces.OpcUa))
{
return true;
}
if (node.Design.SymbolicName ==
new XmlQualifiedName("OutputArguments", Namespaces.OpcUa))
{
return true;
}
break;
case VariableDesign:
case VariableTypeDesign:
if (node.Design!.SymbolicName ==
new XmlQualifiedName("EnumStrings", Namespaces.OpcUa))
{
return true;
}
break;
}
return false;
}
internal static bool HasFixedChildSlot(TypeDesign? typeDefinitionNode, string symbolicName)
{
if (typeDefinitionNode == null)
{
return false;
}
if (typeDefinitionNode.Children?.Items != null)
{
foreach (InstanceDesign child in typeDefinitionNode.Children.Items)
{
if (child.SymbolicName.Name == symbolicName &&
child.ModellingRule is ModellingRule.Mandatory or ModellingRule.Optional)
{
return true;
}
}
}
return HasFixedChildSlot(typeDefinitionNode.BaseTypeNode, symbolicName);
}
/// <summary>
/// Returns the effective modelling rule for a <see cref="NodeToGenerate"/>.
/// When <see cref="GeneratorOptions.UseTypeDefinitionModellingRules"/>
/// is enabled (opt-in) and the node belongs to an instance hierarchy,
/// the type definition's modelling rule is used unconditionally.
/// When the option is off (default), the instance may only
/// <em>promote</em> the type-definition rule per OPC UA rules:
/// <list type="bullet">
/// <item><c>Optional → Mandatory</c></item>
/// <item><c>OptionalPlaceholder → Mandatory | MandatoryPlaceholder</c></item>
/// <item><c>MandatoryPlaceholder → Mandatory</c></item>
/// </list>
/// Any other instance override (demotion) is ignored and the
/// type-definition rule is returned instead.
/// </summary>
private ModellingRule GetEffectiveModellingRule(
NodeToGenerate node,
InstanceDesign instance)
{
if (!node.RootIsTypeDefinition &&
node.TypeDefinitionModellingRule.HasValue)
{
ModellingRule typeDefRule = node.TypeDefinitionModellingRule.Value;
if (m_context.Options.UseTypeDefinitionModellingRules)
{
return typeDefRule;
}
// When not opt-in, only allow valid promotions per
// the OPC UA modelling rule standard.
if (!IsValidPromotion(typeDefRule, instance.ModellingRule))
{
return typeDefRule;
}
}
return instance.ModellingRule;
}
/// <summary>
/// Returns the effective modelling rule for a hierarchy node.
/// Overload used in <see cref="GetChildren"/> where the hierarchy
/// node and root-is-type-definition flag are available directly.
/// </summary>
private ModellingRule GetEffectiveModellingRule(
HierarchyNode hierarchyNode,
InstanceDesign child,
bool rootIsTypeDefinition)
{
if (!rootIsTypeDefinition &&
hierarchyNode.TypeDefinitionModellingRule.HasValue)
{
ModellingRule typeDefRule =
hierarchyNode.TypeDefinitionModellingRule.Value;
if (m_context.Options.UseTypeDefinitionModellingRules)
{
return typeDefRule;
}
// When not opt-in, only allow valid promotions per
// the OPC UA modelling rule standard.
if (!IsValidPromotion(typeDefRule, child.ModellingRule))
{
return typeDefRule;
}
}
return child.ModellingRule;
}
/// <summary>
/// Determines whether the instance modelling rule is a valid
/// promotion of the type-definition modelling rule per OPC UA:
/// <list type="bullet">
/// <item><c>Optional → Mandatory</c></item>
/// <item><c>OptionalPlaceholder → Mandatory | MandatoryPlaceholder</c></item>
/// <item><c>MandatoryPlaceholder → Mandatory</c></item>
/// </list>
/// Returns <c>true</c> when the instance rule is an allowed
/// promotion (or identical to the type-definition rule), and
/// <c>false</c> when it is a demotion that should be rejected.
/// </summary>
private static bool IsValidPromotion(
ModellingRule typeDefRule,
ModellingRule instanceRule)
{
if (instanceRule == typeDefRule)
{
return true;
}
return typeDefRule switch
{
ModellingRule.Optional =>
instanceRule == ModellingRule.Mandatory,
ModellingRule.OptionalPlaceholder =>
instanceRule is ModellingRule.Mandatory
or ModellingRule.MandatoryPlaceholder,
ModellingRule.MandatoryPlaceholder =>
instanceRule == ModellingRule.Mandatory,
// Mandatory, None, ExposesItsArray, etc. cannot be promoted
_ => false
};
}
private void GetChildren(
NodeToGenerate node,
Dictionary<XmlQualifiedName, NodeToGenerate> children,
bool forInstance)
{
if (!forInstance)
{
if (node.Design!.Children?.Items != null)
{
foreach (InstanceDesign child in node.Design.Children.Items)
{
if (m_context.ModelDesign.IsExcluded(child))
{
continue;
}
string childPath = child.SymbolicName.Name;
if (!string.IsNullOrEmpty(node.Path))
{
childPath = CoreUtils.Format(
"{0}{1}{2}",
node.Path,
NodeDesign.PathChar,
childPath);
}
node.Children[child.SymbolicName.Name] = new NodeToGenerate(
node,
childPath,
node.Hierarchy,
child,
IsNotExplicitlyDefined: false,
RootIsTypeDefinition: node.RootIsTypeDefinition,
InstanceOf: null);
}
}
}
if (node.Hierarchy == null)
{
return;
}
foreach (HierarchyNode current in node.Hierarchy.NodeList)
{
string? childPath = current.RelativePath;
// only looking for nodes in the current tree.
if (!childPath!.StartsWith(node.Path!, StringComparison.Ordinal))
{
continue;
}
// ignore reference to the current base node.
if (childPath == node.Path)
{
continue;
}
// relative should always end in the name of the current instance.
if (!childPath.EndsWith(
current.Instance!.SymbolicName.Name,
StringComparison.Ordinal))
{
continue;
}
// get the parent path.
if (childPath.Length <= current.Instance.SymbolicName.Name.Length)
{
if (!string.IsNullOrEmpty(node.Path))
{
continue;
}
}
else
{
int idx = childPath.Length - current.Instance.SymbolicName.Name.Length - 1;
string parentPath = current.RelativePath![..idx];
if (parentPath != node.Path)
{
continue;
}
}
if (!string.IsNullOrEmpty(node.Path))
{
childPath = childPath[(node.Path.Length + 1)..];
childPath = CoreUtils.Format(
"{0}{1}{2}",
node.Path,
NodeDesign.PathChar,
childPath);
}
if (current.Instance is not InstanceDesign child)
{
continue;
}
ModellingRule effectiveRule = GetEffectiveModellingRule(
current, child, node.RootIsTypeDefinition);
if (!node.RootIsTypeDefinition &&
!current.ExplicitlyDefined &&
effectiveRule != ModellingRule.Mandatory &&
effectiveRule != ModellingRule.Optional)
{
continue;
}
if (!current.ExplicitlyDefined &&
effectiveRule != ModellingRule.Mandatory &&
effectiveRule != ModellingRule.None &&
effectiveRule != ModellingRule.ExposesItsArray &&
effectiveRule != ModellingRule.OptionalPlaceholder &&
effectiveRule != ModellingRule.MandatoryPlaceholder)
{
continue;
}
if (node.RootIsTypeDefinition &&
!current.ExplicitlyDefined &&
current.Inherited &&
current.AdHocInstance &&
effectiveRule != ModellingRule.Mandatory)
{
// Required descendants of generic containers such as DI ParameterSet
// must also initialize derived instances. Keep this shallow-level
// suppression only for non-mandatory inherited ad-hoc declarations.
if (!node.Path!.Contains(NodeDesign.PathChar, StringComparison.Ordinal))
{
continue;
}
}
bool add = false;
if (node.Design is DataTypeDesign or ViewDesign or ReferenceTypeDesign)
{
add = true;
}
else if (node.RootIsTypeDefinition)
{
if (effectiveRule == ModellingRule.Mandatory)
{
add = true;
}
else if (current.ExplicitlyDefined &&
effectiveRule == ModellingRule.Optional)
{
add = true;
}
else if (current.ExplicitlyDefined &&
(effectiveRule is
ModellingRule.ExposesItsArray or
ModellingRule.OptionalPlaceholder or
ModellingRule.MandatoryPlaceholder))
{
add = true;
}
else if (current.StaticValue && !current.Inherited)
{
add = true;
}
else if (effectiveRule == ModellingRule.None &&
current.ExplicitlyDefined)
{
// Include ModellingRule=None children that are explicitly defined
// on this type (e.g., state machine states/transitions, type-level
// methods like ConditionRefresh, and type-level properties like
// Creatable or SupportsFilteredRetain).
add = true;
}
else if (effectiveRule is not ModellingRule.None)
{
if (m_logger.IsEnabled(LogLevel.Debug))
{
m_logger.LogDebug(
"Excluding child node {Node} from generation.",
current.Instance.SymbolicId.Name);
}
}
}
else if (effectiveRule == ModellingRule.Mandatory)
{
add = true;
}
else if (current.ExplicitlyDefined)
{
add = true;
}
else if (m_logger.IsEnabled(LogLevel.Information))
{
m_logger.LogInformation(
"Excluding child node {Node} from generation.",
current.Instance.SymbolicId.Name);
}
if (add)
{
XmlQualifiedName symbolicId = current.Instance.SymbolicId;
var childNodeToGenerate = new NodeToGenerate(
node,
childPath,
node.Hierarchy,
child,
IsNotExplicitlyDefined: !current.ExplicitlyDefined,
RootIsTypeDefinition: node.RootIsTypeDefinition,
InstanceOf: null,
TypeDefinitionModellingRule:
current.TypeDefinitionModellingRule,
IsUnderSingletonInstance: node.IsUnderSingletonInstance);
if (!children.TryAdd(symbolicId, childNodeToGenerate) &&
m_logger.IsEnabled(LogLevel.Information))
{
m_logger.LogInformation(
"Removing duplicate entry for child {Node}.",
symbolicId.Name);
}
node.AllChildren.Add(childNodeToGenerate);
GetChildren(childNodeToGenerate, children, forInstance);
}
}
}
private HashSet<ReferenceToGenerate> GetReferences(NodeToGenerate node)
{
if (node.Hierarchy == null)
{
return [];
}
Hierarchy hierarchy = node.Hierarchy;
NodeDesign? root = node.Design;
// A predefined instance of a NodeSet2 input does not create the
// nodes it omits, so no reference may end in one of them.
bool omitsUndeclaredNodes = OmitsUndeclaredHierarchyNodes(node);
HashSet<ReferenceToGenerate> references = [];
foreach (HierarchyReference reference in hierarchy.References)
{
if (reference.SourcePath != node.Path &&
reference.TargetPath != node.Path)
{
continue;
}
bool isInverse = reference.IsInverse;
// The type's GeneratesEvent references describe its instances; an Object or
// Variable cannot be their source (Part 3 §7.15), so they stay on the type.
if (!isInverse &&
reference.DefinedOnType &&
reference.SourcePath == node.Path &&
root is ObjectDesign or VariableDesign &&
IsGeneratesEventReference(reference.ReferenceType))
{
continue;
}
if (reference.TargetId != null)
{
if (!m_context.ModelDesign.TryFindNode(
reference.TargetId,
root!.SymbolicId.Name,
reference.ReferenceType!.Name,
out NodeDesign? targetNode))
{
continue;
}
references.Add(new ReferenceToGenerate(
targetNode,
reference.ReferenceType,
isInverse));
continue;
}
if (reference.TargetPath != null &&
reference.TargetPath.Length == 0 &&
node.Parent?.Design != null)
{
// An empty target path names the root of the hierarchy
// (the type or the top-level instance), which is only the
// immediate parent for a direct child.
NodeToGenerate hierarchyRoot = node.Parent;
while (hierarchyRoot.Parent?.Design != null)
{
hierarchyRoot = hierarchyRoot.Parent;
}
references.Add(new ReferenceToGenerate(
hierarchyRoot.Design!,
reference.ReferenceType!,
isInverse));
continue;
}
if (reference.SourcePath == node.Path)
{
if (!hierarchy.Nodes.TryGetValue(
reference.TargetPath!,
out HierarchyNode? target))
{
continue;
}
if (!target.ExplicitlyDefined && node.RootIsTypeDefinition)
{
continue;
}
if (omitsUndeclaredNodes &&
!IsCreatedInNodeSetAddressSpace(node, reference.TargetPath!))
{
continue;
}
references.Add(new ReferenceToGenerate(
target.Instance!,
reference.ReferenceType!,
isInverse));
continue;
}
if (!hierarchy.Nodes.TryGetValue(reference.SourcePath!, out HierarchyNode? source))
{
continue;
}
if (!source.ExplicitlyDefined && node.RootIsTypeDefinition)
{
continue;
}
if (omitsUndeclaredNodes &&
!IsCreatedInNodeSetAddressSpace(node, reference.SourcePath!))
{
continue;
}
references.Add(new ReferenceToGenerate(
source.Instance!,
reference.ReferenceType!,
!isInverse));
}
// Apply the instance filter after resolving both identifier and hierarchy-path targets.
if (!node.RootIsTypeDefinition)
{
references.RemoveWhere(reference => reference.TargetNode is InstanceDesign
{
ModellingRule: ModellingRule.MandatoryPlaceholder or ModellingRule.OptionalPlaceholder
});
}
// Inherited and declared designs can differ structurally while
// resolving to the same runtime reference.
HashSet<(string ReferenceTypeId, bool IsInverse, string TargetId)> referenceIds = [];
references.RemoveWhere(reference => !referenceIds.Add((
m_context.ModelDesign.GetNodeIdConstant(
reference.ReferenceTypeId,
"<ReferenceType>",
kNamespaceTableContextVariable),
reference.IsInverse,
reference.TargetNode.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable))));
return references;
}
/// <summary>
/// True for GeneratesEvent and its subtypes such as AlwaysGeneratesEvent.
/// </summary>
private bool IsGeneratesEventReference(XmlQualifiedName? referenceTypeId)
{
var generatesEvent = new XmlQualifiedName("GeneratesEvent", Ua.Types.Namespaces.OpcUa);
if (referenceTypeId == generatesEvent)
{
return true;
}
if (referenceTypeId == null ||
!m_context.ModelDesign.TryFindNode(
referenceTypeId,
string.Empty,
string.Empty,
out NodeDesign? design))
{
return false;
}
for (TypeDesign? type = (design as ReferenceTypeDesign)?.BaseTypeNode;
type != null;
type = type.BaseTypeNode)
{
if (type.SymbolicId == generatesEvent)
{
return true;
}
}
return false;
}
private List<InstanceDesign> GetAdditionalChildren(NodeToGenerate node)
{
List<InstanceDesign> additionalChildren = [];
foreach (NodeToGenerate child in node.Children.Values)
{
if (child.Design is not InstanceDesign instance)
{
continue;
}
if (child.IsNotExplicitlyDefined)
{
continue;
}
if (GetEffectiveModellingRule(child, instance) is
not ModellingRule.Mandatory and
not ModellingRule.Optional)
{
continue;
}
if (instance.IsOverridden() &&
!IsMethodOverrideWithDifferentClass(instance))
{
continue;
}
additionalChildren.Add(instance);
}
return additionalChildren;
}
/// <summary>
/// Returns whether a method overrides the supertype's method with
/// another method state class. The subtype then declares a field and a
/// property of its own that hide the supertype's, so it also has to
/// enumerate and remove the child itself: the supertype's field stays
/// empty.
/// </summary>
private bool IsMethodOverrideWithDifferentClass(InstanceDesign instance)
{
return instance is MethodDesign &&
instance.IsOverridden() &&
!instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces);
}
private List<InstanceDesign> GetChildrenWithProperties(
NodeToGenerate node,
bool excludeBuiltIn = false)
{
List<InstanceDesign> childrenWithProperties = [];
foreach (NodeToGenerate child in node.Children.Values)
{
if (child.Design is not InstanceDesign instance)
{
continue;
}
if (child.IsNotExplicitlyDefined)
{
continue;
}
if (GetEffectiveModellingRule(child, instance) is
not ModellingRule.Mandatory and
not ModellingRule.Optional)
{
continue;
}
if (instance.IsOverriddenWithSameClass(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces))
{
continue;
}
if (excludeBuiltIn && IsBuiltInProperty(child))
{
continue;
}
childrenWithProperties.Add(instance);
}
return childrenWithProperties;
}
private HashSet<RolePermission> GetRolePermissions(NodeDesign node)
{
var rolePermissions = new HashSet<RolePermission>();
// Emit RolePermissions on all nodes (type and instance).
// Type hierarchy nodes carry permissions as metadata but the server
// bypasses enforcement via IsPartOfTypeHierarchy at runtime.
RolePermission[]? nodeRolePermissions =
(node.RolePermissions?.RolePermission) ?? (node.DefaultRolePermissions?.RolePermission);
if (nodeRolePermissions != null)
{
foreach (RolePermission rp in nodeRolePermissions)
{
ObjectDesign? roleNode = m_context.ModelDesign.FindNode<ObjectDesign>(
rp.Role,
rp.Role.Name,
"RoleType");
if (roleNode != null)
{
rolePermissions.Add(rp);
}
}
}
return rolePermissions;
}
private static string? GetDisplayNameValue(NodeDesign node)
{
if (node.DisplayName != null)
{
return CoreUtils.Format(
"{0}, string.Empty, {1}",
node.DisplayName.Key.AsStringLiteral(),
(node.DisplayName.Value?.Trim()).AsStringLiteral());
}
return null;
}
private static string? GetDescriptionValue(NodeDesign node)
{
if (node.Description != null && !node.Description.IsAutogenerated)
{
// The generated templates assign through a nodeState local that
// is statically typed as NodeState, so models with a child
// Property named "Description" cannot shadow this assignment.
return CoreUtils.Format(
"nodeState.Description = {0};",
node.Description.GetLocalizedTextAsCode());
}
return null;
}
private static string? GetInverseNameValue(ReferenceTypeDesign node)
{
if (!node.Symmetric && node.InverseName != null)
{
return CoreUtils.Format(
"state.InverseName = {0};",
node.InverseName.GetLocalizedTextAsCode());
}
if (node.Symmetric)
{
return "state.InverseName = global::Opc.Ua.LocalizedText.Null;";
}
return null;
}
private static string? GetModellingRuleReplacement(ModellingRule modellingRule)
{
string? constant = modellingRule switch
{
ModellingRule.Mandatory => "global::Opc.Ua.Objects.ModellingRule_Mandatory",
ModellingRule.Optional => "global::Opc.Ua.Objects.ModellingRule_Optional",
ModellingRule.MandatoryPlaceholder => "global::Opc.Ua.Objects.ModellingRule_MandatoryPlaceholder",
ModellingRule.OptionalPlaceholder => "global::Opc.Ua.Objects.ModellingRule_OptionalPlaceholder",
ModellingRule.ExposesItsArray => "global::Opc.Ua.Objects.ModellingRule_ExposesItsArray",
_ => null
};
return constant != null
? CoreUtils.Format("baseState.ModellingRuleId = new global::Opc.Ua.NodeId({0});", constant)
: null;
}
private string GetNodeIdConstantForDataType(
VariableTypeDesign type,
Namespace[] namespaceUris)
{
if (!m_context.ModelDesign.UseAllowSubtypes)
{
DataTypeDesign? dataType = m_context.ModelDesign.FindNode<DataTypeDesign>(
type.DataType,
type.SymbolicId.Name,
"DataType");
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
return type.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
private string GetNodeIdConstantForDataType(
VariableDesign instance,
Namespace[] namespaceUris)
{
if (!m_context.ModelDesign.UseAllowSubtypes)
{
DataTypeDesign? dataType = m_context.ModelDesign.FindNode<DataTypeDesign>(
instance.DataType,
instance.SymbolicId.Name,
"DataType");
return dataType.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
return instance.DataTypeNode.GetNodeIdAsCode(namespaceUris, kNamespaceTableContextVariable);
}
private string GetTemplateParameter(TypeDesign type, out string variantBuilder)
{
variantBuilder = "global::Opc.Ua.VariantBuilder";
if (type is not VariableTypeDesign variableType)
{
return string.Empty;
}
if (type.BaseTypeNode == null)
{
return "<T>";
}
if (GetTemplateParameter(type.BaseTypeNode, out variantBuilder) != "<T>")
{
return string.Empty;
}
BasicDataType basicType = variableType.DataTypeNode!.BasicDataType;
if (basicType == BasicDataType.BaseDataType) // == Variant, so any
{
return "<T>";
}
string scalarName;
switch (basicType)
{
case BasicDataType.UserDefined:
scalarName = variableType.DataTypeNode.GetClassName(
m_context.ModelDesign.Namespaces);
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
break;
default:
scalarName = variableType.DataTypeNode.GetDotNetTypeName(
m_context.ModelDesign.TargetNamespace.Value,
m_context.ModelDesign.Namespaces,
nullable: NullableAnnotation.NonNullable,
true);
variantBuilder = variableType.DataTypeNode.GetVariantBuilder(scalarName);
break;
}
if (variableType.ValueRank == ValueRank.Scalar)
{
return $"<{scalarName}>";
}
if (variableType.ValueRank == ValueRank.Array)
{
return $"<global::Opc.Ua.ArrayOf<{scalarName}>>";
}
if (variableType.ValueRank == ValueRank.OneOrMoreDimensions &&
variableType.DataTypeNode.SupportsMatrixOf())
{
return $"<global::Opc.Ua.MatrixOf<{scalarName}>>";
}
variantBuilder = "global::Opc.Ua.VariantBuilder";
return "<global::Opc.Ua.Variant>";
}
private static void CollectMatchingFields(
VariableTypeDesign variableType,
Dictionary<string, Parameter> fields)
{
CollectFields(
variableType.DataTypeNode!,
variableType.ValueRank,
string.Empty,
fields);
List<string> availablePaths = [.. fields.Keys];
for (int ii = 0; ii < availablePaths.Count; ii++)
{
if (!variableType.Hierarchy.Nodes.ContainsKey(availablePaths[ii]))
{
fields.Remove(availablePaths[ii]);
}
}
}
private static void CollectFields(
DataTypeDesign dataType,
ValueRank valueRank,
string basePath,
Dictionary<string, Parameter> fields)
{
if (dataType.BasicDataType != BasicDataType.UserDefined ||
valueRank != ValueRank.Scalar)
{
return;
}
for (DataTypeDesign? parent = dataType;
parent != null;
parent = parent.BaseTypeNode as DataTypeDesign)
{
if (parent.Fields != null)
{
for (int ii = 0; ii < parent.Fields.Length; ii++)
{
Parameter parameter = parent.Fields[ii];
string fieldPath = parameter.Name;
if (!string.IsNullOrEmpty(basePath))
{
fieldPath = CoreUtils.Format("{0}_{1}", basePath, parameter.Name);
}
fields[fieldPath] = parameter;
}
}
}
}
private string? AddXmlInitializerForComplexValue(
NodeDesign node,
DataTypeDesign dataType,
System.Xml.XmlElement element)
{
string? xml = element?.OuterXml;
if (string.IsNullOrEmpty(xml))
{
return null;
}
string resourceName = CoreUtils.Format("Values.{0}", node.SymbolicId.Name);
string uniqueName = resourceName;
for (int i = 0; i < 1000; i++)
{
if (m_initializers.TryAdd(uniqueName, new TextResource(uniqueName, xml)))
{
// Get code to create the variant from the XML resource reference
return CoreUtils.Format(
"global::Opc.Ua.Variant.FromXml({0}AsStream, context)",
uniqueName);
}
uniqueName = resourceName + i;
}
throw new InvalidOperationException("Unexpected duplicate resource names");
}
private static void AddVariantAccessor(IWriteContext context, DataTypeDesign dataType)
{
switch (dataType.BasicDataType)
{
case BasicDataType.UserDefined when !dataType.IsEnumeration:
context.Template.AddReplacement(Tokens.VariantFrom, "FromStructure");
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetStructure");
break;
default:
context.Template.AddReplacement(Tokens.VariantFrom, "From");
context.Template.AddReplacement(Tokens.VariantTryGet, "TryGetValue");
break;
}
}
/// <summary>
/// Embed all initializers as source code
/// </summary>
private Resource? EmbedInitializers()
{
if (m_initializers.Count == 0)
{
return null;
}
var initializers = new ResourceGenerator(m_context);
return initializers.Embed(
m_context.ModelDesign.TargetNamespace.Prefix,
"NodeStates.i",
internalAccess: true,
[.. m_initializers.Values]);
}
/// <summary>
/// Node state generation template
/// </summary>
private record class NodeToGenerate(
NodeToGenerate? Parent = null,
string? Path = null,
Hierarchy? Hierarchy = null,
NodeDesign? Design = null,
bool IsNotExplicitlyDefined = false,
bool RootIsTypeDefinition = false,
NodeToGenerate? InstanceOf = null,
ModellingRule? TypeDefinitionModellingRule = null,
bool IsUnderSingletonInstance = false)
{
/// <summary>
/// Full inherited list of children
/// </summary>
public List<NodeToGenerate> AllChildren { get; } = [];
/// <summary>
/// Direclty defined
/// </summary>
public Dictionary<string, NodeToGenerate> Children { get; } = [];
public NodeToGenerate? Instance { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return CoreUtils.Format("{0} (Parent: {1})", Design, Parent);
}
}
private record class ReferenceToGenerate(
NodeDesign TargetNode,
XmlQualifiedName ReferenceTypeId,
bool IsInverse);
/// <summary>
/// True when the argument's code type is the data type itself (scalar,
/// ArrayOf or MatrixOf). Every other value rank (ScalarOrArray,
/// ScalarOrOneDimension, Any) is carried as a plain Variant, see
/// ModelDesignExtensions.GetMethodArgumentTypeAsCode.
/// </summary>
private static bool HasTypedArgumentValueRank(Parameter parameter)
{
return parameter.ValueRank is
ValueRank.Scalar or
ValueRank.Array or
ValueRank.OneOrMoreDimensions;
}
private static string GetMethodArgumentIdentifier(
Parameter parameter,
bool upperCamelCase = false)
{
return parameter.GetGeneratedCodeIdentifier(
upperCamelCase,
s_methodArgumentCodeNameScope);
}
private void AssignMethodArgumentCodeNames(
MethodDesign method,
Parameter[] inputArguments,
Parameter[] outputArguments)
{
string declaredClassName = method.GetNodeStateClassName(
m_context.ModelDesign.TargetNamespace.Value,
[],
applyStandardFallback: false);
ModelDesignExtensions.AssignMethodArgumentCodeNames(
inputArguments,
outputArguments,
s_methodArgumentCodeNameScope,
declaredClassName + "Result");
}
private sealed record MethodArgumentsToGenerate(
string PropertyName,
string BrowseName,
Parameter[] Arguments);
private sealed record ImportFactoryRegistration(
string NodeClass,
string Discriminator,
string DiscriminatorId,
string CreateExpression);
private const string kNamespaceTableContextVariable = "context.NamespaceUris";
private static readonly XmlQualifiedName s_inputArgumentsBrowseName = new(
"InputArguments",
Namespaces.OpcUa);
private static readonly XmlQualifiedName s_outputArgumentsBrowseName = new(
"OutputArguments",
Namespaces.OpcUa);
private static readonly MethodArgumentCodeNameScope s_methodArgumentCodeNameScope = new(
"_",
"await",
"ct",
"cancellationToken",
"context",
"_context",
"inputArguments",
"_inputArguments",
"method",
"_method",
"objectId",
"_objectId",
"onCall",
"onCallAsync",
"outputArguments",
"_outputArguments",
"_result",
"results",
"serviceResult",
"ServiceResult");
private static readonly string[] s_builtInPropertyNames =
[
"Description",
"Save",
"Handle",
"Specification",
"Update",
"Delete",
// Children whose generated accessor property shadows the
// identically-named global::Opc.Ua.NodeState.Extensions property,
// which carries the NodeSet2 <Extensions> elements rather than an
// address-space member, and therefore must be declared "public new".
"Extensions",
// Method children whose generated accessor property shadows the
// identically-named global::Opc.Ua.NodeState.Validate(ISystemContext)
// instance method and therefore must be declared "public new".
"Validate"
];
/// <summary>
/// True when a child property named <paramref name="name"/> on the
/// generated class of <paramref name="parent"/> hides an inherited
/// member of the runtime base class and must be declared
/// <c>public new</c>.
/// </summary>
internal static bool RequiresNewModifier(NodeDesign parent, string name)
{
return s_builtInPropertyNames.Contains(name) ||
HidesRuntimeBaseMember(parent, name);
}
/// <summary>
/// True when a child property named <paramref name="name"/> on the
/// generated class of <paramref name="parent"/> hides a member it
/// inherits from the runtime base class (for example a child Property
/// named <c>DataType</c> on a VariableType class hides
/// <c>BaseVariableState.DataType</c>, one named <c>Categories</c>
/// hides <c>NodeState.Categories</c>), so it must be declared
/// <c>public new</c>.
/// </summary>
private static bool HidesRuntimeBaseMember(NodeDesign parent, string name)
{
return parent switch
{
ObjectTypeDesign or ObjectDesign => s_objectStateMemberNames.Value.Contains(name),
VariableTypeDesign or VariableDesign => s_variableStateMemberNames.Value.Contains(name),
MethodDesign => s_methodStateMemberNames.Value.Contains(name),
_ => false
};
}
/// <summary>
/// Returns the names of the members of <paramref name="type"/> and its
/// base classes that a derived class sees (public and protected), i.e.
/// the names a generated child property hides. Read from the runtime
/// types themselves so a member added to them is never missed.
/// </summary>
private static HashSet<string> GetInheritedMemberNames(Type type)
{
const BindingFlags kFlags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
var names = new HashSet<string>(StringComparer.Ordinal);
for (Type? current = type; current != null; current = current.BaseType)
{
foreach (MemberInfo member in current.GetMembers(kFlags))
{
bool visible = member switch
{
ConstructorInfo => false,
MethodInfo method => !method.IsSpecialName && IsVisibleToDerived(method),
PropertyInfo property => property.GetIndexParameters().Length == 0 &&
(IsVisibleToDerived(property.GetGetMethod(true)) ||
IsVisibleToDerived(property.GetSetMethod(true))),
EventInfo @event => IsVisibleToDerived(@event.GetAddMethod(true)),
FieldInfo field => !field.IsSpecialName &&
(field.IsPublic || field.IsFamily || field.IsFamilyOrAssembly),
Type nested => nested.IsNestedPublic || nested.IsNestedFamily ||
nested.IsNestedFamORAssem,
_ => false
};
if (visible)
{
names.Add(member.Name);
}
}
}
return names;
}
private static bool IsVisibleToDerived(MethodBase? method)
{
return method != null &&
(method.IsPublic || method.IsFamily || method.IsFamilyOrAssembly);
}
/// <summary>
/// Members inherited by generated Object and ObjectType classes.
/// </summary>
private static readonly Lazy<HashSet<string>> s_objectStateMemberNames =
new(() => GetInheritedMemberNames(typeof(BaseObjectState)));
/// <summary>
/// Members inherited by generated Variable and VariableType classes
/// ("Value" is handled separately).
/// </summary>
private static readonly Lazy<HashSet<string>> s_variableStateMemberNames =
new(() => GetInheritedMemberNames(typeof(BaseVariableState)));
/// <summary>
/// Members inherited by generated Method classes.
/// </summary>
private static readonly Lazy<HashSet<string>> s_methodStateMemberNames =
new(() => GetInheritedMemberNames(typeof(MethodState)));
private static readonly string[] s_builtInMethodNames =
[
"Child"
];
private readonly Dictionary<string, Resource> m_initializers = [];
private readonly Dictionary<XmlQualifiedName, NodeToGenerate> m_nodes = [];
private readonly Dictionary<XmlQualifiedName, NodeToGenerate> m_instances = [];
private readonly Dictionary<XmlQualifiedName, List<NodeToGenerate>> m_singletonsByType = [];
private readonly ILogger m_logger;
private readonly IServiceMessageContext m_messageContext;
private readonly IGeneratorContext m_context;
}
}