# Structured data

> How-to guide: Access structured data types in a simple, name-based, dynamic, or typed way, and generate and define data types.

> This content was machine translated and may contain errors. [Read in German](https://www.traeger.de/docs/development/opcua/client/structured-data)

**Sample applications:**

- Client.DataTypes ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Client.DataTypes), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Client.DataTypes)): This sample demonstrates how to consume structured data types defined and used by the OPC UA server.
- Server.DataTypes ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server.DataTypes), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server.DataTypes)): This sample demonstrates how to setup an OPC UA server to provide nodes with custom data types.

The following sections assume that the server provides a node with the NodeId `ns=2;s=Machine/Operator`. This node uses the fictitious data type **`StaffType`**.
The data type has the following structure:

```
StaffType
  .Name : string
  .ID : int
  .Shift : ShiftInfoType
    .Name : string
    .Elapsed : DateTime
    .Remaining : byte
```

## Access data with dynamic

Types used: `OpcClient`, `OpcNodeId`, `OpcValue`, `OpcDataObject`

For simple access to the values of variable nodes with structured data, the OPC UA .NET SDK supports the `dynamic` keyword. .NET evaluates access to variables declared with `dynamic` at runtime. This lets you access the data of a structured data type without implementing a .NET type first. This is what such access looks like:

```csharp
client.UseDynamic = true;
client.Connect();
 
dynamic staff = client.ReadNode("ns=2;s=Machine/Operator").Value;
 
// Access the 'Name' and 'ID' field of the data without to declare the data type itself.
// Just use the field names known as they would be defined in a .NET Type.
Console.WriteLine("Name: {0}", staff.Name);
Console.WriteLine("Staff ID: {0}", staff.ID);
 
// Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name);
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed);
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining);
 
// Change Shift
staff.Name = "John";
staff.ID = 4242;
staff.Shift.Name = "Swing Shift";
 
client.WriteNode("ns=2;s=Machine/Operator", staff);

```

```vbnet
client.UseDynamic = True
client.Connect()

Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value

'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as they would be defined in a .NET Type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)

'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)

'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"

client.WriteNode("ns=2;s=Machine/Operator", staff)

```

## Access data by name with OpcDataObject

Types used: `OpcClient`, `OpcNodeId`, `OpcValue`, `OpcDataObject`, `OpcDataField`

For name-based access to the values of variable nodes with structured data, use the `OpcDataObject` class directly. If you know the names of the fields of the data type, access the fields like this:

```csharp
client.UseDynamic = true;
client.Connect();
 
OpcDataObject staff = client.ReadNode("ns=2;s=Machine/Operator").As<OpcDataObject>();
 
// Access the 'Name' and 'ID' field of the data without to declare the data type itself.
// Just use the field names known as the 'key' to access the according field value.
Console.WriteLine("Name: {0}", staff["Name"].Value);
Console.WriteLine("Staff ID: {0}", staff["ID"].Value);
 
// Continue accessing subsequently used data types using the OpcDataObject as before.
OpcDataObject shift = (OpcDataObject)staff["Shift"].Value;
 
Console.WriteLine("Shift: {0}", shift["Name"].Value);
Console.WriteLine("- Time Elapsed: {0}", shift["Elapsed"].Value);
Console.WriteLine("- Jobs Remaining: {0}", shift["Remaining"].Value);
 
// Change Shift
staff["Name"].Value = "John";
staff["ID"].Value = 4242;
shift["Name"].Value = "Swing Shift";
 
client.WriteNode("ns=2;s=Machine/Operator", staff);

```

```vbnet
client.UseDynamic = True
client.Connect()

Dim staff As OpcDataObject = client.ReadNode("ns=2;s=Machine/Operator").As(Of OpcDataObject)()

'Access the 'Name' and 'ID' field of the data without to declare the data type itself.
'Just use the field names known as the 'key' to access the according field value.
Console.WriteLine("Name: {0}", staff("Name").Value)
Console.WriteLine("Staff ID: {0}", staff("ID").Value)

'Continue accessing subsequently used data types using the OpcDataObject as before.
Dim shift As OpcDataObject = CType(staff("Shift").Value, OpcDataObject)

Console.WriteLine("Shift: {0}", shift("Name").Value)
Console.WriteLine("- Time Elapsed: {0}", shift("Elapsed").Value)
Console.WriteLine("- Jobs Remaining: {0}", shift("Remaining").Value)

'Change Shift
staff("Name").Value = "John"
staff("ID").Value = 4242
shift("Name").Value = "Swing Shift"

client.WriteNode("ns=2;s=Machine/Operator", staff)

```

