Files
UA-.NETStandard/tests/Opc.Ua.Core.Tests/Stack/Schema/UANodeSetHelpersTests.cs
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# Description

Nullable reference types are now enabled once, in `common.props`, and
every project in the repository builds warning-clean with them.

**Configuration**
- `<Nullable>enable</Nullable>` lives in `common.props`. All per-project
`<Nullable>` settings are removed: 178 project files (enable,
annotations, and the `disable` in the three Aot test projects).
- 419 per-file `#nullable enable`/`restore` directives and the 4
`#nullable disable` directives are removed.
- Kept on purpose:
- The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type
Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`,
`PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the
project setting in generated code, so the directive is required there.
- `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against
the 1.5.378 packages and does not import the repository props.
- `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now
describe the central setting.

**Migration** (the 43 unset and 13 annotations-only projects, roughly
10k diagnostics)
- **Production code: annotated at the declarations.** Covers
`tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the
LDS sample.
- Members, parameters and returns are nullable where null actually
flows.
- Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`,
`IsMethodTypeDesign`).
- `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take
`params object?[]`. `Template.AddReplacement` takes `string?` (rendering
already skips null replacements).
- `IRegisteredServerStore.RegisterAsync` returns
`Task<RegistrationEntry?>`: it returns null when a registration is
removed.
- Dereferences backed by an invariant (for example, the validated model)
use `!`, so runtime behaviour is unchanged.
- **Behavioural changes in production code** (small, deliberate):
- `NodeSetToModelDesign` throws `InvalidOperationException` when the
file is not a NodeSet. Before, it threw a NullReferenceException on the
next line.
- `ModelDesignValidator` starts its lookup tables empty instead of null.
A redundant `if (m_nodes == null) return;` guard is gone.
- **Test code: minimal annotations.**
  - `?` on values that can be null, `!` where the test asserts non-null.
  - `= null!` for fixture members assigned in SetUp.
- `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the
`IEncodeable` constraint.
- A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`)
folded into the adjacent `IsNull`/`Count` checks.

**Notes for reviewers**
- The diff is large (~1,500 files) but almost entirely mechanical: `?`,
`!` and directive removal. The non-annotation changes are listed above.
- Known follow-up, not done here:
`IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay
non-nullable even though the validator returns null before it loads a
dictionary. Making them nullable would ripple through every generator
call site.

## Related Issues

- Fixes #4439

## Checklist

- [ ] 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.
- [ ] 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.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

