Import NodeSet2 Views as standalone top-level nodes in the source generator (#4259)

## Problem

The NodeState source generator mishandled `UAView` nodes imported from a
NodeSet2 model. When an exporter set a View's `ParentNodeId` to an
organizing folder, the importer/generator absorbed the View into that
folder's `Children` and tried to emit an invalid `AddChild(...)` call.
Because `ViewState` derives from `NodeState` (not `BaseInstanceState`),
a View can never be an `AddChild` component, so the generated code was
invalid.

## Fix

Make Views standalone top-level predefined nodes linked purely via
`Organizes` references — mirroring how `DataType`/`ReferenceType` type
nodes and `DataTypeEncoding` are handled:

-
**`tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs`**
— clear the `UAView` `ParentNodeId` so the View stays a standalone
top-level node, keep the organizing folder's forward `Organizes`
references to those Views, and carry the View's `ContainsNoLoops`
attribute through to the `ViewDesign`.
-
**`tools/Opc.Ua.SourceGeneration.Core/Generators/NodeStateGenerator.cs`**
— return `null` for `ViewDesign` in the `AddChild` and `ReplaceChild`
template paths so Views are never emitted as components.
-
**`tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/NodeSetToModelDesignTests.cs`**
— new `ViewImport` NodeSet constant and
`ImportPreservesFolderOrganizesReferencesToViews` test verifying Views
are imported as top-level nodes while the organizing folder retains its
forward `Organizes` references to them.

## Validation

- `dotnet build tools\Opc.Ua.SourceGeneration.Core` (Release): 0
warnings / 0 errors.
- `dotnet test tests\Opc.Ua.SourceGeneration.Core.Tests` (net8.0,
filtered to `NodeSetToModelDesign`): all 17 tests pass, including the
new `ImportPreservesFolderOrganizesReferencesToViews`.

Extracted from the larger PR #4220 work — this PR contains only the
source-generator View fix (no WriteMask, UserAccessLevel, NodeSet2.xml,
or ReferenceNodeManager changes).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
romanett
2026-08-14 07:57:06 -07:00
committed by GitHub
co-authored by Copilot App
parent 2c2bfa7080
commit cc4791e2fd
3 changed files with 129 additions and 2 deletions
@@ -62,6 +62,70 @@ namespace Opc.Ua.Schema.Model.Tests
private const string SameNamedArgumentsResource =
"SameNamedMethodArguments.NodeSet2.xml";
private const string DesignResource = "TestDataDesign.xml";
private const string ViewImportNodeSet = """
<?xml version="1.0" encoding="utf-8"?>
<UANodeSet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
<NamespaceUris>
<Uri>http://test.org/UA/ViewImport/</Uri>
</NamespaceUris>
<Models>
<Model ModelUri="http://test.org/UA/ViewImport/"
PublicationDate="2026-08-12T00:00:00Z"
Version="1.0.0" />
</Models>
<Aliases>
<Alias Alias="HasSubtype">i=45</Alias>
<Alias Alias="HasTypeDefinition">i=40</Alias>
<Alias Alias="Organizes">i=35</Alias>
</Aliases>
<UAReferenceType NodeId="i=33" BrowseName="HierarchicalReferences" IsAbstract="true">
<DisplayName>HierarchicalReferences</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=33</Reference>
</References>
</UAReferenceType>
<UAReferenceType NodeId="i=35" BrowseName="Organizes">
<DisplayName>Organizes</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=33</Reference>
</References>
</UAReferenceType>
<UAObjectType NodeId="i=58" BrowseName="BaseObjectType">
<DisplayName>BaseObjectType</DisplayName>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=58</Reference>
</References>
</UAObjectType>
<UAObject NodeId="ns=1;s=Views" BrowseName="1:Views">
<DisplayName>Views</DisplayName>
<References>
<Reference ReferenceType="Organizes">ns=1;s=Views_Operations</Reference>
<Reference ReferenceType="Organizes">ns=1;s=Views_Engineering</Reference>
<Reference ReferenceType="HasTypeDefinition">i=58</Reference>
</References>
</UAObject>
<UAView NodeId="ns=1;s=Views_Operations"
BrowseName="1:Operations"
ParentNodeId="ns=1;s=Views"
ContainsNoLoops="true">
<DisplayName>Operations</DisplayName>
<References>
<Reference ReferenceType="Organizes" IsForward="false">i=87</Reference>
</References>
</UAView>
<UAView NodeId="ns=1;s=Views_Engineering"
BrowseName="1:Engineering"
ParentNodeId="ns=1;s=Views"
ContainsNoLoops="true">
<DisplayName>Engineering</DisplayName>
<References>
<Reference ReferenceType="Organizes" IsForward="false">i=87</Reference>
</References>
</UAView>
</UANodeSet>
""";
private VirtualFileSystem m_fileSystem;
@@ -291,6 +355,41 @@ namespace Opc.Ua.Schema.Model.Tests
});
}
/// <summary>
/// Verifies importing views as top-level nodes keeps their organizing folder references.
/// </summary>
[Test]
public void ImportPreservesFolderOrganizesReferencesToViews()
{
const string path = "memory://ViewImport.NodeSet2.xml";
m_fileSystem.Add(path, Encoding.UTF8.GetBytes(ViewImportNodeSet));
var settings = new NodeSetReaderSettings();
NodeSetToModelDesign importer = new(
m_fileSystem,
path,
settings,
CreateTelemetry());
ModelDesign model = importer.Import("ViewImport", "ViewImport");
ObjectDesign viewsFolder = model.Items
.OfType<ObjectDesign>()
.Single(x => x.SymbolicName?.Name == "Views");
string[] expectedViews = ["Operations", "Engineering"];
Assert.Multiple(() =>
{
Assert.That(model.Items.OfType<ViewDesign>().Select(x => x.SymbolicName?.Name), Is.EquivalentTo(
expectedViews));
Assert.That(viewsFolder.Children?.Items, Is.Null.Or.Empty);
Assert.That(viewsFolder.References, Has.Length.EqualTo(2));
Assert.That(viewsFolder.References.Select(x => x.ReferenceType.Name), Is.All.EqualTo("Organizes"));
Assert.That(viewsFolder.References.Select(x => x.IsInverse), Is.All.False);
Assert.That(viewsFolder.References.Select(x => x.TargetId.Name), Is.EquivalentTo(
expectedViews));
});
}
[Test]
public void TypeSymbolicNameUsesNodeIdNamespace()
{
@@ -1319,6 +1319,14 @@ namespace Opc.Ua.SourceGeneration
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;
@@ -1529,7 +1537,8 @@ namespace Opc.Ua.SourceGeneration
private TemplateString LoadTemplate_ReplaceChild(ILoadContext context)
{
if (context.Target is not NodeToGenerate node ||
node.Design is not InstanceDesign instance)
node.Design is not InstanceDesign instance ||
node.Design is ViewDesign)
{
return null;
}
@@ -950,6 +950,7 @@ namespace Opc.Ua.Schema.Model
return;
}
output.SupportsEvents = (input.EventNotifier & EventNotifiers.SubscribeToEvents) != 0;
output.ContainsNoLoops = input.ContainsNoLoops;
}
private void UpdateVariableDesign(UAVariable input, VariableDesign output)
@@ -1600,7 +1601,10 @@ namespace Opc.Ua.Schema.Model
continue;
}
if (targetId.NamespaceIndex != nodeId.NamespaceIndex ||
if ((ii.IsForward &&
referenceTypeId == ReferenceTypeIds.Organizes &&
target is ViewDesign) ||
targetId.NamespaceIndex != nodeId.NamespaceIndex ||
IsTypeOf(referenceTypeId, ReferenceTypeIds.NonHierarchicalReferences))
{
references.Add(new Reference
@@ -1953,6 +1957,21 @@ namespace Opc.Ua.Schema.Model
if (node is UAInstance instance)
{
// View nodes are independent address-space nodes even when an
// exporter sets ParentNodeId to an organizing folder. ViewState
// derives from NodeState (not BaseInstanceState) and therefore
// cannot be added as an AddChild component. Keeping the parent
// would absorb the view into the folder's Children, exclude it
// from the top-level model items and make the generator emit an
// invalid AddChild(...) call. Clearing the parent keeps the view
// a standalone predefined node linked purely via Organizes
// references (mirrors the DataTypeEncoding handling below and the
// way DataType/ReferenceType type nodes are modelled).
if (node is UAView)
{
instance.ParentNodeId = null;
}
// ensure parents are in the same namespace.
if (instance.ParentNodeId != null)
{