Files
UA-.NETStandard/Docs/ComplexTypes.md
Copilot 35e03d9067 Add comprehensive documentation for ComplexTypes library (#3373)
* Initial plan

* Add comprehensive ComplexTypes documentation

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

* Fix code review issues in ComplexTypes documentation

Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2025-12-03 21:07:23 +01:00

19 KiB

OPC UA Complex Types

Overview

The Opc.Ua.Client.ComplexTypes library provides support for handling custom data types (complex types) in OPC UA client applications. Complex types include:

  • Custom Structures: User-defined structured data types with multiple fields
  • Custom Enumerations: User-defined enumeration types with custom values

This library allows OPC UA clients to automatically discover, load, and work with server-specific custom types, enabling seamless reading and writing of structured data without manual type definitions.

Key Concepts

What are Complex Types?

In OPC UA, complex types are custom data types defined by the server that extend beyond the built-in OPC UA data types. These types are commonly used to represent structured data such as:

  • Configuration structures with multiple parameters
  • Device status information with multiple fields
  • Custom enumerations specific to a domain or device

Type Discovery and Loading

The ComplexTypeSystem class manages the discovery and loading of custom types from an OPC UA server. It:

  1. Browses the server's type system to discover custom types
  2. Loads type definitions (using DataTypeDefinition attribute or binary/XML dictionaries)
  3. Dynamically generates .NET types at runtime
  4. Registers these types in the session's type factory for encoding/decoding

Supported Type Systems

The library supports multiple type definition mechanisms:

  • OPC UA 1.04+ DataTypeDefinition Attribute: Modern structured type definitions
  • OPC UA 1.03 Binary Schema Dictionaries: Legacy binary type dictionaries
  • OPC UA 1.03 XML Schema Dictionaries: Legacy XML type dictionaries

The library automatically uses the most appropriate mechanism available on the server.

Getting Started

Installation

Add the NuGet package to your project:

dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes

Or via Package Manager:

Install-Package OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes

Basic Usage

1. Loading All Custom Types from a Server

The most common approach is to load all custom types after establishing a session:

using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.ComplexTypes;

// Create and connect session
var session = await Session.Create(...);

// Create and load the complex type system
var complexTypeSystem = new ComplexTypeSystem(session);
await complexTypeSystem.LoadAsync();

Console.WriteLine($"Loaded {complexTypeSystem.GetDefinedTypes().Length} custom types");

After loading, the session can automatically encode and decode custom types when reading or writing values.

2. Reading Values with Complex Types

Once the type system is loaded, reading complex type values is straightforward:

// Read a value that contains a complex type
NodeId nodeId = new NodeId("ns=2;s=MyCustomStructVariable");
DataValue dataValue = await session.ReadValueAsync(nodeId);

// The value is automatically decoded to a .NET type
if (dataValue.Value is ExtensionObject extensionObject)
{
    // Access the structured data
    if (extensionObject.Body is IComplexTypeProperties complexType)
    {
        // Access properties by name
        var temperatureValue = complexType["Temperature"];
        var pressureValue = complexType["Pressure"];
        
        Console.WriteLine($"Temperature: {temperatureValue}");
        Console.WriteLine($"Pressure: {pressureValue}");
        
        // Enumerate all properties
        foreach (var property in complexType.GetPropertyEnumerator())
        {
            Console.WriteLine($"{property.Name}: {property.GetValue(complexType)}");
        }
    }
}

3. Writing Values with Complex Types

To write complex type values, modify the properties and write back:

// Read current value
DataValue dataValue = await session.ReadValueAsync(nodeId);
var extensionObject = (ExtensionObject)dataValue.Value;
var complexType = (IComplexTypeProperties)extensionObject.Body;

// Modify properties
complexType["Temperature"] = 25.5;
complexType["Pressure"] = 101.3;

// Write the modified value back
var writeValue = new WriteValue
{
    NodeId = nodeId,
    AttributeId = Attributes.Value,
    Value = new DataValue
    {
        Value = extensionObject,
        SourceTimestamp = DateTime.UtcNow
    }
};

var writeValues = new WriteValueCollection { writeValue };
var response = await session.WriteAsync(null, writeValues, CancellationToken.None);

if (StatusCode.IsGood(response.Results[0]))
{
    Console.WriteLine("Value written successfully");
}

Advanced Usage

Loading Specific Types

Instead of loading all types, you can load specific types or namespaces:

Load a Specific Type

var complexTypeSystem = new ComplexTypeSystem(session);

// Load a specific type by NodeId
ExpandedNodeId typeNodeId = new ExpandedNodeId("ns=2;i=3001");
Type systemType = await complexTypeSystem.LoadTypeAsync(typeNodeId);

if (systemType != null)
{
    Console.WriteLine($"Loaded type: {systemType.Name}");
}

Load a Specific Type with Subtypes

// Load a type and all its subtypes
ExpandedNodeId typeNodeId = new ExpandedNodeId("ns=2;i=3001");
Type systemType = await complexTypeSystem.LoadTypeAsync(typeNodeId, subTypes: true);

Load All Types from a Namespace

var complexTypeSystem = new ComplexTypeSystem(session);

// Load all custom types from a specific namespace
string namespaceUri = "http://mycompany.com/MyCustomTypes";
bool success = await complexTypeSystem.LoadNamespaceAsync(namespaceUri);

if (success)
{
    Console.WriteLine($"Successfully loaded types from namespace: {namespaceUri}");
}

Working with Complex Type Properties

The IComplexTypeProperties interface provides flexible access to complex type fields:

Access by Name

if (extensionObject.Body is IComplexTypeProperties complexType)
{
    // Get property value by name
    object value = complexType["PropertyName"];
    
    // Set property value by name
    complexType["PropertyName"] = newValue;
}

Access by Index

if (extensionObject.Body is IComplexTypeProperties complexType)
{
    // Get property value by index
    object value = complexType[0];
    
    // Set property value by index
    complexType[0] = newValue;
}

Enumerate Properties

if (extensionObject.Body is IComplexTypeProperties complexType)
{
    // Get all property names
    IList<string> names = complexType.GetPropertyNames();
    
    // Get all property types
    IList<Type> types = complexType.GetPropertyTypes();
    
    // Enumerate with detailed information
    foreach (ComplexTypePropertyInfo property in complexType.GetPropertyEnumerator())
    {
        Console.WriteLine($"Property: {property.Name}");
        Console.WriteLine($"  Type: {property.PropertyType}");
        Console.WriteLine($"  Order: {property.Order}");
        Console.WriteLine($"  IsOptional: {property.IsOptional}");
        Console.WriteLine($"  ValueRank: {property.ValueRank}");
        Console.WriteLine($"  Value: {property.GetValue(complexType)}");
    }
}

Handling Enumeration Types

Custom enumerations are also supported:

// After loading the type system, enum values are automatically decoded
DataValue dataValue = await session.ReadValueAsync(enumNodeId);

if (dataValue.Value is int enumValue)
{
    // The value is the numeric representation
    Console.WriteLine($"Enum value: {enumValue}");
    
    // You can also get the enum type from the factory
    var enumType = session.Factory.GetSystemType(enumTypeId);
    if (enumType?.IsEnum == true)
    {
        var enumName = Enum.GetName(enumType, enumValue);
        Console.WriteLine($"Enum name: {enumName}");
    }
}

Type Information and Introspection

Get All Loaded Types

var complexTypeSystem = new ComplexTypeSystem(session);
await complexTypeSystem.LoadAsync();

// Get all types that were dynamically created
Type[] definedTypes = complexTypeSystem.GetDefinedTypes();

foreach (Type type in definedTypes)
{
    Console.WriteLine($"Type: {type.Namespace}.{type.Name}");
}

Get Type Definitions

// Get the DataTypeDefinition for a specific type
ExpandedNodeId dataTypeId = new ExpandedNodeId("ns=2;i=3001");
var definitions = complexTypeSystem.GetDataTypeDefinitionsForDataType(dataTypeId);

foreach (var kvp in definitions)
{
    Console.WriteLine($"NodeId: {kvp.Key}");
    Console.WriteLine($"Definition: {kvp.Value}");
}

Get Loaded Data Type IDs

// Get all NodeIds for loaded data types
IEnumerable<ExpandedNodeId> dataTypeIds = complexTypeSystem.GetDefinedDataTypeIds();

foreach (var dataTypeId in dataTypeIds)
{
    Console.WriteLine($"Loaded type: {dataTypeId}");
}

Using Custom Type Factories

You can provide a custom type factory for advanced scenarios:

// Create a custom factory (implement IComplexTypeFactory)
var customFactory = new MyCustomComplexTypeFactory();

// Use it with the ComplexTypeSystem
var complexTypeSystem = new ComplexTypeSystem(session, customFactory);
await complexTypeSystem.LoadAsync();

Working with Telemetry and Logging

The ComplexTypeSystem supports the OPC UA telemetry context for observability:

// Use the session's telemetry context (default)
var complexTypeSystem = new ComplexTypeSystem(session);

// Or provide a custom telemetry context
ITelemetryContext telemetry = myCustomTelemetryContext;
var complexTypeSystem = new ComplexTypeSystem(session, telemetry);

// The system will log type loading information
await complexTypeSystem.LoadAsync();

Common Patterns

Pattern 1: One-Time Type Loading at Session Start

public async Task<ISession> CreateSessionWithTypes(string endpointUrl)
{
    var session = await Session.Create(
        configuration,
        new ConfiguredEndpoint(null, new EndpointDescription(endpointUrl)),
        false,
        "MyClient",
        60000,
        null,
        null
    );
    
    // Load all custom types immediately after connection
    var complexTypeSystem = new ComplexTypeSystem(session);
    await complexTypeSystem.LoadAsync();
    
    return session;
}

Pattern 2: Lazy Loading on Demand

public class OpcUaClient
{
    private ISession _session;
    private ComplexTypeSystem _complexTypeSystem;
    private bool _typesLoaded;
    
    public async Task EnsureTypesLoadedAsync()
    {
        if (!_typesLoaded)
        {
            _complexTypeSystem = new ComplexTypeSystem(_session);
            await _complexTypeSystem.LoadAsync();
            _typesLoaded = true;
        }
    }
    
    public async Task<DataValue> ReadComplexValueAsync(NodeId nodeId)
    {
        await EnsureTypesLoadedAsync();
        return await _session.ReadValueAsync(nodeId);
    }
}

Pattern 3: Reading Multiple Complex Values

public async Task ReadMultipleComplexValuesAsync(IList<NodeId> nodeIds)
{
    // Load types once
    var complexTypeSystem = new ComplexTypeSystem(session);
    await complexTypeSystem.LoadAsync();
    
    // Read all values
    var nodesToRead = nodeIds.Select(id => new ReadValueId
    {
        NodeId = id,
        AttributeId = Attributes.Value
    }).ToList();
    
    var response = await session.ReadAsync(
        null,
        0,
        TimestampsToReturn.Both,
        new ReadValueIdCollection(nodesToRead),
        CancellationToken.None
    );
    
    // Process results
    for (int i = 0; i < response.Results.Count; i++)
    {
        var dataValue = response.Results[i];
        if (dataValue.Value is ExtensionObject extensionObject &&
            extensionObject.Body is IComplexTypeProperties complexType)
        {
            Console.WriteLine($"NodeId: {nodeIds[i]}");
            foreach (var property in complexType.GetPropertyEnumerator())
            {
                Console.WriteLine($"  {property.Name}: {property.GetValue(complexType)}");
            }
        }
    }
}

Error Handling

Handling Type Loading Failures

try
{
    var complexTypeSystem = new ComplexTypeSystem(session);
    bool success = await complexTypeSystem.LoadAsync(throwOnError: true);
    
    if (success)
    {
        Console.WriteLine("All types loaded successfully");
    }
    else
    {
        Console.WriteLine("Some types could not be loaded");
    }
}
catch (ServiceResultException ex)
{
    Console.WriteLine($"Failed to load types: {ex.Message}");
    // Handle error appropriately
}

Handling Missing Type Definitions

// Try to load a specific type
var complexTypeSystem = new ComplexTypeSystem(session);
Type systemType = await complexTypeSystem.LoadTypeAsync(typeNodeId, throwOnError: false);

if (systemType == null)
{
    Console.WriteLine($"Type {typeNodeId} could not be loaded");
    // Fall back to reading as ExtensionObject with opaque body
}

Performance Considerations

Type System Caching

  • The ComplexTypeSystem loads types once and caches them in the session's factory
  • Subsequent reads/writes use the cached types automatically
  • Types remain available for the lifetime of the session

Minimizing Load Time

// Load only enumerations (faster)
var complexTypeSystem = new ComplexTypeSystem(session);
await complexTypeSystem.LoadAsync(onlyEnumTypes: true);

// Or load only specific namespaces
await complexTypeSystem.LoadNamespaceAsync("http://mycompany.com/MyTypes");

Batch Operations

When reading multiple values with complex types, read them in batches to minimize round-trips:

// Read multiple values in one call
var response = await session.ReadAsync(
    null,
    0,
    TimestampsToReturn.Both,
    new ReadValueIdCollection(nodesToRead),
    CancellationToken.None
);

Troubleshooting

Types Not Loading

Problem: LoadAsync() completes but types are not available

Solutions:

  • Verify the server supports DataTypeDefinition or provides type dictionaries
  • Check server logs for encoding/dictionary availability
  • Use verbose logging to see what types are discovered
// Enable detailed logging through telemetry context
var telemetry = session.MessageContext.Telemetry;
var complexTypeSystem = new ComplexTypeSystem(session, telemetry);

// The ComplexTypeSystem will log detailed information during type loading
await complexTypeSystem.LoadAsync();

Values Still Encoded as ExtensionObject

Problem: Values are read as ExtensionObject with opaque body instead of structured types

Solutions:

  • Ensure LoadAsync() was called before reading the value
  • Verify the type is actually loaded: complexTypeSystem.GetDefinedTypes()
  • Check if the server's type definition is complete and valid

Performance Issues

Problem: Type loading takes too long

Solutions:

  • Load only required namespaces instead of all types
  • Load types once at session start rather than on-demand
  • Consider caching type information across sessions if reconnecting frequently

API Reference

ComplexTypeSystem Class

The main class for managing complex types.

Constructors

// Create with session
ComplexTypeSystem(ISession session)
ComplexTypeSystem(ISession session, ITelemetryContext telemetry)
ComplexTypeSystem(ISession session, IComplexTypeFactory factory)
ComplexTypeSystem(ISession session, IComplexTypeFactory factory, ITelemetryContext telemetry)

// Create with resolver
ComplexTypeSystem(IComplexTypeResolver resolver, ITelemetryContext telemetry)
ComplexTypeSystem(IComplexTypeResolver resolver, IComplexTypeFactory factory, ITelemetryContext telemetry)

Methods

// Load all custom types from the server
ValueTask<bool> LoadAsync(bool onlyEnumTypes = false, bool throwOnError = false, CancellationToken ct = default)

// Load a specific type with optional subtypes
Task<Type> LoadTypeAsync(ExpandedNodeId nodeId, bool subTypes = false, bool throwOnError = false, CancellationToken ct = default)

// Load all types from a namespace
Task<bool> LoadNamespaceAsync(string ns, bool throwOnError = false, CancellationToken ct = default)

// Get all dynamically created types
Type[] GetDefinedTypes()

// Get all loaded data type NodeIds
IEnumerable<ExpandedNodeId> GetDefinedDataTypeIds()

// Get data type definitions for a type
NodeIdDictionary<DataTypeDefinition> GetDataTypeDefinitionsForDataType(ExpandedNodeId dataTypeId)

// Clear the data type cache
void ClearDataTypeCache()

Properties

// Get the loaded data type dictionaries
NodeIdDictionary<DataDictionary> DataTypeSystem { get; }

IComplexTypeProperties Interface

Interface for accessing properties of complex types.

Methods and Properties

// Access property by zero-based index
object this[int index] { get; set; }

// Access property by name
object this[string name] { get; set; }

// Get property count
int GetPropertyCount()

// Get property names
IList<string> GetPropertyNames()

// Get property types
IList<Type> GetPropertyTypes()

// Enumerate properties with metadata
IEnumerable<ComplexTypePropertyInfo> GetPropertyEnumerator()

ComplexTypePropertyInfo Class

Provides metadata about a property in a complex type.

Properties

PropertyInfo PropertyInfo { get; }        // Reflection property info
string Name { get; }                      // Property name
Type PropertyType { get; }                // Property type
int Order { get; }                        // Field order in structure
bool IsOptional { get; }                  // Whether field is optional
int ValueRank { get; }                    // Array rank (-1 = scalar, >=0 = array)
BuiltInType BuiltInType { get; }         // OPC UA built-in type

Methods

object GetValue(object o)                 // Get property value
void SetValue(object o, object v)         // Set property value

Known Limitations

  1. OptionSet Support: OPC UA 1.04 OptionSet types do not automatically create enumeration flags
  2. Legacy Dictionary Support: Some OPC UA 1.03 structured types that cannot be mapped to OPC UA 1.04 definitions are ignored
  3. Type Modifications: Once loaded, types cannot be dynamically updated during a session. Reconnect to reload modified types.

Additional Resources

See Also