The "no new warnings" box covers net10.0 and net48: full non-incremental
builds of `UA.slnx`, `tools/SourceGeneration.slnx`,
`tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0
warnings and 0 errors under the repository's warnings-as-errors
settings. net9.0 and net8.0 also had no nullable diagnostics, but they
were not rebuilt after the final fixes. Tests have not been run locally
yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:48:13 +02:00

1148 lines
56 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.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Tests.Stack.Schema
{
/// <summary>
/// Tests for the UANodeSet helper.
/// </summary>
[TestFixture]
[Category("UANodeSet")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public class UANodeSetHelpersTests
{
/// <summary>
/// A NodeSet writes a Variable's value as the typed element alone, but
/// the Variant XML encoding nests that element inside a <c>Value</c>
/// element. Importing the bare element found no <c>Value</c> to begin
/// and silently produced a null Variant, so every value in every
/// imported NodeSet was lost — browsable nodes that read back empty.
/// </summary>
[Test]
public void ImportRestoresVariableValues()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string nodeSetXml = @"
<UANodeSet xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>urn:test:values</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='String'>i=12</Alias>
<Alias Alias='Boolean'>i=1</Alias>
</Aliases>
<UAVariable NodeId='ns=1;s=Text' BrowseName='1:Text' DataType='String'>
<DisplayName>Text</DisplayName>
<Value>
<uax:String xmlns:uax='http://opcfoundation.org/UA/2008/02/Types.xsd'>hello</uax:String>
</Value>
</UAVariable>
<UAVariable NodeId='ns=1;s=Flag' BrowseName='1:Flag' DataType='Boolean'>
<DisplayName>Flag</DisplayName>
<Value>
<uax:Boolean xmlns:uax='http://opcfoundation.org/UA/2008/02/Types.xsd'>true</uax:Boolean>
</Value>
</UAVariable>
</UANodeSet>";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(nodeSetXml));
Export.UANodeSet nodeSet = Export.UANodeSet.Read(stream)!;
var context = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in nodeSet!.NamespaceUris!)
{
context.NamespaceUris.Append(namespaceUri);
}
var imported = new NodeStateCollection();
nodeSet.Import(context, imported);
BaseVariableState text = imported.OfType<BaseVariableState>()
.Single(node => node.BrowseName.Name == "Text");
BaseVariableState flag = imported.OfType<BaseVariableState>()
.Single(node => node.BrowseName.Name == "Flag");
Assert.That(text.WrappedValue.IsNull, Is.False, "The String value must survive the import.");
Assert.That(text.WrappedValue.GetString(), Is.EqualTo("hello"));
Assert.That(flag.WrappedValue.IsNull, Is.False, "The Boolean value must survive the import.");
Assert.That(flag.WrappedValue.GetBoolean(), Is.True);
}
/// <summary>
/// The value element was copied with ImportNode(deep), which recurses
/// once per element level, so a deeply nested XmlElement value in a
/// NodeSet exhausted the stack. The copy no longer recurses and the
/// decoder bounds the element depth.
/// </summary>
[Test]
public void ImportRejectsDeeplyNestedXmlElementValue()
{
string nested = string.Concat(Enumerable.Repeat("<a>", 100_000)) +
string.Concat(Enumerable.Repeat("</a>", 100_000));
ServiceResultException ex = Assert.Throws<ServiceResultException>(
() => ImportXmlElementValue(nested));
Assert.That(ex.StatusCode, Is.EqualTo(StatusCodes.BadEncodingLimitsExceeded));
}
[Test]
public void ImportRestoresXmlElementValue()
{
BaseVariableState variable = ImportXmlElementValue("<a x='1'><b>text</b></a>");
Assert.That(variable.WrappedValue.TryGetValue(out XmlElement value), Is.True);
// the content inherits the default namespace of the NodeSet.
Assert.That(value, Is.EqualTo(XmlElement.From(
"<a x=\"1\" xmlns=\"http://opcfoundation.org/UA/2011/03/UANodeSet.xsd\">" +
"<b>text</b></a>")));
}
private static BaseVariableState ImportXmlElementValue(string content)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
string nodeSetXml = @"
<UANodeSet xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>urn:test:values</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='XmlElement'>i=16</Alias>
</Aliases>
<UAVariable NodeId='ns=1;s=Xml' BrowseName='1:Xml' DataType='XmlElement'>
<DisplayName>Xml</DisplayName>
<Value>
<uax:XmlElement xmlns:uax='http://opcfoundation.org/UA/2008/02/Types.xsd'>" +
content +
@"</uax:XmlElement>
</Value>
</UAVariable>
</UANodeSet>";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(nodeSetXml));
Export.UANodeSet nodeSet = Export.UANodeSet.Read(stream)!;
var context = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in nodeSet!.NamespaceUris!)
{
context.NamespaceUris.Append(namespaceUri);
}
var imported = new NodeStateCollection();
nodeSet.Import(context, imported);
return imported.OfType<BaseVariableState>().Single();
}
/// <summary>
/// Test Structure Field ArrayDimensions attribute is correctly imported respectively exported
/// </summary>
[Test]
public void ArrayDimensionsValidationTest()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string bufferPath = "./ArrayDimensionsValidationTest.xml";
const string importBuffer =
@"<?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' LastModified='2021-09-16T19:10:18.097476Z' xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>urn:foobar</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasSubtype'>i=45</Alias>
<Alias Alias='HasEncoding'>i=38</Alias>
</Aliases>
<UADataType NodeId='ns=1;s=Simple Structure' BrowseName='Simple Structure'>
<DisplayName>Simple Structure</DisplayName>
<References>
<Reference ReferenceType='HasEncoding'>ns=1;s=Simple Structure Encoding</Reference>
<Reference ReferenceType='HasSubtype' IsForward='false'>i=22</Reference>
</References>
<Definition Name='Simple Structure' IsUnion='true'>
<Field Name='Duration Field' DataType='i=290' />
<Field Name='Double Field' DataType='i=11' />
</Definition>
</UADataType>
<UADataType NodeId='ns=1;s=Complex Structure' BrowseName='Complex Structure'>
<DisplayName>Complex Structure</DisplayName>
<References>
<Reference ReferenceType='HasEncoding'>ns=1;s=Complex Structure Encoding</Reference>
<Reference ReferenceType='HasSubtype' IsForward='false'>i=22</Reference>
</References>
<Definition Name='Complex Structure'>
<Field Name='Scalar Structure' DataType='i=22' />
<Field Name='Scalar BuildInfo' DataType='i=338' />
<Field Name='Scalar Simple Structure' DataType='ns=1;s=Simple Structure' />
<Field Name='Scalar Boolean' DataType='i=1' />
<Field Name='Scalar Duration' DataType='i=290' />
<Field Name='Scalar String within max length' DataType='i=12' MaxStringLength='256' />
<Field Name='1D Array String no max length' DataType='i=12' ValueRank='1' />
<Field Name='1D Array String within max length' DataType='i=12' ValueRank='1' MaxStringLength='256' />
<Field Name='1D Array of Simple Structure 1' DataType='ns=1;s=Simple Structure' ValueRank='1' ArrayDimensions='2' />
<Field Name='1D Array of Simple Structure 2' DataType='ns=1;s=Simple Structure' ValueRank='1' ArrayDimensions='3' />
<Field Name='1D Array of BuildInfo' DataType='i=338' ValueRank='1' />
<Field Name='1D Array of Simple Structure' DataType='ns=1;s=Simple Structure' ValueRank='1' />
<Field Name='1D Array of Boolean' DataType='i=1' ValueRank='1' />
<Field Name='1D Array of Duration' DataType='i=290' ValueRank='1' />
<Field Name='1D Array of MessageSecurityMode' DataType='i=302' ValueRank='1' />
<Field Name='2D Array of Structure' DataType='i=22' ValueRank='2' ArrayDimensions='2,3' />
<Field Name='2D Array of BuildInfo' DataType='i=338' ValueRank='2' ArrayDimensions='2,3' />
<Field Name='2D Array of Simple Structure' DataType='ns=1;s=Simple Structure' ValueRank='2' ArrayDimensions='2,3' />
<Field Name='2D Array of Boolean' DataType='i=1' ValueRank='2' ArrayDimensions='2,3' />
<Field Name='2D Array of Duration' DataType='i=290' ValueRank='2' ArrayDimensions='2,3' />
<Field Name='2D Array of MessageSecurityMode' DataType='i=302' ValueRank='2' ArrayDimensions='2,3' />
</Definition>
</UADataType>
</UANodeSet>";
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
var importedNodeSet = Export.UANodeSet.Read(importStream);
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
importedNodeSet.Import(localContext, importedNodeStates);
Assert.That(importedNodeSet.NamespaceUris, Has.Length.EqualTo(1));
Assert.That(importedNodeSet.Items, Has.Length.EqualTo(2));
var dataType1 = importedNodeSet.Items[0] as Export.UADataType;
var dataType2 = importedNodeSet.Items[1] as Export.UADataType;
Assert.That(dataType1, Is.Not.Null);
Assert.That(dataType1.Definition!.Field, Has.Length.EqualTo(2));
Assert.IsEmpty(dataType1.Definition.Field[0].ArrayDimensions);
Assert.That(dataType1.Definition.IsUnion, Is.True);
Assert.That(dataType2, Is.Not.Null);
Assert.That(dataType2.Definition!.IsUnion, Is.False);
Assert.That(dataType2.Definition.Field, Has.Length.EqualTo(21));
Assert.That(dataType2.Definition.Field[15].ArrayDimensions, Is.EqualTo("2,3"));
Assert.That(dataType2.Definition.Field[5].MaxStringLength, Is.EqualTo(256));
// export the nodeSet to a file, reimport it and re-test.
using (var fileStream = new FileStream(bufferPath, FileMode.Create))
{
importedNodeStates.SaveAsNodeSet2(localContext, fileStream);
}
try
{
using var exportStream = new FileStream(bufferPath, FileMode.Open);
var exportedNodeSet = Export.UANodeSet.Read(exportStream);
var exportedNodeStates = new NodeStateCollection();
localContext.NamespaceUris = new NamespaceTable();
foreach (string namespaceUri in exportedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
exportedNodeSet.Import(localContext, exportedNodeStates);
Assert.That(exportedNodeSet.NamespaceUris, Has.Length.EqualTo(1));
Assert.That(exportedNodeSet.Items, Has.Length.EqualTo(2));
dataType1 = exportedNodeSet.Items[0] as Export.UADataType;
dataType2 = exportedNodeSet.Items[1] as Export.UADataType;
Assert.That(dataType1, Is.Not.Null);
Assert.That(dataType1.Definition!.Field, Has.Length.EqualTo(2));
Assert.IsEmpty(dataType1.Definition.Field[0].ArrayDimensions);
Assert.That(dataType1.Definition.IsUnion, Is.True);
Assert.That(dataType2, Is.Not.Null);
Assert.That(dataType2.Definition!.IsUnion, Is.False);
Assert.That(dataType2.Definition.Field, Has.Length.EqualTo(21));
Assert.That(dataType2.Definition.Field[15].ArrayDimensions, Is.EqualTo("2,3"));
Assert.That(dataType2.Definition.Field[5].MaxStringLength, Is.EqualTo(256));
}
finally
{
File.Delete(bufferPath);
}
}
/// <summary>
/// Test that parent-child references are correctly established after importing a NodeSet2.
/// </summary>
[TestCase("HasComponent")]
[TestCase("Organizes")]
public void ParentChildReferencesTest(string objectReferenceType)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
string importBuffer =
@"<?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'
LastModified='2024-01-01T00:00:00.000Z'
xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>http://opcfoundation.org/UA/Test</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasProperty'>i=46</Alias>
<Alias Alias='Organizes'>i=35</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAObject NodeId='ns=1;i=1000' BrowseName='1:ParentObject'>
<DisplayName>ParentObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
<Reference ReferenceType='HasProperty'>ns=1;i=1001</Reference>
<Reference ReferenceType='HasComponent'>ns=1;i=1002</Reference>
</References>
</UAObject>
<UAVariable DataType='i=12' ParentNodeId='ns=1;i=1000' NodeId='ns=1;i=1001' BrowseName='1:ChildProperty' ValueRank='-1'>
<DisplayName>ChildProperty</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=68</Reference>
<Reference ReferenceType='HasProperty' IsForward='false'>ns=1;i=1000</Reference>
</References>
</UAVariable>
<UAObject ParentNodeId='ns=1;i=1000' NodeId='ns=1;i=1002' BrowseName='1:ChildObject'>
<DisplayName>ChildObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=1000</Reference>
</References>
</UAObject>
</UANodeSet>";
importBuffer = importBuffer.Replace(
"ReferenceType='HasComponent'",
$"ReferenceType='{objectReferenceType}'",
StringComparison.Ordinal);
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
var importedNodeSet = Export.UANodeSet.Read(importStream);
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
// Verify that all nodes were imported
Assert.That(importedNodeStates, Has.Count.EqualTo(3));
// Find the parent object
BaseObjectState? parentObject = null;
BaseVariableState? childProperty = null;
BaseObjectState? childObject = null;
foreach (NodeState node in importedNodeStates)
{
if (node.BrowseName.Name == "ParentObject")
{
parentObject = (node as BaseObjectState)!;
}
else if (node.BrowseName.Name == "ChildProperty")
{
childProperty = (node as BaseVariableState)!;
}
else if (node.BrowseName.Name == "ChildObject")
{
childObject = (node as BaseObjectState)!;
}
}
Assert.That(parentObject, Is.Not.Null, "ParentObject should be imported");
Assert.That(childProperty, Is.Not.Null, "ChildProperty should be imported");
Assert.That(childObject, Is.Not.Null, "ChildObject should be imported");
// Verify parent-child relationships are established
Assert.That(childProperty.Parent, Is.EqualTo(parentObject), "ChildProperty's Parent should be set to ParentObject");
Assert.That(childObject.Parent, Is.EqualTo(parentObject), "ChildObject's Parent should be set to ParentObject");
// Verify GetChildren works
var children = new List<BaseInstanceState>();
parentObject.GetChildren(localContext, children);
Assert.That(children, Has.Count.EqualTo(2), "ParentObject should have 2 children");
Assert.That(children, Does.Contain(childProperty), "Children should contain ChildProperty");
Assert.That(children, Does.Contain(childObject), "Children should contain ChildObject");
NodeId objectReferenceId = objectReferenceType == "Organizes"
? ReferenceTypeIds.Organizes
: ReferenceTypeIds.HasComponent;
Assert.That(
BrowseReferenceTypes(localContext, childProperty, parentObject.NodeId),
Is.EquivalentTo(new[] { ReferenceTypeIds.HasProperty }));
Assert.That(
BrowseReferenceTypes(localContext, childObject, parentObject.NodeId),
Is.EquivalentTo(new[] { objectReferenceId }));
Assert.That(
BrowseReferenceTypes(localContext, parentObject, childProperty.NodeId),
Is.EquivalentTo(new[] { ReferenceTypeIds.HasProperty }));
Assert.That(
BrowseReferenceTypes(localContext, parentObject, childObject.NodeId),
Is.EquivalentTo(new[] { objectReferenceId }));
}
/// <summary>
/// A node whose declared parent is not part of the same import batch —
/// because it lives in another NodeSet or another NodeManager — must
/// not have that parent silently discarded. The caller needs it so it
/// can be wired as an external reference.
/// </summary>
[Test]
public void ImportKeepsAParentThatIsNotInTheSameBatch()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string importBuffer =
@"<?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'
LastModified='2024-01-01T00:00:00.000Z'
xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>http://opcfoundation.org/UA/Test</Uri>
<Uri>http://opcfoundation.org/UA/Elsewhere</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAObject ParentNodeId='ns=2;i=5001' NodeId='ns=1;i=1001' BrowseName='1:Orphan'>
<DisplayName>Orphan</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=2;i=5001</Reference>
</References>
</UAObject>
</UANodeSet>";
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
Export.UANodeSet importedNodeSet = Export.UANodeSet.Read(importStream)!;
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
Assert.That(importedNodeStates, Has.Count.EqualTo(1));
var orphan = (BaseInstanceState)importedNodeStates[0];
Assert.That(orphan.Parent, Is.Null,
"The parent is not in this batch, so it cannot be linked as an in-memory parent.");
NodeId expectedParent = new(5001u, (ushort)localContext.NamespaceUris.GetIndex(
"http://opcfoundation.org/UA/Elsewhere"));
Assert.Multiple(() => {
Assert.That(
Export.UANodeSet.TryGetUnresolvedParentNodeId(orphan, out NodeId unresolvedParent),
Is.True,
"The declared parent must survive the import so a caller can wire it " +
"as an external reference.");
Assert.That(unresolvedParent, Is.EqualTo(expectedParent));
});
}
/// <summary>
/// The record must be precise: a parent that was linked normally is not
/// an unresolved parent, or a caller would wire references that already
/// exist.
/// </summary>
[Test]
public void ImportRecordsNoUnresolvedParentWhenTheParentIsInTheBatch()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string importBuffer =
@"<?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'
LastModified='2024-01-01T00:00:00.000Z'
xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>http://opcfoundation.org/UA/Test</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAObject NodeId='ns=1;i=1000' BrowseName='1:ParentObject'>
<DisplayName>ParentObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
</References>
</UAObject>
<UAObject ParentNodeId='ns=1;i=1000' NodeId='ns=1;i=1002' BrowseName='1:ChildObject'>
<DisplayName>ChildObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=1000</Reference>
</References>
</UAObject>
</UANodeSet>";
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
Export.UANodeSet importedNodeSet = Export.UANodeSet.Read(importStream)!;
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
foreach (NodeState node in importedNodeStates)
{
Assert.Multiple(() => {
Assert.That(
Export.UANodeSet.TryGetUnresolvedParentNodeId(node, out NodeId unresolvedParent),
Is.False,
$"'{node.BrowseName.Name}' resolved inside the batch, so nothing is unresolved.");
Assert.That(unresolvedParent.IsNull, Is.True);
});
}
}
/// <summary>
/// Imported Method argument Variables must be adopted through
/// <see cref="MethodState"/>'s declared children so Call validation
/// sees the authored argument definitions.
/// </summary>
[Test]
public void ImportBindsMethodArgumentProperties(
[Values(true, false)] bool includeParentHints,
[Values(true, false)] bool parentFirst,
[Values("Forward", "Inverse", "Both")] string referenceDirection,
[Values(true, false)] bool useNamespaceUriTargets)
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string importBuffer =
@"<?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:uax='http://opcfoundation.org/UA/2008/02/Types.xsd'
xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>urn:test:method-arguments</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='Argument'>i=296</Alias>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasProperty'>i=46</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAVariable NodeId='ns=1;i=1003' BrowseName='1:InputArguments'
ParentNodeId='ns=1;i=1000' DataType='i=12'
ValueRank='-1'>
<DisplayName>Custom InputArguments</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=68</Reference>
<Reference ReferenceType='HasProperty' IsForward='false'>ns=1;i=1000</Reference>
</References>
</UAVariable>
<UAVariable NodeId='ns=1;i=1001' BrowseName='InputArguments'
ParentNodeId='ns=1;i=1000' DataType='Argument'
ValueRank='1' ArrayDimensions='1'
UserWriteMask='1' AccessRestrictions='1'
DesignToolOnly='true'>
<DisplayName>Imported inputs</DisplayName>
<Description>Input metadata</Description>
<Category>Method metadata</Category>
<Documentation>https://example.org/input-arguments</Documentation>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=68</Reference>
<Reference ReferenceType='HasProperty' IsForward='false'>ns=1;i=1000</Reference>
</References>
<RolePermissions>
<RolePermission Permissions='1'>i=15644</RolePermission>
</RolePermissions>
<Value>
<uax:ListOfExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=297</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Name>revision</uax:Name>
<uax:DataType>
<uax:Identifier>i=12</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:ArrayDimensions />
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
</UAVariable>
<UAVariable NodeId='ns=1;i=1002' BrowseName='OutputArguments'
ParentNodeId='ns=1;i=1000' DataType='Argument'
ValueRank='1' ArrayDimensions='1'>
<DisplayName>Imported outputs</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=68</Reference>
<Reference ReferenceType='HasProperty' IsForward='false'>ns=1;i=1000</Reference>
</References>
<Value>
<uax:ListOfExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=297</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Name>accepted</uax:Name>
<uax:DataType>
<uax:Identifier>i=1</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:ArrayDimensions />
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
</UAVariable>
<UAMethod NodeId='ns=1;i=1000' BrowseName='1:Load'>
<DisplayName>Load</DisplayName>
<References>
<Reference ReferenceType='HasProperty'>ns=1;i=1003</Reference>
<Reference ReferenceType='HasProperty'>ns=1;i=1001</Reference>
<Reference ReferenceType='HasProperty'>ns=1;i=1002</Reference>
</References>
</UAMethod>
</UANodeSet>";
string xml = useNamespaceUriTargets
? importBuffer.Replace(">ns=1;", ">nsu=urn:test:method-arguments;", StringComparison.Ordinal)
: importBuffer;
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
Export.UANodeSet importedNodeSet = Export.UANodeSet.Read(importStream)!;
foreach (Export.UAVariable variable in importedNodeSet!.Items!.OfType<Export.UAVariable>())
{
if (variable.BrowseName != BrowseNames.InputArguments &&
variable.BrowseName != BrowseNames.OutputArguments)
{
continue;
}
if (!includeParentHints)
{
variable.ParentNodeId = null;
}
if (referenceDirection == "Forward")
{
variable.References = variable.References!
.Where(reference => reference.ReferenceType != "HasProperty")
.ToArray();
}
}
if (referenceDirection == "Inverse")
{
Export.UAMethod methodNode = importedNodeSet.Items!.OfType<Export.UAMethod>().Single();
methodNode.References = methodNode.References!
.Where(reference => reference.Value == "ns=1;i=1003")
.ToArray();
}
if (parentFirst)
{
importedNodeSet.Items = importedNodeSet.Items!
.OrderBy(node => node is Export.UAMethod ? 0 : 1)
.ToArray();
}
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
localContext.NamespaceUris.Append("urn:test:preexisting");
foreach (string namespaceUri in importedNodeSet.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
localContext.ServerUris = new StringTable();
localContext.EncodeableFactory = EncodeableFactory.Create();
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
MethodState method = importedNodeStates.OfType<MethodState>().Single();
ushort namespaceIndex = (ushort)localContext.NamespaceUris.GetIndex(
"urn:test:method-arguments");
var inputId = new NodeId(1001u, namespaceIndex);
var outputId = new NodeId(1002u, namespaceIndex);
var customInputId = new NodeId(1003u, namespaceIndex);
var children = new List<BaseInstanceState>();
method.GetChildren(localContext, children);
Assert.That(method.InputArguments, Is.Not.Null);
Assert.That(method.OutputArguments, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(method.InputArguments!.NodeId, Is.EqualTo(inputId));
Assert.That(method.OutputArguments!.NodeId, Is.EqualTo(outputId));
Assert.That(
method.InputArguments.ReferenceTypeId,
Is.EqualTo(ReferenceTypeIds.HasProperty));
Assert.That(method.InputArguments.DisplayName.Text, Is.EqualTo("Imported inputs"));
Assert.That(method.InputArguments.Description.Text, Is.EqualTo("Input metadata"));
Assert.That(
method.InputArguments.UserWriteMask,
Is.EqualTo(AttributeWriteMask.AccessLevel));
Assert.That(
method.InputArguments.AccessRestrictions,
Is.EqualTo(AccessRestrictionType.SigningRequired));
Assert.That(method.InputArguments.DesignToolOnly, Is.True);
Assert.That(
method.InputArguments.NodeSetDocumentation,
Is.EqualTo("https://example.org/input-arguments"));
Assert.That(
method.InputArguments.Categories,
Has.Count.EqualTo(1));
Assert.That(
method.InputArguments.Categories,
Has.Member("Method metadata"));
Assert.That(method.InputArguments.RolePermissions, Has.Count.EqualTo(1));
Assert.That(
method.InputArguments.RolePermissions[0].RoleId,
Is.EqualTo(ObjectIds.WellKnownRole_Anonymous));
Assert.That(
method.InputArguments.RolePermissions[0].Permissions,
Is.EqualTo((uint)PermissionType.Browse));
Assert.That(method.InputArguments.Value, Has.Count.EqualTo(1));
Assert.That(method.InputArguments.Value[0].Name, Is.EqualTo("revision"));
Assert.That(method.OutputArguments.Value, Has.Count.EqualTo(1));
Assert.That(method.OutputArguments.Value[0].Name, Is.EqualTo("accepted"));
Assert.That(
importedNodeStates.Single(node => node.NodeId == inputId),
Is.SameAs(method.InputArguments));
Assert.That(
importedNodeStates.Single(node => node.NodeId == outputId),
Is.SameAs(method.OutputArguments));
Assert.That(children, Has.Count.EqualTo(3));
Assert.That(children, Does.Contain(method.InputArguments));
Assert.That(children, Does.Contain(method.OutputArguments));
Assert.That(
children.Single(child => child.NodeId == customInputId),
Is.TypeOf<PropertyState>());
});
importedNodeSet.Items = [];
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
children.Clear();
method.GetChildren(localContext, children);
Assert.That(children, Has.Count.EqualTo(3));
Assert.That(
importedNodeStates.Single(node => node.NodeId == inputId),
Is.SameAs(method.InputArguments));
Assert.That(
importedNodeStates.Single(node => node.NodeId == outputId),
Is.SameAs(method.OutputArguments));
}
/// <summary>
/// Linking must preserve the importer's previous behavior for unnamed
/// children and sibling BrowseName collisions.
/// </summary>
[Test]
public void ImportPreservesUnnamedAndDuplicateBrowseNameChildren()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string importBuffer =
@"<?xml version='1.0' encoding='utf-8'?>
<UANodeSet xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>urn:test:duplicate-children</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAObject NodeId='ns=1;i=2000' BrowseName='1:Parent'>
<DisplayName>Parent</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
</References>
</UAObject>
<UAObject NodeId='ns=1;i=2001' BrowseName='1:Duplicate'
ParentNodeId='ns=1;i=2000'>
<DisplayName>First</DisplayName>
<References>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=2000</Reference>
</References>
</UAObject>
<UAObject NodeId='ns=1;i=2002' BrowseName='1:Duplicate'
ParentNodeId='ns=1;i=2000'>
<DisplayName>Second</DisplayName>
<References>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=2000</Reference>
</References>
</UAObject>
<UAObject NodeId='ns=1;i=2003' ParentNodeId='ns=1;i=2000'>
<DisplayName>Unnamed</DisplayName>
<References>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=2000</Reference>
</References>
</UAObject>
</UANodeSet>";
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
Export.UANodeSet importedNodeSet = Export.UANodeSet.Read(importStream)!;
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
importedNodeSet.Import(localContext, importedNodeStates, linkParentChild: true);
BaseObjectState parent = importedNodeStates
.OfType<BaseObjectState>()
.Single(node => node.BrowseName.Name == "Parent");
var children = new List<BaseInstanceState>();
parent.GetChildren(localContext, children);
Assert.Multiple(() =>
{
Assert.That(children, Has.Count.EqualTo(3));
Assert.That(
children.Count(child => child.BrowseName.Name == "Duplicate"),
Is.EqualTo(2));
Assert.That(
children.Count(child => child.BrowseName.IsNull),
Is.EqualTo(1));
Assert.That(
importedNodeStates.Count(node =>
node.NodeId.NamespaceIndex == parent.NodeId.NamespaceIndex),
Is.EqualTo(4));
});
}
[Test]
public void ImportBindsMethodArgumentsAcrossAccumulatedImports(
[Values(true, false)] bool propertyFirst,
[Values(true, false)] bool includeParentHint)
{
Export.UANodeSet nodeSet = CreateMethodReferenceNodeSet();
Export.UANode[] items = nodeSet.Items!;
Export.UAVariable property = items!.OfType<Export.UAVariable>().Single();
property.ParentNodeId = includeParentHint ? "ns=1;i=1000" : null;
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = new NamespaceTable(),
EncodeableFactory = EncodeableFactory.Create()
};
var nodes = new NodeStateCollection();
nodeSet.Items = items.Where(node => (node is Export.UAVariable) == propertyFirst).ToArray();
nodeSet.Import(context, nodes, linkParentChild: true);
nodeSet.Items = items.Where(node => (node is Export.UAVariable) != propertyFirst).ToArray();
nodeSet.Import(context, nodes, linkParentChild: true);
MethodState method = nodes.OfType<MethodState>().Single(node => node.BrowseName.Name == "Load");
Assert.That(method.InputArguments, Is.Not.Null);
NodeState input = nodes.Single(node => node.BrowseName.Name == BrowseNames.InputArguments);
Assert.That(input, Is.SameAs(method.InputArguments));
Assert.That(method.InputArguments.Parent, Is.SameAs(method));
Assert.That(Export.UANodeSet.TryGetUnresolvedParentNodeId(input, out _), Is.False);
nodeSet.Items = [];
nodeSet.Import(context, nodes, linkParentChild: true);
var children = new List<BaseInstanceState>();
method.GetChildren(context, children);
Assert.That(children, Has.Count.EqualTo(1));
Assert.That(children[0], Is.SameAs(input));
Assert.That(nodes, Has.Count.EqualTo(3));
}
[TestCase("CustomName")]
[TestCase("DataType")]
[TestCase("TypeDefinition")]
[TestCase("Rank")]
[TestCase("ReferenceType")]
[TestCase("Ambiguous")]
[TestCase("AmbiguousWithHint")]
[TestCase("ConflictingHint")]
[TestCase("UnavailableHint")]
public void ImportPreservesInvalidMethodArgumentDeclarations(string scenario)
{
Export.UANodeSet nodeSet = CreateMethodReferenceNodeSet();
Export.UAVariable property = nodeSet.Items!.OfType<Export.UAVariable>().Single();
switch (scenario)
{
case "CustomName":
property.BrowseName = "1:InputArguments";
break;
case "DataType":
property.DataType = "i=12";
break;
case "TypeDefinition":
property.References![0].Value = "i=63";
break;
case "Rank":
property.ValueRank = -1;
break;
case "ReferenceType":
property.References![1].ReferenceType = "i=47";
break;
case "Ambiguous":
case "AmbiguousWithHint":
property.References =
[
.. property.References!,
new Export.Reference
{
ReferenceType = "i=46",
IsForward = false,
Value = "ns=1;i=1002"
}
];
if (scenario == "AmbiguousWithHint")
{
property.ParentNodeId = "ns=1;i=1000";
}
break;
case "ConflictingHint":
property.ParentNodeId = "ns=1;i=1002";
break;
case "UnavailableHint":
property.ParentNodeId = "ns=1;i=1999";
break;
default:
throw new ArgumentOutOfRangeException(nameof(scenario));
}
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = new NamespaceTable()
};
var nodes = new NodeStateCollection();
nodeSet.Import(context, nodes, linkParentChild: true);
Assert.That(nodes.OfType<MethodState>().Select(method => method.InputArguments), Is.All.Null);
BaseVariableState imported = nodes.OfType<BaseVariableState>().Single();
Assert.That(
imported.TypeDefinitionId,
Is.EqualTo(NodeId.Parse(property.References![0].Value!)));
Assert.That(imported.DataType, Is.EqualTo(NodeId.Parse(property.DataType!)));
Assert.That(imported.ValueRank, Is.EqualTo(property.ValueRank));
var references = new List<IReference>();
imported.GetReferences(context, references);
Assert.That(
references.Select(reference =>
(reference.ReferenceTypeId, reference.IsInverse, reference.TargetId)),
Is.EquivalentTo(property.References
.Where(reference => reference.ReferenceType != "i=40")
.Select(reference =>
(NodeId.Parse(reference.ReferenceType!),
!reference.IsForward,
ExpandedNodeId.Parse(reference.Value!)))));
Assert.That(nodes, Has.Count.EqualTo(3));
if (scenario == "UnavailableHint")
{
Assert.That(
Export.UANodeSet.TryGetUnresolvedParentNodeId(
nodes.OfType<BaseVariableState>().Single(),
out NodeId unresolved),
Is.True);
Assert.That(unresolved, Is.EqualTo(new NodeId(1999u, 1)));
}
}
[Test]
public void ImportDoesNotInferMethodArgumentsWhenLinkingIsDisabled(
[Values(true, false)] bool useNamespaceUriTargets)
{
Export.UANodeSet nodeSet = CreateMethodReferenceNodeSet();
if (useNamespaceUriTargets)
{
nodeSet.Items!.OfType<Export.UAVariable>().Single().References![1].Value =
"nsu=urn:test:method-reference-parent;i=1000";
}
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = new NamespaceTable()
};
var nodes = new NodeStateCollection();
nodeSet.Import(context, nodes);
Assert.That(nodes.OfType<MethodState>().Select(method => method.InputArguments), Is.All.Null);
Assert.That(nodes.OfType<BaseVariableState>().Single().Parent, Is.Null);
}
/// <summary>
/// Test that parent-child references are NOT established by default (backward compatibility).
/// </summary>
[Test]
public void ParentChildReferencesDefaultBehaviorTest()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
const string importBuffer =
@"<?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'
LastModified='2024-01-01T00:00:00.000Z'
xmlns='http://opcfoundation.org/UA/2011/03/UANodeSet.xsd'>
<NamespaceUris>
<Uri>http://opcfoundation.org/UA/Test</Uri>
</NamespaceUris>
<Aliases>
<Alias Alias='HasComponent'>i=47</Alias>
<Alias Alias='HasTypeDefinition'>i=40</Alias>
</Aliases>
<UAObject NodeId='ns=1;i=1000' BrowseName='1:ParentObject'>
<DisplayName>ParentObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
</References>
</UAObject>
<UAObject ParentNodeId='ns=1;i=1000' NodeId='ns=1;i=1002' BrowseName='1:ChildObject'>
<DisplayName>ChildObject</DisplayName>
<References>
<Reference ReferenceType='HasTypeDefinition'>i=58</Reference>
<Reference ReferenceType='HasComponent' IsForward='false'>ns=1;i=1000</Reference>
</References>
</UAObject>
</UANodeSet>";
using var importStream = new MemoryStream(Encoding.UTF8.GetBytes(importBuffer));
var importedNodeSet = Export.UANodeSet.Read(importStream);
var importedNodeStates = new NodeStateCollection();
var localContext = new SystemContext(telemetry) { NamespaceUris = new NamespaceTable() };
foreach (string namespaceUri in importedNodeSet!.NamespaceUris!)
{
localContext.NamespaceUris.Append(namespaceUri);
}
// Import without linkParentChild parameter (default is false)
importedNodeSet.Import(localContext, importedNodeStates);
// Verify that all nodes were imported
Assert.That(importedNodeStates, Has.Count.EqualTo(2));
// Find the parent object
BaseObjectState? parentObject = null;
BaseObjectState? childObject = null;
foreach (NodeState node in importedNodeStates)
{
if (node.BrowseName.Name == "ParentObject")
{
parentObject = (node as BaseObjectState)!;
}
else if (node.BrowseName.Name == "ChildObject")
{
childObject = (node as BaseObjectState)!;
}
}
Assert.That(parentObject, Is.Not.Null, "ParentObject should be imported");
Assert.That(childObject, Is.Not.Null, "ChildObject should be imported");
// Verify parent-child relationships are NOT established (backward compatibility)
Assert.That(childObject.Parent, Is.Null, "ChildObject's Parent should be null by default");
// Verify GetChildren returns empty (backward compatibility)
var children = new List<BaseInstanceState>();
parentObject.GetChildren(localContext, children);
Assert.That(children, Is.Empty, "ParentObject should have 0 children by default (backward compatibility)");
}
private static Export.UANodeSet CreateMethodReferenceNodeSet()
{
const string xml = """
<UANodeSet xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd">
<NamespaceUris><Uri>urn:test:method-reference-parent</Uri></NamespaceUris>
<UAMethod NodeId="ns=1;i=1000" BrowseName="1:Load">
<DisplayName>Load</DisplayName>
</UAMethod>
<UAMethod NodeId="ns=1;i=1002" BrowseName="1:Other">
<DisplayName>Other</DisplayName>
</UAMethod>
<UAVariable NodeId="ns=1;i=1001" BrowseName="InputArguments" DataType="i=296" ValueRank="1">
<DisplayName>InputArguments</DisplayName>
<References>
<Reference ReferenceType="i=40">i=68</Reference>
<Reference ReferenceType="i=46" IsForward="false">ns=1;i=1000</Reference>
</References>
</UAVariable>
</UANodeSet>
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml));
return Export.UANodeSet.Read(stream)!;
}
private static List<NodeId> BrowseReferenceTypes(
ISystemContext context,
NodeState source,
NodeId targetId)
{
using INodeBrowser browser = source.CreateBrowser(
context,
null,
NodeId.Null,
false,
BrowseDirection.Both,
QualifiedName.Null,
null,
true);
var referenceTypes = new List<NodeId>();
IReference reference;
while ((reference = browser.Next()!) is not null)
{
if (reference.TargetId == targetId)
{
referenceTypes.Add(reference.ReferenceTypeId);
}
}
return referenceTypes;
}
}
}