Source generator: option to emit exactly the nodes of a NodeSet2 (#4797)

# Description

For a NodeSet2 input the generator built the predefined nodes from the
merged instance hierarchy. A subtype therefore got a copy of every
Mandatory InstanceDeclaration of its supertypes as a new child node,
with
a NodeId the NodeSet2 never assigned (the supertype's numeric identifier
in the subtype's namespace, or a string identifier derived from the path
when the supertype comes from a referenced assembly). A DeviceType
subtype got eight such children from DI's DeviceType, so the address
space
a server loads from the generated code differs from the NodeSet2 it was
generated from.

ModelSourceGeneratorEmitNodeSetNodesOnly (GeneratorOptions
.EmitNodeSetNodesOnly), off by default, makes the predefined nodes of a
NodeSet2 input exactly the nodes the NodeSet2 declares: a child the
generator derives from a supertype or type definition is not added to a
predefined node. The typed state classes are unchanged and still reach
inherited members through the state classes of their supertypes, and the
CreateInstanceOf* factories still create every Mandatory child of the
type. ModelDesign inputs are not affected. IModelDesign.IsSourceNodeSet
tells the generator whether the target model was read from a NodeSet2.

A test in Opc.Ua.Di.Tests generates a DI-style NodeSet2 with the option
on, exports the generated predefined nodes as a NodeSet2 and compares it
with the input, node for node and reference for reference.

## 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>
This commit is contained in:
Wolfgang Colsman
2026-10-16 06:38:19 -04:00
committed by GitHub
parent 87012a147f
commit 716858aa7d
11 changed files with 783 additions and 23 deletions
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A DI-style companion model: a DeviceType subtype, which inherits the
Mandatory InstanceDeclarations of DeviceType, and one device instance that
declares every Mandatory child of DeviceType with its own NodeId.
PumpType also declares a Mandatory Sensor whose SensorType holds a
Mandatory DeviceType Transmitter; neither declaration repeats the Mandatory
children of its type definition. PumpType declares a Controller that
references a Motor; Pump1 declares the Controller but neither the Motor nor
the Sensor.
-->
<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd"
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
<NamespaceUris>
<Uri>http://test.org/UA/NodeSetNodesOnlyPump/</Uri>
<Uri>http://opcfoundation.org/UA/DI/</Uri>
</NamespaceUris>
<Models>
<Model ModelUri="http://test.org/UA/NodeSetNodesOnlyPump/" Version="1.0.0" PublicationDate="2026-10-01T00:00:00Z">
<RequiredModel ModelUri="http://opcfoundation.org/UA/" Version="1.05.04" PublicationDate="2025-01-08T00:00:00Z" />
<RequiredModel ModelUri="http://opcfoundation.org/UA/DI/" Version="1.05.0" PublicationDate="2025-11-15T00:00:00Z" />
</Model>
</Models>
<Aliases>
<Alias Alias="Double">i=11</Alias>
<Alias Alias="Int32">i=6</Alias>
<Alias Alias="String">i=12</Alias>
<Alias Alias="LocalizedText">i=21</Alias>
<Alias Alias="Organizes">i=35</Alias>
<Alias Alias="HasModellingRule">i=37</Alias>
<Alias Alias="HasTypeDefinition">i=40</Alias>
<Alias Alias="HasSubtype">i=45</Alias>
<Alias Alias="HasProperty">i=46</Alias>
<Alias Alias="HasComponent">i=47</Alias>
<Alias Alias="HasEffect">i=54</Alias>
</Aliases>
<UAObjectType NodeId="ns=1;i=1001" BrowseName="1:SensorType">
<DisplayName>SensorType</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=5010</Reference>
</References>
</UAObjectType>
<UAObject NodeId="ns=1;i=5010" BrowseName="1:Transmitter" ParentNodeId="ns=1;i=1001">
<DisplayName>Transmitter</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">ns=2;i=1002</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
</UAObject>
<UAObjectType NodeId="ns=1;i=1000" BrowseName="1:PumpType">
<DisplayName>PumpType</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">ns=2;i=1002</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6000</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=5011</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=5012</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=5013</Reference>
</References>
</UAObjectType>
<UAObject NodeId="ns=1;i=5011" BrowseName="1:Sensor" ParentNodeId="ns=1;i=1000">
<DisplayName>Sensor</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">ns=1;i=1001</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
</UAObject>
<UAObject NodeId="ns=1;i=5012" BrowseName="1:Controller" ParentNodeId="ns=1;i=1000">
<DisplayName>Controller</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
<Reference ReferenceType="HasEffect">ns=1;i=5013</Reference>
</References>
</UAObject>
<UAObject NodeId="ns=1;i=5013" BrowseName="1:Motor" ParentNodeId="ns=1;i=1000">
<DisplayName>Motor</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
</UAObject>
<UAVariable NodeId="ns=1;i=6000" BrowseName="1:RatedFlow" ParentNodeId="ns=1;i=1000" DataType="Double">
<DisplayName>RatedFlow</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
<Reference ReferenceType="HasModellingRule">i=78</Reference>
</References>
</UAVariable>
<UAObject NodeId="ns=1;i=5000" BrowseName="1:Pump1">
<DisplayName>Pump1</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">ns=1;i=1000</Reference>
<Reference ReferenceType="Organizes" IsForward="false">i=85</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6010</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6011</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6012</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6013</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6014</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6015</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6016</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6017</Reference>
<Reference ReferenceType="HasProperty">ns=1;i=6018</Reference>
<Reference ReferenceType="HasComponent">ns=1;i=6020</Reference>
</References>
</UAObject>
<UAObject NodeId="ns=1;i=6020" BrowseName="1:Controller" ParentNodeId="ns=1;i=5000">
<DisplayName>Controller</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
</References>
</UAObject>
<UAVariable NodeId="ns=1;i=6010" BrowseName="1:RatedFlow" ParentNodeId="ns=1;i=5000" DataType="Double">
<DisplayName>RatedFlow</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6011" BrowseName="2:SerialNumber" ParentNodeId="ns=1;i=5000" DataType="String">
<DisplayName>SerialNumber</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
<Value>
<uax:String>SN-0001</uax:String>
</Value>
</UAVariable>
<UAVariable NodeId="ns=1;i=6012" BrowseName="2:RevisionCounter" ParentNodeId="ns=1;i=5000" DataType="Int32">
<DisplayName>RevisionCounter</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6013" BrowseName="2:Manufacturer" ParentNodeId="ns=1;i=5000" DataType="LocalizedText">
<DisplayName>Manufacturer</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6014" BrowseName="2:Model" ParentNodeId="ns=1;i=5000" DataType="LocalizedText">
<DisplayName>Model</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6015" BrowseName="2:HardwareRevision" ParentNodeId="ns=1;i=5000" DataType="String">
<DisplayName>HardwareRevision</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6016" BrowseName="2:SoftwareRevision" ParentNodeId="ns=1;i=5000" DataType="String">
<DisplayName>SoftwareRevision</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6017" BrowseName="2:DeviceRevision" ParentNodeId="ns=1;i=5000" DataType="String">
<DisplayName>DeviceRevision</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable NodeId="ns=1;i=6018" BrowseName="2:DeviceManual" ParentNodeId="ns=1;i=5000" DataType="String">
<DisplayName>DeviceManual</DisplayName>
<References>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
</UANodeSet>
@@ -0,0 +1,250 @@
/* ========================================================================
* Copyright (c) 2005-2026 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.IO;
using System.Linq;
using NodeSetNodesOnlyPump;
using NUnit.Framework;
using Opc.Ua.Export;
using Opc.Ua.Tests;
namespace Opc.Ua.Di.Tests
{
/// <summary>
/// With <c>ModelSourceGeneratorEmitNodeSetNodesOnly</c>, the predefined
/// nodes generated for a NodeSet2 are exactly the nodes of the NodeSet2.
/// Without it, a DeviceType subtype got a copy of every Mandatory
/// InstanceDeclaration of DeviceType, each with a NodeId the NodeSet2
/// never assigned.
/// </summary>
[TestFixture]
[Category("DI")]
public sealed class NodeSetNodesOnlyAddressSpaceTests
{
private const string kResource = "Opc.Ua.Di.Tests.Assets.NodeSetNodesOnlyPump.NodeSet2.xml";
private const string kModelUri = "http://test.org/UA/NodeSetNodesOnlyPump/";
[Test]
public void ExportedAddressSpaceHasTheNodesOfTheNodeSet()
{
UANodeSet input = ReadInput();
UANodeSet exported = ExportGeneratedNodes();
Assert.That(
Nodes(exported),
Is.EquivalentTo(Nodes(input)));
}
[Test]
public void ExportedAddressSpaceHasTheReferencesOfTheNodeSet()
{
UANodeSet input = ReadInput();
UANodeSet exported = ExportGeneratedNodes();
Assert.That(
References(exported),
Is.EquivalentTo(References(input)));
}
[Test]
public void InstancesCreatedAtRunTimeStillGetTheInheritedMandatoryChildren()
{
SystemContext context = CreateContext();
PumpState pump = context.CreateInstanceOfPumpType(
browseName: new QualifiedName("Pump2", 1));
Assert.Multiple(() =>
{
Assert.That(pump.SerialNumber, Is.Not.Null);
Assert.That(pump.RevisionCounter, Is.Not.Null);
Assert.That(pump.RatedFlow, Is.Not.Null);
});
}
[Test]
public void ExportedInstanceHasNoReferenceToAnUndeclaredChild()
{
UANodeSet input = ReadInput();
UANodeSet exported = ExportGeneratedNodes();
// Pump1 declares the Controller of PumpType, not the Motor that
// the Controller references.
string controller = kModelUri + ";i=6020";
Assert.Multiple(() =>
{
Assert.That(
References(exported).Where(r => r.Contains(controller, StringComparison.Ordinal)),
Is.EquivalentTo(References(input).Where(r => r.Contains(controller, StringComparison.Ordinal))));
Assert.That(UnresolvedModelReferences(exported), Is.Empty);
});
}
[Test]
public void InstancesCreatedAtRunTimeGetTheMandatoryDescendantsOfDeclaredChildren()
{
// PumpType declares Sensor but not the Transmitter of SensorType,
// and SensorType declares Transmitter but not the children of
// DeviceType.
SystemContext context = CreateContext();
PumpState pump = context.CreateInstanceOfPumpType(
browseName: new QualifiedName("Pump3", 1));
Assert.Multiple(() =>
{
Assert.That(pump.Sensor, Is.Not.Null);
Assert.That(pump.Sensor?.Transmitter, Is.Not.Null);
Assert.That(pump.Sensor?.Transmitter?.SerialNumber, Is.Not.Null);
Assert.That(pump.Sensor?.Transmitter?.RevisionCounter, Is.Not.Null);
Assert.That(pump.Sensor?.Transmitter?.Manufacturer, Is.Not.Null);
Assert.That(pump.Controller, Is.Not.Null);
Assert.That(pump.Motor, Is.Not.Null);
Assert.That(
pump.Controller?.ReferenceExists(
global::Opc.Ua.ReferenceTypeIds.HasEffect,
false,
pump.Motor!.NodeId),
Is.True);
});
}
private static UANodeSet ExportGeneratedNodes()
{
SystemContext context = CreateContext();
var nodes = new NodeStateCollection();
nodes.AddNodeSetNodesOnlyPump(context);
using var stream = new MemoryStream();
nodes.SaveAsNodeSet2(context, stream);
stream.Position = 0;
return UANodeSet.Read(stream)!;
}
private static SystemContext CreateContext()
{
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = new NamespaceTable()
};
context.NamespaceUris.GetIndexOrAppend(kModelUri);
context.NamespaceUris.GetIndexOrAppend(global::Opc.Ua.Di.Namespaces.OpcUaDi);
return context;
}
private static UANodeSet ReadInput()
{
using Stream stream = typeof(NodeSetNodesOnlyAddressSpaceTests).Assembly
.GetManifestResourceStream(kResource)!;
return UANodeSet.Read(stream)!;
}
/// <summary>
/// Every node as its NodeClass, namespace qualified NodeId and
/// namespace qualified BrowseName.
/// </summary>
private static string[] Nodes(UANodeSet nodeSet)
{
return [.. nodeSet.Items!.Select(node =>
node.GetType().Name + " " +
Normalize(nodeSet, node.NodeId!) + " " +
NormalizeBrowseName(nodeSet, node.BrowseName!))];
}
/// <summary>
/// Every reference, wherever it is declared, as its forward form:
/// source, reference type and target, namespace qualified.
/// </summary>
private static string[] References(UANodeSet nodeSet)
{
var references = new HashSet<string>();
foreach (UANode node in nodeSet.Items!)
{
string source = Normalize(nodeSet, node.NodeId!);
foreach (Reference reference in node.References ?? [])
{
string type = Normalize(nodeSet, reference.ReferenceType!);
string target = Normalize(nodeSet, reference.Value!);
references.Add(reference.IsForward
? source + " " + type + " " + target
: target + " " + type + " " + source);
}
}
return [.. references];
}
/// <summary>
/// Every reference with an end in the model's namespace that is not a
/// node of the NodeSet.
/// </summary>
private static string[] UnresolvedModelReferences(UANodeSet nodeSet)
{
var nodeIds = new HashSet<string>(
nodeSet.Items!.Select(node => Normalize(nodeSet, node.NodeId!)));
var unresolved = new List<string>();
foreach (UANode node in nodeSet.Items!)
{
foreach (Reference reference in node.References ?? [])
{
string target = Normalize(nodeSet, reference.Value!);
if (target.StartsWith(kModelUri + ";", StringComparison.Ordinal) &&
!nodeIds.Contains(target))
{
unresolved.Add(Normalize(nodeSet, node.NodeId!) + " " +
Normalize(nodeSet, reference.ReferenceType!) + " " + target);
}
}
}
return [.. unresolved];
}
private static string Normalize(UANodeSet nodeSet, string nodeId)
{
NodeIdAlias? alias = nodeSet.Aliases?.FirstOrDefault(a => a.Alias == nodeId);
var parsed = NodeId.Parse(alias?.Value ?? nodeId);
return NamespaceUri(nodeSet, parsed.NamespaceIndex) + ";" +
parsed.WithNamespaceIndex(0);
}
private static string NormalizeBrowseName(UANodeSet nodeSet, string browseName)
{
var parsed = QualifiedName.Parse(browseName);
return NamespaceUri(nodeSet, parsed.NamespaceIndex) + ";" + parsed.Name;
}
private static string NamespaceUri(UANodeSet nodeSet, ushort namespaceIndex)
{
return namespaceIndex == 0
? global::Opc.Ua.Namespaces.OpcUa
: nodeSet.NamespaceUris![namespaceIndex - 1];
}
}
}
@@ -28,6 +28,30 @@
<ItemGroup>
<Compile Include="..\Common\Main.cs" />
</ItemGroup>
<!-- A DI companion NodeSet2 generated with EmitNodeSetNodesOnly: its
predefined nodes must be exactly the nodes of the NodeSet2, which is
embedded as well to compare against. -->
<PropertyGroup>
<ModelSourceGeneratorEmitNodeSetNodesOnly>true</ModelSourceGeneratorEmitNodeSetNodesOnly>
<ModelSourceGeneratorOmitFluentApi>true</ModelSourceGeneratorOmitFluentApi>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\NodeSetNodesOnlyPump.NodeSet2.xml">
<ModelSourceGeneratorModelUri>http://test.org/UA/NodeSetNodesOnlyPump/</ModelSourceGeneratorModelUri>
<ModelSourceGeneratorName>NodeSetNodesOnlyPump</ModelSourceGeneratorName>
<ModelSourceGeneratorPrefix>NodeSetNodesOnlyPump</ModelSourceGeneratorPrefix>
</AdditionalFiles>
<EmbeddedResource Include="Assets\NodeSetNodesOnlyPump.NodeSet2.xml">
<LogicalName>Opc.Ua.Di.Tests.Assets.NodeSetNodesOnlyPump.NodeSet2.xml</LogicalName>
</EmbeddedResource>
</ItemGroup>
<Import Project="..\..\tools\Opc.Ua.SourceGeneration\OPCFoundation.Opc.Ua.SourceGeneration.props" />
<ItemGroup>
<ProjectReference Include="..\..\tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Opc.Ua.Core\Opc.Ua.Core.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Client\Opc.Ua.Client.csproj" />
@@ -151,6 +151,26 @@ namespace Opc.Ua.SourceGeneration
/// </summary>
public bool UseTypeDefinitionModellingRules { get; set; }
/// <summary>
/// When set to <c>true</c>, the predefined nodes generated for a
/// NodeSet2 input are exactly the nodes the NodeSet2 declares. A
/// subtype in the NodeSet2 does not get copies of the Mandatory
/// InstanceDeclarations of its supertypes, and an instance does not
/// get children its NodeSet2 does not declare or references to them,
/// so the address space holds no node with an identifier the NodeSet2
/// does not assign.
/// <para>
/// The typed state classes and the <c>CreateInstanceOf*</c> factories
/// are not affected: inherited members still resolve through the
/// state classes of the supertypes, and an instance created at run
/// time still gets every Mandatory descendant of its type.
/// </para>
/// <para>
/// Off by default. A ModelDesign input is not affected.
/// </para>
/// </summary>
public bool EmitNodeSetNodesOnly { get; set; }
/// <summary>
/// Maps an OPC UA namespace URI (key) to the C# namespace (value)
/// in which the corresponding source-generated <c>*TypeClient</c>
@@ -2088,6 +2088,107 @@ namespace Opc.Ua.SourceGeneration
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 ||
@@ -2097,6 +2198,17 @@ namespace Opc.Ua.SourceGeneration
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
@@ -2228,16 +2340,51 @@ namespace Opc.Ua.SourceGeneration
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:
context.Out.WriteLine(
output.WriteLine(
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
instance.SymbolicId.Name,
forInstanceVariableValue);
return null;
return;
case InstanceDesign parentInstance:
if (HasFixedChildSlot(parentInstance.TypeDefinitionNode, instance.SymbolicName.Name) ||
IsBuiltInProperty(node))
@@ -2258,11 +2405,11 @@ namespace Opc.Ua.SourceGeneration
case ModellingRule.Mandatory:
case ModellingRule.Optional:
EmitCreateOrReplaceChild(
context,
output,
node,
instance,
forInstanceVariableValue);
return null;
return;
case ModellingRule.OptionalPlaceholder:
case ModellingRule.MandatoryPlaceholder:
// TODO
@@ -2279,11 +2426,10 @@ namespace Opc.Ua.SourceGeneration
}
}
context.Out.WriteLine(
output.WriteLine(
"state.AddChild(Create{0}(context, state, forInstance: {1}));",
instance.SymbolicId.Name,
forInstanceVariableValue);
return null;
}
/// <summary>
@@ -2301,7 +2447,7 @@ namespace Opc.Ua.SourceGeneration
/// rather than the type-level placeholders.
/// </summary>
private void EmitCreateOrReplaceChild(
ILoadContext context,
ITemplateWriter output,
NodeToGenerate node,
InstanceDesign instance,
string forInstanceVariableValue)
@@ -2313,7 +2459,7 @@ namespace Opc.Ua.SourceGeneration
string SingletonChildSymbolicId)> singletonChildren = FindSingletonChildren(node);
if (singletonChildren.Count == 0)
{
context.Out.WriteLine(
output.WriteLine(
"state.CreateOrReplace{0}(context, Create{1}(context, state, " +
"forInstance: {2}), assignInstanceNodeIds: false);",
instance.SymbolicName.Name,
@@ -2327,45 +2473,45 @@ namespace Opc.Ua.SourceGeneration
// materialising an actual well-known instance subtree. The
// <paramref name="forInstance"/> gate keeps the type-template
// emission path (forInstance: false) unchanged.
context.Out.WriteLine("if ({0} && parent != null)", forInstanceVariableValue);
context.Out.WriteLine("{");
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";
context.Out.WriteLine(
output.WriteLine(
" {0} (parent.NodeId.Equals({1}))",
keyword,
singletonRoot.Design.GetNodeIdAsCode(
m_context.ModelDesign.Namespaces,
kNamespaceTableContextVariable));
context.Out.WriteLine(" {");
context.Out.WriteLine(
output.WriteLine(" {");
output.WriteLine(
" state.CreateOrReplace{0}(context, Create{1}(context, state, " +
"forInstance: true), assignInstanceNodeIds: false);",
instance.SymbolicName.Name,
singletonChildSymbolicId);
context.Out.WriteLine(" }");
output.WriteLine(" }");
}
context.Out.WriteLine(" else");
context.Out.WriteLine(" {");
context.Out.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);
context.Out.WriteLine(" }");
context.Out.WriteLine("}");
context.Out.WriteLine("else");
context.Out.WriteLine("{");
context.Out.WriteLine(
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);
context.Out.WriteLine("}");
output.WriteLine("}");
}
private bool WriteTemplate_RolePermissions(IWriteContext context)
@@ -4242,6 +4388,9 @@ namespace Opc.Ua.SourceGeneration
}
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)
{
@@ -4311,6 +4460,11 @@ namespace Opc.Ua.SourceGeneration
if (!target.ExplicitlyDefined && node.RootIsTypeDefinition)
{
continue;
}
if (omitsUndeclaredNodes &&
!IsCreatedInNodeSetAddressSpace(node, reference.TargetPath!))
{
continue;
}
@@ -4328,6 +4482,11 @@ namespace Opc.Ua.SourceGeneration
{
continue;
}
if (omitsUndeclaredNodes &&
!IsCreatedInNodeSetAddressSpace(node, reference.SourcePath!))
{
continue;
}
references.Add(new ReferenceToGenerate(
source.Instance!,
reference.ReferenceType!,
@@ -89,6 +89,13 @@ namespace Opc.Ua.Schema.Model
/// <inheritdoc/>
public bool UseAllowSubtypes { get; set; }
/// <summary>
/// Whether the target model was read from a NodeSet2 rather than
/// from a ModelDesign. Internal provenance for the generators; it is
/// deliberately not part of <see cref="IModelDesign"/>.
/// </summary>
internal bool IsSourceNodeSet { get; private set; }
/// <inheritdoc/>
public NamespaceTable NamespaceUris { get; private set; } = null!;
@@ -431,6 +438,7 @@ namespace Opc.Ua.Schema.Model
namespaces,
designFilePaths[0],
identifierFilePath);
IsSourceNodeSet = targetModel.IsSourceNodeSet;
for (int ii = targetIndex - 1; ii >= 0; ii--)
{
@@ -0,0 +1,112 @@
/* ========================================================================
* Copyright (c) 2005-2026 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.Globalization;
namespace Opc.Ua.SourceGeneration
{
/// <summary>
/// Writes every line to an inner writer indented by four more spaces,
/// for code emitted into a nested block.
/// </summary>
internal sealed class IndentedTemplateWriter : ITemplateWriter
{
/// <summary>
/// Create the writer
/// </summary>
public IndentedTemplateWriter(ITemplateWriter inner)
{
m_inner = inner;
}
/// <inheritdoc/>
public void Write(char text)
{
Write(text.ToString());
}
/// <inheritdoc/>
public void Write(string text)
{
if (m_atLineStart && !string.IsNullOrEmpty(text))
{
m_inner.Write(kIndent);
m_atLineStart = false;
}
m_inner.Write(text);
}
/// <inheritdoc/>
public void Write(string format, object arg1)
{
Write(string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, arg1));
}
/// <inheritdoc/>
public void Write(string format, object arg1, object arg2)
{
Write(string.Format(CultureInfo.InvariantCulture, format ?? string.Empty, arg1, arg2));
}
/// <inheritdoc/>
public void Write(string format, object arg1, object arg2, object arg3)
{
Write(string.Format(
CultureInfo.InvariantCulture, format ?? string.Empty, arg1, arg2, arg3));
}
/// <inheritdoc/>
public void WriteLine()
{
m_inner.WriteLine();
m_atLineStart = true;
}
/// <inheritdoc/>
public void WriteLine(string text)
{
if (m_atLineStart && !string.IsNullOrEmpty(text))
{
m_inner.Write(kIndent);
}
m_inner.WriteLine(text);
m_atLineStart = true;
}
/// <inheritdoc/>
public void WriteLine(string text, params object?[] args)
{
WriteLine(string.Format(CultureInfo.InvariantCulture, text ?? string.Empty, args ?? []));
}
private const string kIndent = " ";
private readonly ITemplateWriter m_inner;
private bool m_atLineStart = true;
}
}
@@ -123,6 +123,7 @@ namespace Opc.Ua.SourceGeneration
: m_options.ObjectTypeProxyNamespace,
UseTypeDefinitionModellingRules =
m_options.UseTypeDefinitionModellingRules,
EmitNodeSetNodesOnly = m_options.EmitNodeSetNodesOnly,
EmitDependencyMetadata = ResolveEmitDependencyMetadata(),
OmitFluentApi = m_options.OmitFluentApi,
FluentAccessorsOnly = m_options.FluentAccessorsOnly,
@@ -95,6 +95,15 @@ namespace Opc.Ua.SourceGeneration
/// </summary>
public bool UseTypeDefinitionModellingRules { get; set; }
/// <summary>
/// When set to <c>true</c>, the predefined nodes generated for a
/// NodeSet2 input are exactly the nodes of the NodeSet2. See
/// <c>GeneratorOptions.EmitNodeSetNodesOnly</c>.
/// Surfaced from MSBuild via the
/// <c>ModelSourceGeneratorEmitNodeSetNodesOnly</c> property.
/// </summary>
public bool EmitNodeSetNodesOnly { get; set; }
/// <summary>
/// Override for whether the generator emits the
/// <c>[assembly: ModelDependencyAttribute]</c> + corresponding
@@ -211,6 +220,8 @@ namespace Opc.Ua.SourceGeneration
nameof(ObjectTypeProxyNamespace)),
UseTypeDefinitionModellingRules = provider.GlobalOptions.GetBool(
nameof(UseTypeDefinitionModellingRules)),
EmitNodeSetNodesOnly = provider.GlobalOptions.GetBool(
nameof(EmitNodeSetNodesOnly)),
EmitDependencyMetadata = ParseEmitMode(provider.GlobalOptions.GetString(
nameof(EmitDependencyMetadata))),
OmitFluentApi = provider.GlobalOptions.GetBool(
@@ -11,6 +11,7 @@
<CompilerVisibleProperty Include="ModelSourceGeneratorOmitObjectTypeProxies" />
<CompilerVisibleProperty Include="ModelSourceGeneratorObjectTypeProxyNamespace" />
<CompilerVisibleProperty Include="ModelSourceGeneratorUseTypeDefinitionModellingRules" />
<CompilerVisibleProperty Include="ModelSourceGeneratorEmitNodeSetNodesOnly" />
<CompilerVisibleProperty Include="ModelSourceGeneratorGenerateNodeManager" />
<CompilerVisibleProperty Include="ModelSourceGeneratorEmitDependencyMetadata" />
<CompilerVisibleProperty Include="ModelSourceGeneratorOmitFluentApi" />
+1
View File
@@ -220,6 +220,7 @@ the consuming project:
| `ModelSourceGeneratorOmitObjectTypeProxies` | Set to `true` to suppress emission of `*TypeClient` proxies. By default proxies are emitted for every `ObjectType` in the model alongside the standard model output (constants, NodeIds, NodeStates, DataTypes, schemas). |
| `ModelSourceGeneratorObjectTypeProxyNamespace` | Optional. Overrides the C# namespace used for the emitted proxy classes. Defaults to the model's own namespace. |
| `ModelSourceGeneratorUseTypeDefinitionModellingRules` | Set to `true` to make instance code generation use the modelling rules from the referenced type definition unconditionally, rather than the overridden rules on the instance. Off by default; enabled by default for stack generation. **Note:** regardless of this setting, the generator always enforces OPC UA modelling rule promotion semantics — instances may only *promote* the type definition's rule (`Optional→Mandatory`, `OptionalPlaceholder→Mandatory\|MandatoryPlaceholder`, `MandatoryPlaceholder→Mandatory`); demotions are silently rejected. |
| `ModelSourceGeneratorEmitNodeSetNodesOnly` | Set to `true` to make the predefined nodes generated for a NodeSet2 input exactly the nodes of the NodeSet2. A subtype then does not get copies of the Mandatory InstanceDeclarations of its supertypes (with NodeIds the NodeSet2 never assigned), and an instance does not get children its NodeSet2 does not declare, or references to them. Typed state classes and the `CreateInstanceOf*` factories are not affected: an instance created at run time still gets every Mandatory descendant of its type. Off by default; ModelDesign inputs are not affected. |
Example (suppress proxy emission for a model that does not need them):