## Load type information for dynamic access

Types used: `OpcClient`, `OpcNodeSet`, `OpcAutomatism`, `OpcDataTypeSystem`, `OpcRawObject`, `OpcData`

For access with `dynamic` (section [Access data with dynamic](https://www.traeger.de/en/docs/development/opcua/client/structured-data#access-data-with-dynamic)) and for name-based access (section [Access data by name with OpcDataObject](https://www.traeger.de/en/docs/development/opcua/client/structured-data#access-data-by-name-with-opcdataobject)), the client needs information about the data types of the server. In Visual Basic, `dynamic` corresponds to an untyped declaration with `Dim`. The `OpcClient` class retrieves this information from the server automatically when you call `Connect()`, provided you set the `UseDynamic` property to `true` **before** that call. This property exists on the `OpcClient` instance and on the static `OpcAutomatism` class.

With this setting, the client loads the required type information from the server. After calling `Connect()`, you access the data as shown in the previous sections, without writing code for the types yourself.

If you have a `UANodeSet.xml` or another XML file that describes the server, you can also load this file into the client application. The content of such a file starts with `UANodeSet`. The client then takes the information from the NodeSet and does not have to retrieve the type information when it connects. To load the NodeSet:

```csharp
using (var client = new OpcClient("opc.tcp://localhost:4840")) {
    client.NodeSet = OpcNodeSet.Load(@"..\Resources\MyServersNodeSet.xml");
    client.UseDynamic = true;
 
    client.Connect();
    dynamic staff = client.ReadNode("ns=2;s=Machine/Operator").Value;
 
    Console.WriteLine("Name: {0}", staff.Name);
    Console.WriteLine("Staff ID: {0}", staff.ID);
}
```

```vbnet
Using client = New OpcClient("opc.tcp://localhost:4840")
    client.NodeSet = OpcNodeSet.Load("..\Resources\MyServersNodeSet.xml")
    client.UseDynamic = True

    client.Connect()
    Dim staff = client.ReadNode("ns=2;s=Machine/Operator").Value

    Console.WriteLine("Name: {0}", staff.Name)
    Console.WriteLine("Staff ID: {0}", staff.ID)
End Using
```

If a NodeSet is set, the `GetDataTypeSystem()` method of the `OpcClient` class returns an instance of `OpcDataTypeSystem` that describes the type system from the NodeSet. If you do not set a NodeSet through the `NodeSet` property, the returned instance describes the type system of the server. The client requests the required information from the server when it needs it. In both cases, the call requires an existing connection to the server. Large type systems can exceed the data transfer limits (section [Data transfer limits](https://www.traeger.de/en/docs/development/opcua/client/configuration#data-transfer-limits)).

**Unknown structures**: If the client does not know the data type of a structure, or a field has no fixed ValueRank, it cannot decode the value. It then returns it as an `OpcRawObject`. Its `Encoding` property states the encoding (binary or XML), and its `RawData` property contains the encoded body. With the `Decode<T>` method, you decode the body once the type is known. Conversely, `OpcData.Encode` encodes a structure as an `OpcRawObject`.

## Access data with your own .NET types

Types used: `OpcClient`, `OpcNodeId`, `OpcDataTypeAttribute`, `OpcDataTypeEncodingAttribute`

For typed access, you define .NET types in the client application the same way the server defines them. Attributes provide all the required metadata. By default, the SDK registers types with these attributes automatically when it connects. You can turn this off with the `OpcAutomatism.UseDynamicTypeRegistration` property. To define .NET types for structured data types:

```csharp
[OpcDataType("ns=2;s=StaffType")]
[OpcDataTypeEncoding("ns=2;s=StaffType-Binary")]
public class Staff
{
    public string Name { get; set; }
    public int ID { get; set; }
    public ShiftInfo Shift { get; set; }
}
 
[OpcDataType("ns=2;s=ShiftInfoType")]
[OpcDataTypeEncoding("ns=2;s=ShiftInfoType-Binary")]
public class ShiftInfo
{
    public string Name { get; set; }
    public DateTime Elapsed { get; set; }
    public byte Remaining { get; set; }
}

```

```vbnet
<OpcDataType("ns=2;s=StaffType")>
<OpcDataTypeEncoding("ns=2;s=StaffType-Binary")>
Public Class Staff
    Public Property Name As String
    Public Property ID As Integer
    Public Property Shift As ShiftInfo
End Class

<OpcDataType("ns=2;s=ShiftInfoType")>
<OpcDataTypeEncoding("ns=2;s=ShiftInfoType-Binary")>
Public Class ShiftInfo
    Public Property Name As String
    Public Property Elapsed As DateTime
    Public Property Remaining As Byte
End Class

```

Once you have defined a client-side .NET type for a structured data type defined on the server, use it like this:

```csharp
client.Connect();
Staff staff = client.ReadNode("ns=2;s=Machine/Operator").As<Staff>();
 
// Access the 'Name' and 'ID' field of the data with the declared the data type.
Console.WriteLine("Name: {0}", staff.Name);
Console.WriteLine("Staff ID: {0}", staff.ID);
 
// Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name);
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed);
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining);
 
// Change Shift
staff.Name = "John";
staff.ID = 4242;
staff.Shift.Name = "Swing Shift";
 
client.WriteNode("ns=2;s=Machine/Operator", staff);

```

```vbnet
client.Connect()
Dim staff As Staff = client.ReadNode("ns=2;s=Machine/Operator").As(Of Staff)()

'Access the 'Name' and 'ID' field of the data with the declared the data type.
Console.WriteLine("Name: {0}", staff.Name)
Console.WriteLine("Staff ID: {0}", staff.ID)

'Continue accessing subsequently used data types.
Console.WriteLine("Shift: {0}", staff.Shift.Name)
Console.WriteLine("- Time Elapsed: {0}", staff.Shift.Elapsed)
Console.WriteLine("- Jobs Remaining: {0}", staff.Shift.Remaining)

'Change Shift
staff.Name = "John"
staff.ID = 4242
staff.Shift.Name = "Swing Shift"

client.WriteNode("ns=2;s=Machine/Operator", staff)

```

## Generate data types

For typed access (section [Access data with your own .NET types](https://www.traeger.de/en/docs/development/opcua/client/structured-data#access-data-with-your-own-net-types)), you can generate single or all data types of an OPC UA server with OPC Watch (page [Downloads](https://www.traeger.de/en/docs/development/opcua/downloads)).

**Generate from the server**

To generate a single data type as a .NET implementation:

1. Open OPC Watch
2. Create a new connection (+) and configure it if needed
3. Connect to the server (plug icon)
4. Select a node:
   - either a variable node with a structured data type as its value
   - or a data type node below `/Types/DataTypes/BaseDataType/Structure` or `/Enumeration`
5. Right-click this node
6. Click "Generate DataType"
7. Paste the code into your application

You can generate all data types into either a code file (`*.cs`) or an assembly file (`*.dll`). To do so:

1. Open OPC Watch
2. Create a new connection (+) and configure it if needed
3. Connect to the server (plug icon)
4. Select the server node whose name starts with `opc.tcp://`
5. Right-click this node
6. Click "Generate Models"
7. Select the file type you want in the dialog
8. Click "Save"
9. Add the file to your project

**Generate from a NodeSet**

1. Open OPC Watch
2. Click the first icon in the upper right corner of the application
3. Open the NodeSet file, usually an XML file of a companion specification
4. OPC Watch loads the NodeSet and then shows the "Save As" dialog
5. Select the file type you want in the dialog
6. Click "Save"
7. Add the file to your project

## Define data types

Types used: `OpcClient`, `OpcNodeId`, `OpcData`, `OpcDataTypeAttribute`, `OpcDataTypeEncodingAttribute`, `OpcDataTypeSystem`, `OpcDataTypeInfo`

Instead of generating the data type as .NET code or a .NET assembly (section [Generate data types](https://www.traeger.de/en/docs/development/opcua/client/structured-data#generate-data-types)), you can also define it manually. To do so, implement the type the same way the server defines it. The .NET type must provide the fields of the structured data type in exactly the same order and with the same types, regardless of whether it is a struct or a class. Attributes provide all other metadata:

```csharp
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
        "<NodeId of Binary Encoding Node>",
        NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal struct MyDataType
{
    public short FieldA;
    public int FieldB;
    public string FieldC;
    ...
}

```

```vbnet
<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Structure MyDataType
    Public FieldA As Short
    Public FieldB As Integer
    Public FieldC As String
    ...
End Structure

```

You get the required information from the manual of the OPC UA server, from the team that programs the PLC, or through the `OpcClient` class. With `OpcClient`, you examine the variable node that uses the structured data type:

```csharp
OpcNodeInfo node = client.BrowseNode("ns=2;s=Machine/Operator");
 
if (node is OpcVariableNodeInfo variableNode) {
    OpcNodeId dataTypeId = variableNode.DataTypeId;
    OpcDataTypeInfo dataType = client.GetDataTypeSystem().GetType(dataTypeId);
 
    Console.WriteLine(dataType.TypeId);
    Console.WriteLine(dataType.Encoding);
 
    Console.WriteLine(dataType.Name);
 
    foreach (OpcDataFieldInfo field in dataType.GetFields())
        Console.WriteLine(".{0} : {1}", field.Name, field.FieldType);
 
    Console.WriteLine();        
    Console.WriteLine("Data Type Attributes:");
    Console.WriteLine(
            "\t[OpcDataType(\"{0}\")]",
            dataType.TypeId.ToString(OpcNodeIdFormat.Foundation));
    Console.WriteLine(
            "\t[OpcDataTypeEncoding(\"{0}\", NamespaceUri = \"{1}\")]",
            dataType.Encoding.Id.ToString(OpcNodeIdFormat.Foundation),
            dataType.Encoding.Namespace.Value);
}

```

```vbnet
Dim node As OpcNodeInfo = client.BrowseNode("ns=2;s=Machine/Operator")
Dim variableNode As OpcVariableNodeInfo = TryCast(node, OpcVariableNodeInfo)

If variableNode IsNot Nothing Then
    Dim dataTypeId As OpcNodeId = variableNode.DataTypeId
    Dim dataType As OpcDataTypeInfo = client.GetDataTypeSystem().GetType(dataTypeId)

    Console.WriteLine(dataType.TypeId)
    Console.WriteLine(dataType.Encoding)

    Console.WriteLine(dataType.Name)

    For Each field As OpcDataFieldInfo In dataType.GetFields()
        Console.WriteLine(".{0} : {1}", field.Name, field.FieldType)
    Next

    Console.WriteLine()
    Console.WriteLine("Data Type Attributes:")
    Console.WriteLine( _
            vbTab & "[OpcDataType(""{0}"")]", _
            dataType.TypeId.ToString(OpcNodeIdFormat.Foundation))
    Console.WriteLine( _
            vbTab & "[OpcDataTypeEncoding(""{0}"", NamespaceUri = ""{1}"")]", _
            dataType.Encoding.Id.ToString(OpcNodeIdFormat.Foundation), _
            dataType.Encoding.Namespace.Value)
End If

```

## Union data types

Types used: `OpcClient`, `OpcDataTypeAttribute`, `OpcDataTypeEncodingAttribute`, `IOpcUnion`

A union data type always transfers only one of several fields. A preceding switch value determines which field is transferred. To define such a data type, the .NET type implements the `IOpcUnion` interface. Its `SwitchValue` property contains the switch value. The value `1` selects the first field, the value `2` the second, and so on. With the value `0`, no field is transferred:

```csharp
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
        "<NodeId of Binary Encoding Node>",
        NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal class MyUnionDataType : IOpcUnion
{
    // 0 = no field, 1 = 'Number', 2 = 'Text'.
    public uint SwitchValue { get; set; }
 
    public int Number { get; set; }
 
    public string Text { get; set; }
}

```

```vbnet
<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Class MyUnionDataType
    Implements IOpcUnion

    '0 = no field, 1 = 'Number', 2 = 'Text'.
    Public Property SwitchValue As UInteger Implements IOpcUnion.SwitchValue

    Public Property Number As Integer

    Public Property Text As String
End Class
```

## Data types with optional fields

Types used: `OpcClient`, `OpcNodeId`, `OpcData`, `OpcDataTypeAttribute`, `OpcDataTypeEncodingAttribute`, `OpcDataTypeEncodingMaskAttribute`, `OpcEncodingMaskKind`, `OpcDataTypeMemberSwitchAttribute`, `OpcMemberSwitchOperator`

To reduce the amount of data transferred, you can mark certain fields of a structured data type as present or missing in the data stream. This depends on certain conditions or on the value of another field in the data structure. Whether a field is present is therefore indicated either by the value of another field or by a single bit in the `EncodingMask`. The `EncodingMask` is a field encoded as a preamble before the data in the data stream. You specify its size in bytes in the `OpcDataTypeEncodingMaskAttribute`. If you do not set the `Size` property explicitly, `OpcEncodingMaskKind.Auto` gives it the smallest number of bytes required, calculated from the number of optional fields.

With `OpcEncodingMaskKind.Auto`, there are three ways to define optional fields whose presence a bit of the `EncodingMask` indicates. Use nullable types (`Nullable`), the `OpcDataTypeMemberSwitchAttribute` without parameters (the next free bit), or the `OpcDataTypeMemberSwitchAttribute` with the number of a specific bit:

```csharp
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
        "<NodeId of Binary Encoding Node>",
        NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
[OpcDataTypeEncodingMask(OpcEncodingMaskKind.Auto)]
internal struct MyDataTypeWithOptionalFields
{
    public short FieldA;
    public int FieldB;
    public string FieldC;
 
    // Nullables are treat as optional fields by default.
    // Existence-Indicator-Bit is Bit0 in the encoding mask.
    public uint? OptionalField1;
 
    // Existence-Indicator-Bit is Bit1 (the next unused bit) in the encoding mask.
    [OpcDataTypeMemberSwitch]
    public int OptionalField2;
 
    // Existence-Indicator-Bit is Bit3 (bit 2 is unused) in the encoding mask.
    [OpcDataTypeMemberSwitch(bit: 3)]
    public byte OptionalField3;
}
```

```vbnet
<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
<OpcDataTypeEncodingMask(OpcEncodingMaskKind.Auto)>
Friend Structure MyDataTypeWithOptionalFields
    Public FieldA As Short
    Public FieldB As Integer
    Public FieldC As String

    'Nullables are treat as optional fields by default.
    'Existence-Indicator-Bit is Bit0 in the encoding mask.
    Public OptionalField1 As UInteger?

    'Existence-Indicator-Bit is Bit1 (the next unused bit) in the encoding mask.
    <OpcDataTypeMemberSwitch>
    Public OptionalField2 As Integer

    'Existence-Indicator-Bit is Bit3 (bit 2 is unused) in the encoding mask.
    <OpcDataTypeMemberSwitch(3)>
    Public OptionalField3 As Byte
End Structure

```

If the presence of a field should instead depend on the value of a preceding field, specify the name of that field in the `OpcDataTypeMemberSwitchAttribute`, optionally together with a comparison value and an `OpcMemberSwitchOperator`. Such a field does not use a bit of the `EncodingMask`:

```csharp
[OpcDataType("<NodeId of DataType Node>")]
[OpcDataTypeEncoding(
        "<NodeId of Binary Encoding Node>",
        NamespaceUri = "<NamespaceUri.Value of binary Dictionary-Node>")]
internal struct MyDataTypeWithConditionalFields
{
    public bool FieldD;
 
    // 'OptionalField4' exists only if the value of 'FieldD' is equals 'true'.
    [OpcDataTypeMemberSwitch("FieldD")]
    public string OptionalField4;
 
    public int FieldE;
 
    // 'OptionalField5' exists only if the value of 'FieldE' is greater than '42'.
    [OpcDataTypeMemberSwitch("FieldE", value: 42, @operator: OpcMemberSwitchOperator.GreaterThan)]
    public string OptionalField5;
}
```

```vbnet
<OpcDataType("<NodeId of DataType Node>")>
<OpcDataTypeEncoding( _
        "<NodeId of Binary Encoding Node>", _
        NamespaceUri:="<NamespaceUri.Value of binary Dictionary-Node>")>
Friend Structure MyDataTypeWithConditionalFields
    Public FieldD As Boolean

    ''OptionalField4' exists only if the value of 'FieldD' is equals 'true'.
    <OpcDataTypeMemberSwitch("FieldD")>
    Public OptionalField4 As String

    Public FieldE As Integer

    ''OptionalField5' exists only if the value of 'FieldE' is greater than '42'.
    <OpcDataTypeMemberSwitch("FieldE", 42, OpcMemberSwitchOperator.GreaterThan)>
    Public OptionalField5 As String
End Structure

```

In OPC UA, a DataTypeDefinition describes optional fields only through bits of the `EncodingMask`. Other clients that know such a data type only through its DataTypeDefinition might therefore not be able to decode it.
