Files
UA-.NETStandard/tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs
T
romanettandClaude Opus 5.5 7212607f16 OPC UA Part 3 compliance: permissions, Write semantics, NodeManagement, NodeSet DataTypes, locales (#4675)
# Description

Fixes the High and Medium findings of an OPC UA Part 3 (OPC 10000-3,
Address Space Model) compliance audit. Each finding was checked against
the full spec text (Parts 3, 4, 5, 6, 14 and 18) before it was fixed.
Every fix comes with regression tests that fail without it.

### Role permissions (Part 3 §8.55, Part 4 §5.12.2.2)
- The type-hierarchy exemption in
`MasterNodeManager.ValidateRolePermissions` /
`ValidateAccessRestrictions` now covers only Browse, Read and
ReadRolePermissions. Type nodes stay visible to generic clients, which
was the intent of #3705; every other permission bit is now enforced on
them.
- `ReceiveEvents` is enforced on the EventType node. The standard
NodeSet grants it on `AuditEventType` and its subtypes only to
SecurityAdmin, so only SecurityAdmin sessions now receive audit events.
- Call checks the `Call` permission and AccessRestrictions on the Object
as well as on the Method.
- When a Call's `methodId` is the Method declaration of the ObjectType,
the Object's Method with the same BrowseName is permission-checked and
invoked. The declaration is used only if the Object has no such Method.
This applies to `AsyncCustomNodeManager`, `CustomNodeManager2` and the
sample node manager.

### Write service (Part 3 §5.6.2, §8.40, §8.57; Part 4 §5.11.4)
- The synchronous `OnWriteValue` path (fluent `OnWrite`) checks DataType
and ValueRank first (`Bad_TypeMismatch`). After an IndexRange write it
merges the slice into the cached value instead of replacing it.
- StatusWrite / TimestampWrite are enforced (`Bad_WriteNotSupported`).
- Undefined enumeration values and invalid OptionSet writes return
`Bad_OutOfRange`. OptionSet writes merge only the valid bits into the
stored value.
- New `ReportPropertyValueChanged` / `ReportSemanticChange` let server
code raise SemanticChangeEvents for changes it makes itself.
- Required follow-up (Part 14 §6.2.11.1): if an external target rejects
a timestamp, the PubSub subscriber's TargetVariables writer retries
without it.

### NodeManagement and Call arguments (Part 3 §5.5.2, §5.7.3, §6.4.2;
Part 4 §5.8, §5.12.2)
- AddNodes creates the Mandatory InstanceDeclarations of the
TypeDefinition and rejects abstract or Interface types
(`Bad_TypeDefinitionInvalid`).
- AddReferences, DeleteReferences and DeleteNodes bump NodeVersion and
raise GeneralModelChangeEvents.
- Clients may omit trailing optional input arguments
(`HasOptionalInputArgumentDescription`).

### NodeSet DataTypes (Part 3 §5.8.3, §8.48; Part 6 Annex F.12)
- `UANodeSet.Import` completes structure definitions with the base
type's fields (base fields first). It creates definitions for structures
without fields and sets `DefaultEncodingId`.
`StructureDefinition.FirstExplicitFieldIndex` marks the subtype's own
fields.
- `DataTypeState` no longer mutates the stored definition on read.

### Locales (Part 3 §5.2.5, §8.5; Part 4 §5.4)
- Read, Browse and data change notifications return the translation that
best matches the session's LocaleIds, or all translations for `mul`. The
selection lives in the server dispatcher and is built on
`ResourceManager`: the new `TranslateValue` calls the overridable
`Translate`.
- Locale ids are matched case-insensitively, and fallback translations
carry their real locale.

### Client complex types (Part 6 §5.1.7)
- The Reflection.Emit builder encodes only `IsOptional` subtyped fields
as ExtensionObject. It also supports OptionSet subtypes: an unsupported
type is skipped instead of aborting the whole load.
- `TypeInfo.GetXmlName` uses the data type's XML name for dynamic
structures and OptionSets (the XML body element was `<Structure>`). Both
builders now produce identical XML.

