How-to guide
Structured data
Access structured data types in a simple, name-based, dynamic, or typed way, and generate and define data types.
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 : byteAccess 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:
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);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:
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);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:
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);
}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 UsingIf 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:
[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; }
}<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 ClassOnce you have defined a client-side .NET type for a structured data type defined on the server, use it like this:
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);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:
- Open OPC Watch
- Create a new connection (+) and configure it if needed
- Connect to the server (plug icon)
- Select a node:
- either a variable node with a structured data type as its value
- or a data type node below
/Types/DataTypes/BaseDataType/Structureor/Enumeration
- Right-click this node
- Click "Generate DataType"
- 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:
- Open OPC Watch
- Create a new connection (+) and configure it if needed
- Connect to the server (plug icon)
- Select the server node whose name starts with
opc.tcp:// - Right-click this node
- Click "Generate Models"
- Select the file type you want in the dialog
- Click "Save"
- Add the file to your project
Generate from a NodeSet
- Open OPC Watch
- Click the first icon in the upper right corner of the application
- Open the NodeSet file, usually an XML file of a companion specification
- OPC Watch loads the NodeSet and then shows the "Save As" dialog
- Select the file type you want in the dialog
- Click "Save"
- 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:
[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;
...
}<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 StructureYou 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:
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);
}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 IfUnion 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:
[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; }
}<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 ClassData 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:
[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;
}<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 StructureIf 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:
[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;
}<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 StructureIn 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.

