Skip to content

How-to guide

Structured data

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

How-to guide

Structured data ​

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 usedOpcClientOpcNodeIdOpcValueOpcDataObject

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 usedOpcClientOpcNodeIdOpcValueOpcDataObjectOpcDataField

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 usedOpcClientOpcNodeSetOpcAutomatismOpcDataTypeSystemOpcRawObjectOpcData

For access with dynamic (section Access data with dynamic) and for name-based access (section 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).

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 usedOpcClientOpcNodeIdOpcDataTypeAttributeOpcDataTypeEncodingAttribute

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), you can generate single or all data types of an OPC UA server with OPC Watch (page 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 usedOpcClientOpcNodeIdOpcDataOpcDataTypeAttributeOpcDataTypeEncodingAttributeOpcDataTypeSystemOpcDataTypeInfo

Instead of generating the data type as .NET code or a .NET assembly (section 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 usedOpcClientOpcDataTypeAttributeOpcDataTypeEncodingAttributeIOpcUnion

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 usedOpcClientOpcNodeIdOpcDataOpcDataTypeAttributeOpcDataTypeEncodingAttributeOpcDataTypeEncodingMaskAttributeOpcEncodingMaskKindOpcDataTypeMemberSwitchAttributeOpcMemberSwitchOperator

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.