## Behaviour changes
All are documented in `docs/MigrationGuide.md`.
- **Audit events:** only SecurityAdmin receives them. The CTT "Auditing
Connections" unit needs a SecurityAdmin user (e.g. sysadmin);
`AuditingOperationTests` was updated.
- **Write timestamps and status:** clients writing a SourceTimestamp or
a non-Good StatusCode get `Bad_WriteNotSupported` unless the Variable
has StatusWrite/TimestampWrite. No variable in `src/` sets these bits.
- **AddNodes:** it rejects abstract types (including `BaseVariableType`
and `BaseEventType`), and new instances now come with child nodes.
- **Call:** the Object needs the Call permission. The Object's own
Method runs even when the call names the type declaration.

## Not included
- Low-severity findings: missing validation on optional services and
author-side NodeSet rules.
- LocalizedText nested inside structures or Variant arrays, and in
HistoryRead or Call outputs.

## Testing
net10.0, Debug, one suite at a time on the merged branch. All suites
below pass:

| Suite | Passed |
|---|---|
| Types | 11430 |
| Core.Encoders | 2947 |
| Core.Schema | 179 |
| Core.Security | 558 |
| Client | 2761 |
| Server | 8210 |
| InformationModel | 1080 |
| Features | 145 |
| History | 536 |
| Di | 455 |
| Machinery | 167 |
| Robotics | 661 |
| Robotics.Intent | 22 |
| Pumps | 75 |
| Scales | 122 |
| Positioning | 69 |
| ISA95 | 144 |
| Vision | 484 |
| PubSub | 1490 |
| PubSub.Adapter | 151 |
| PubSub.Server | 177 |
| Redundancy.Server | 856 |
| Lds | 173 |
| Configuration | 241 |
| WotCon | 1496 |

- **Client.ComplexTypes:** one test wrote a SourceTimestamp to a
variable without TimestampWrite. It is fixed in this PR, and the suite
has not been re-run since.
- **Core:** 6068 passed, 1 failure in
`ActivityTraceFlags_Metrics_ShouldRecordMetricsOnly`. That telemetry
test passed when re-run alone, so it looks flaky.
- **Still running:** Sessions, Subscriptions (Classic, Durable,
standard) and Interop. Results will follow on this PR.
- `UA.slnx` builds with 0 errors for net10.0 and net48, with no new
analyzer warnings in changed files.

## Related Issues

- Part 3 compliance audit (no tracking issue)

## 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.
- [ ] 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.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:43:59 +00:00

504 lines
21 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.Threading;
using System.Threading.Tasks;
using System.Xml;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using Opc.Ua.Server.TestFramework;
using Opc.Ua.Tests;
using Quickstarts;
using Quickstarts.ReferenceServer;
namespace Opc.Ua.Client.ComplexTypes.Tests
{
/// <summary>
/// Load Type System tests.
/// </summary>
[TestFixture]
[Category("Client")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[NonParallelizable]
public class TypeSystemClientTest : IUAClient
{
public ISession Session { get; private set; }
private ServerFixture<ReferenceServer> m_serverFixture;
#pragma warning disable NUnit1032 // An IDisposable field/property should be Disposed in a TearDown method
private ClientFixture m_clientFixture;
#pragma warning restore NUnit1032 // An IDisposable field/property should be Disposed in a TearDown method
private ReferenceServer m_server;
private readonly string m_uriScheme;
private ITelemetryContext m_telemetry;
private string m_pkiRoot;
private Uri m_url;
/// <summary>
/// for test that fetched and browsed node count match
/// </summary>
private int m_fetchedNodesCount;
private int m_browsedNodesCount;
public TypeSystemClientTest()
{
m_uriScheme = Utils.UriSchemeOpcTcp;
}
public TypeSystemClientTest(string uriScheme)
{
m_uriScheme = uriScheme;
}
/// <summary>
/// Set up a Server and a Client instance.
/// </summary>
[OneTimeSetUp]
public Task OneTimeSetUpAsync()
{
m_fetchedNodesCount = -1;
m_browsedNodesCount = -1;
return OneTimeSetUpAsync(NUnitTelemetryContext.Create());
}
/// <summary>
/// Setup a server and client fixture.
/// </summary>
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
private async Task OneTimeSetUpAsync(ITelemetryContext telemetry)
{
// pki directory root for test runs.
m_telemetry = telemetry;
m_pkiRoot = Path.GetTempPath() + Path.GetRandomFileName();
// start Ref server
m_serverFixture = new ServerFixture<ReferenceServer>(t => new ReferenceServer(t))
{
UriScheme = m_uriScheme,
SecurityNone = true,
AutoAccept = true,
AllNodeManagers = true,
OperationLimits = true
};
m_server = await m_serverFixture.StartAsync(m_pkiRoot)
.ConfigureAwait(false);
m_clientFixture = new ClientFixture(telemetry);
await m_clientFixture.LoadClientConfigurationAsync(m_pkiRoot).ConfigureAwait(false);
m_clientFixture.Config.TransportQuotas.MaxMessageSize = 4 * 1024 * 1024;
m_url = new Uri(
m_uriScheme +
"://localhost:" +
m_serverFixture.Port.ToString(CultureInfo.InvariantCulture));
try
{
Session = await m_clientFixture
.ConnectAsync(m_url, SecurityPolicies.Basic256Sha256)
.ConfigureAwait(false);
}
catch (Exception e)
{
Assert.Ignore(
$"OneTimeSetup failed to create session, tests skipped. Error: {e.Message}");
}
}
/// <summary>
/// Tear down the Server and the Client.
/// </summary>
[OneTimeTearDown]
public async Task OneTimeTearDownAsync()
{
if (Session != null)
{
await Session.CloseAsync().ConfigureAwait(false);
Session.Dispose();
Session = null;
}
await m_serverFixture.StopAsync().ConfigureAwait(false);
m_clientFixture?.Dispose();
m_server?.Dispose();
}
[Test]
[Order(100)]
[TestCase(false, false, false)]
[TestCase(true, false, false)]
[TestCase(false, true, false)]
[TestCase(false, false, true)]
public async Task LoadTypeSystemAsync(
bool onlyEnumTypes,
bool disableDataTypeDefinition,
bool disableDataTypeDictionary)
{
var typeSystem = ComplexTypeSystem.Create(Session, new ComplexTypeBuilderFactory(), m_telemetry);
Assert.That(typeSystem, Is.Not.Null);
typeSystem.DisableDataTypeDefinition = disableDataTypeDefinition;
typeSystem.DisableDataTypeDictionary = disableDataTypeDictionary;
bool success = await typeSystem.LoadAsync(onlyEnumTypes, true).ConfigureAwait(false);
Assert.That(success, Is.True);
IReadOnlyList<XmlQualifiedName> types = typeSystem.GetDefinedTypes();
TestContext.Out.WriteLine("Types loaded: {0} ", types.Count);
foreach (XmlQualifiedName type in types)
{
TestContext.Out.WriteLine("Type: {0} ", type);
}
foreach (ExpandedNodeId dataTypeId in typeSystem.GetDefinedDataTypeIds())
{
NodeIdDictionary<DataTypeDefinition> definitions = typeSystem
.GetDataTypeDefinitionsForDataType(
dataTypeId);
Assert.That(definitions, Is.Not.Empty);
Type type = Session.Factory.GetSystemType(dataTypeId);
Assert.That(type, Is.Not.Null);
var localTypeId = ExpandedNodeId.ToNodeId(dataTypeId, Session.NamespaceUris);
if (type.IsEnum)
{
Assert.That(definitions, Has.Count.EqualTo(1));
Assert.That(definitions.First().Value, Is.InstanceOf<EnumDefinition>());
Assert.That(definitions.First().Key, Is.EqualTo(localTypeId));
}
else
{
Assert.That(definitions[localTypeId], Is.InstanceOf<StructureDefinition>());
}
}
}
[Test]
[Order(200)]
public async Task BrowseComplexTypesServerAsync()
{
var samples = new ClientSamples(m_telemetry, null, null, true);
var complexTypeSystem = ComplexTypeSystem.Create(Session, new ComplexTypeBuilderFactory(), m_telemetry);
await samples.LoadTypeSystemAsync(complexTypeSystem, default).ConfigureAwait(false);
ArrayOf<ReferenceDescription> referenceDescriptions = await samples
.BrowseFullAddressSpaceAsync(this, ObjectIds.RootFolder)
.ConfigureAwait(false);
TestContext.Out.WriteLine("References: {0}", referenceDescriptions.Count);
m_browsedNodesCount = referenceDescriptions.Count;
ArrayOf<NodeId> variableIds =
referenceDescriptions
.Filter(r => r.NodeClass == NodeClass.Variable)
.ConvertAll(r => ExpandedNodeId.ToNodeId(r.NodeId, Session.NamespaceUris));
TestContext.Out.WriteLine("VariableIds: {0}", variableIds.Count);
(ArrayOf<DataValue> values, ArrayOf<ServiceResult> serviceResults) =
await samples.ReadAllValuesAsync(this, variableIds).ConfigureAwait(false);
int ii = 0;
foreach (ServiceResult serviceResult in serviceResults)
{
ServiceResult result = serviceResults[ii++];
Assert.That(
ServiceResult.IsGood(serviceResult) ||
serviceResult.StatusCode == StatusCodes.BadNotReadable ||
serviceResult.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected good result, but received {serviceResult}");
}
}
[Test]
[Order(300)]
public async Task FetchComplexTypesServerAsync()
{
var samples = new ClientSamples(m_telemetry, null, null, true);
var complexTypeSystem = ComplexTypeSystem.Create(Session, new ComplexTypeBuilderFactory(), m_telemetry);
await samples.LoadTypeSystemAsync(complexTypeSystem, default).ConfigureAwait(false);
IList<INode> allNodes = await samples
.FetchAllNodesNodeCacheAsync(this, ObjectIds.RootFolder, true, false, false)
.ConfigureAwait(false);
TestContext.Out.WriteLine("References: {0}", allNodes.Count);
m_fetchedNodesCount = allNodes.Count;
var variableIds =
allNodes
.Where(r =>
r.NodeClass == NodeClass.Variable &&
r is VariableNode v &&
v.DataType.NamespaceIndex != 0)
.Select(r => ExpandedNodeId.ToNodeId(r.NodeId, Session.NamespaceUris))
.ToList();
TestContext.Out.WriteLine("VariableIds: {0}", variableIds.Count);
(ArrayOf<DataValue> values, ArrayOf<ServiceResult> serviceResults) =
await samples.ReadAllValuesAsync(this, variableIds).ConfigureAwait(false);
foreach (ServiceResult serviceResult in serviceResults)
{
Assert.That(ServiceResult.IsGood(serviceResult), Is.True, serviceResult.ToString());
}
// check if complex type is properly decoded
bool testFailed = false;
for (int ii = 0; ii < values.Count; ii++)
{
DataValue value = values[ii];
NodeId variableId = variableIds[ii];
var variableExpandedNodeId = NodeId.ToExpandedNodeId(
variableId,
Session.NamespaceUris);
if (allNodes.FirstOrDefault(
n => n.NodeId == variableId) is VariableNode variableNode &&
variableNode.DataType.NamespaceIndex != 0)
{
TestContext.Out.WriteLine("Check for custom type: {0}", variableNode);
var fullTypeId = NodeId.ToExpandedNodeId(
variableNode.DataType,
Session.NamespaceUris);
Type type = Session.Factory.GetSystemType(fullTypeId);
if (type == null)
{
// check for opaque type
NodeId superType =
await Session.NodeCache.FindSuperTypeAsync(fullTypeId).ConfigureAwait(false);
NodeId lastGoodType = variableNode.DataType;
while (!superType.IsNull && superType != DataTypes.BaseDataType)
{
if (superType == DataTypeIds.Structure)
{
testFailed = true;
break;
}
lastGoodType = superType;
superType =
await Session.NodeCache.FindSuperTypeAsync(superType).ConfigureAwait(false);
}
if (testFailed)
{
TestContext.Out.WriteLine(
"-- Variable: {0} complex type unavailable --> {1}",
variableNode.NodeId,
variableNode.DataType);
(_, _) = await samples.ReadAllValuesAsync(this, [variableId])
.ConfigureAwait(false);
}
else
{
TestContext.Out.WriteLine(
"-- Variable: {0} opaque typeid --> {1}",
variableNode.NodeId,
lastGoodType);
}
continue;
}
if (value.WrappedValue.TryGetValue(out ExtensionObject extensionObject) &&
extensionObject.TryGetValue(out IEncodeable encodeable))
{
Type valueType = encodeable.GetType();
if (valueType != type)
{
testFailed = true;
TestContext.Out.WriteLine(
"Variable: {0} type is decoded as ExtensionObject --> {1}",
variableNode,
value.WrappedValue);
(_, _) = await samples.ReadAllValuesAsync(this, [variableId])
.ConfigureAwait(false);
}
continue;
}
if (value.WrappedValue.TryGetValue(out ArrayOf<ExtensionObject> array))
{
foreach (ExtensionObject valueItem in array.ToList())
{
if (valueItem.TryGetValue(out encodeable))
{
Type valueType = encodeable.GetType();
if (valueType != type)
{
testFailed = true;
TestContext.Out.WriteLine(
"Variable: {0} type is decoded as ExtensionObject --> {1}",
variableNode,
valueItem);
(_, _) = await samples.ReadAllValuesAsync(this, [variableId])
.ConfigureAwait(false);
}
}
}
}
}
}
if (testFailed)
{
Assert.Fail(
"Test failed, unknown or undecodable complex type detected. See log for information.");
}
}
[Test]
[Order(330)]
public void ValidateFetchedAndBrowsedNodesMatch()
{
if (m_browsedNodesCount < 0 || m_fetchedNodesCount < 0)
{
Assert.Ignore("The browse or fetch test did not run.");
}
// The Browse and Fetch traversals run sequentially against a live
// server. Diagnostic and session-state nodes (e.g.
// Server.ServerDiagnostics.SessionDiagnosticsArray entries) can
// come and go between the two calls, so a small drift is
// expected. Anything more than a handful of nodes is a real
// structural mismatch.
Assert.That(
Math.Abs(m_browsedNodesCount - m_fetchedNodesCount),
Is.LessThanOrEqualTo(8),
"Browsed=" + m_browsedNodesCount + ", Fetched=" + m_fetchedNodesCount);
}
[Test]
[Order(400)]
public async Task ReadWriteScalarVariableTypeAsync()
{
var samples = new ClientSamples(m_telemetry, null, null, true);
var complexTypeSystem = ComplexTypeSystem.Create(Session, new ComplexTypeBuilderFactory(), m_telemetry);
await samples.LoadTypeSystemAsync(complexTypeSystem, default).ConfigureAwait(false);
// test the static version of the structure
ExpandedNodeId structureVariable = TestData.VariableIds
.Data_Static_Structure_ScalarStructure;
Assert.That(structureVariable.IsNull, Is.False);
var nodeId = ExpandedNodeId.ToNodeId(structureVariable, Session.NamespaceUris);
Assert.That(nodeId.IsNull, Is.False);
Node node = await Session.ReadNodeAsync(nodeId).ConfigureAwait(false);
Assert.That(node, Is.Not.Null);
Assert.That(node, Is.InstanceOf<VariableNode>());
DataValue dataValue = await Session.ReadValueAsync(nodeId).ConfigureAwait(false);
Assert.That(dataValue.IsNull, Is.False);
// test the accessor to the complex types
Assert.That(dataValue.WrappedValue.TryGetValue(out ExtensionObject extensionObject), Is.True);
Assert.That(extensionObject.TryGetValue(out IEncodeable encodeable), Is.True);
Assert.That(encodeable, Is.Not.Null);
var complexType = encodeable as IComplexTypeProperties;
Assert.That(complexType, Is.Not.Null);
// list properties
TestContext.Out.WriteLine("{0} Properties", complexType.GetPropertyCount());
foreach (ComplexTypePropertyInfo property in complexType.GetPropertyEnumerator())
{
TestContext.Out.WriteLine(
"{0}:{1:20}: Type: {2}: ValueRank: {3} Value: {4}",
property.Order,
property.Name,
property.PropertyType.Name,
property.ValueRank,
complexType[property.Name].ToString());
}
complexType["ByteValue"] = (byte)0;
complexType["StringValue"] = "badbeef";
complexType["NumberValue"] = new Variant((uint)3210);
complexType["IntegerValue"] = new Variant((long)54321);
complexType["UIntegerValue"] = new Variant((ulong)12345);
// without the TimestampWrite access level bit only a null
// SourceTimestamp may be written (OPC 10000-3 8.57).
var dataWriteValue = new DataValue(
dataValue.WrappedValue,
StatusCodes.Good);
// write value back
ArrayOf<WriteValue> writeValues =
[
new WriteValue
{
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = dataWriteValue
}
];
WriteResponse response = await Session
.WriteAsync(null, writeValues, CancellationToken.None)
.ConfigureAwait(false);
Assert.That(response, Is.Not.Null);
Assert.That(response.Results.IsNull, Is.False);
TestContext.Out.WriteLine(new ServiceResult(response.Results[0]).StatusCode);
TestContext.Out.WriteLine(response.Results[0].ToString());
Assert.That(StatusCode.IsGood(response.Results[0]), Is.True);
// read back written values
dataValue = await Session.ReadValueAsync(nodeId).ConfigureAwait(false);
Assert.That(dataValue.IsNull, Is.False);
Assert.That(dataValue.WrappedValue.TryGetValue(out extensionObject), Is.True);
Assert.That(extensionObject.TryGetValue(out encodeable), Is.True);
Assert.That(encodeable, Is.Not.Null);
complexType = encodeable as IComplexTypeProperties;
Assert.That(complexType, Is.Not.Null);
// list properties
TestContext.Out.WriteLine("{0} Properties", complexType.GetPropertyCount());
foreach (ComplexTypePropertyInfo property in complexType.GetPropertyEnumerator())
{
TestContext.Out.WriteLine(
"{0}:{1:20}: Type: {2}: ValueRank: {3} Value: {4}",
property.Order,
property.Name,
property.GetType().Name,
property.ValueRank,
complexType[property.Name].ToString());
}
Assert.That(complexType["ByteValue"], Is.EqualTo(new Variant((byte)0)));
Assert.That(complexType["StringValue"], Is.EqualTo(new Variant("badbeef")));
Assert.That(complexType["NumberValue"], Is.EqualTo(new Variant((uint)3210)));
Assert.That(complexType["IntegerValue"], Is.EqualTo(new Variant((long)54321)));
Assert.That(complexType["UIntegerValue"], Is.EqualTo(new Variant((ulong)12345)));
}
}
}