# NodeSets

> How-to guide: Import information models as NodeSets into the server and implement their nodes.

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

**Sample applications:**

- Server.NodeTypes ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server.NodeTypes), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server.NodeTypes)): This use case demonstrates how to import and implement custom node types using a nodeset.
- Server.NodeSet.PackML ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.NodeSet.PackML), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.NodeSet.PackML)): This use case demonstrates how to import and implement an alarm from a nodeset using the companion specification from PackML.
- Server.NodeSet.Umati.MachineTool ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.NodeSet.Umati.MachineTool), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.NodeSet.Umati.MachineTool)): This use case demonstrates how to import and implement an alarm from a nodeset using the companion specifications from UMATI: In this case 'MachineTool'.
- Server.SimaticNodeSet ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Scenarios/Server.SimaticNodeSet), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Scenarios/Server.SimaticNodeSet)): This use case demonstrates how to setup an OPC UA server that imports the nodeset of a SIMATIC S7-1500 controller and identifies itself like the server of the controller, e.g. to test clients without the hardware.

NodeSets describe the content of the address space of a server in XML (eXtensible Markup Language). The description defines data types with their transported logical structure (data-dependent information) and their physical structure (in memory). A NodeSet also contains the definitions of node types and of concrete node instances. The root element `UANodeSet` also describes the references between the nodes. This applies not only to the nodes from this NodeSet but also to nodes from other NodeSets and from the specification.

Companion specifications not only extend the general definition of the address space but also bring their own data types and node types, among others. Each companion specification therefore comes with one or more NodeSets. For a server to comply with a companion specification, it must implement the types and behavior defined there. The [Companion specifications](https://www.traeger.de/en/docs/development/opcua/server/nodesets#companion-specifications) section shows what a Companion SDK takes care of.

NodeSets also describe the address space in PLC engineering. You can export the entire configuration of a PLC from engineering software such as TIA Portal. With this export, you can initialize the OPC UA server of the PLC.

Whatever the source, if a server should use a NodeSet, it must provide the logic to import and implement the address space described in it. The following sections show how this works.

## Import NodeSets

Types used: `OpcNodeSet`, `OpcNodeSetManager`, `OpcNodeManager`, `OpcNodeSetImportOptions`

With the `OpcNodeSetManager`, you import the nodes that a NodeSet describes 1:1:

```csharp
var umatiManager = OpcNodeSetManager.Create(
        OpcNodeSet.Load(@".\umati.xml"),
        OpcNodeSet.Load(@".\umati-instances.xml"));

using (var server = new OpcServer("opc.tcp://localhost:4840/", umatiManager)) {
    server.Start();
    ...
}

```

```vbnet
Dim umatiManager = OpcNodeSetManager.Create( _
        OpcNodeSet.Load(".\umati.xml"), _
        OpcNodeSet.Load(".\umati-instances.xml"))

Using server As New OpcServer("opc.tcp://localhost:4840/", umatiManager)
    server.Start()
    ' ...
End Using

```

When you call `OpcNodeSetManager.Create`, you can specify one or more NodeSets. The resulting `OpcNodeManager` imports the NodeSets. When the server starts, it creates the nodes defined in them in the address space of the server. Besides a file path, the `OpcNodeSet.Load` method also accepts a `Stream`, for example for a NodeSet embedded in the application as a resource. With the `GetNodes` and `GetNode` methods, you can also evaluate the nodes described in the NodeSet:

```csharp
using (var stream = File.OpenRead(@".\umati.xml")) {
    var nodeSet = OpcNodeSet.Load(stream);
 
    foreach (var node in nodeSet.GetNodes())
        Console.WriteLine("{0}: {1}", node.Id, node.DisplayName);
}

```

```vbnet
Using stream = File.OpenRead(".\umati.xml")
    Dim nodeSet = OpcNodeSet.Load(stream)

    For Each node In nodeSet.GetNodes()
        Console.WriteLine("{0}: {1}", node.Id, node.DisplayName)
    Next
End Using
```

The `ImportOptions` property of the `OpcNodeSetManager` controls how the node manager creates the imported nodes (default `OpcNodeSetImportOptions.None`):

- `CompleteDataTypeDefinitionFields`: adds the fields of its base types to the DataTypeDefinition of a structure type.
- `CompleteDataTypeDefinitionDefaultEncoding`: adds a missing `DefaultEncodingId` to the DataTypeDefinition.
- `CreateNonGenericNodeInstances`: creates the typed classes of the OPC UA .NET SDK for the imported nodes instead of generic instances.

The first two options apply to all NodeSets of the server:

```csharp
umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields
        | OpcNodeSetImportOptions.CreateNonGenericNodeInstances;
```

```vbnet
umatiManager.ImportOptions = OpcNodeSetImportOptions.CompleteDataTypeDefinitionFields _
        Or OpcNodeSetImportOptions.CreateNonGenericNodeInstances
```

If a custom node manager should import a NodeSet instead, override the `ImportNodes` method of the `OpcNodeManager` class:

```csharp
protected override IEnumerable<OpcNodeSet> ImportNodes()
{
    yield return OpcNodeSet.Load(@".\umati.xml");
    yield return OpcNodeSet.Load(@".\umati-instances.xml");
}
```

```vbnet
Protected Overrides Iterator Function ImportNodes() As IEnumerable(Of OpcNodeSet)
    Yield OpcNodeSet.Load(".\umati.xml")
    Yield OpcNodeSet.Load(".\umati-instances.xml")
End Function

```

## Connect imported NodeSets to logic

Types used: `OpcNodeManager`, `IOpcNode`

Importing a NodeSet alone is often not enough, because the logic that connects the underlying system is still missing. This logic maps, for example, reading a node to reading a word in a data block.
The same applies to writing a node.

If you create the node manager through `OpcNodeSetManager.Create`, you set the logic through the `ImplementNodeCallback` property of the `OpcNodeSetManager`. The SDK calls the callback for each imported node. For a UMATI NodeSet, the logic that simulates the status of a lamp could look like this:

```csharp
var lampTypeId = OpcNodeId.Parse("ns=2;i=1041");
var random = new Random();
 
var umatiManager = OpcNodeSetManager.Create(
        OpcNodeSet.Load(@".\umati.xml"),
        OpcNodeSet.Load(@".\umati-instances.xml"));
 
umatiManager.ImplementNodeCallback = (context, node) => {
    var variableNode = node as OpcVariableNode;
 
    if (variableNode == null || variableNode.Name != "2:Status")
        return;
 
    var objectNode = variableNode.Parent as OpcObjectNode;
 
    if (objectNode != null && objectNode.TypeDefinitionId == lampTypeId) {
        variableNode.ReadVariableValueCallback = (readContext, value)
                => new OpcVariableValue<object>(random.Next(minValue: 0, maxValue: 2));
    }
};

```

```vbnet
Dim lampTypeId = OpcNodeId.Parse("ns=2;i=1041")
Dim random = New Random()

Dim umatiManager = OpcNodeSetManager.Create( _
        OpcNodeSet.Load(".\umati.xml"), _
        OpcNodeSet.Load(".\umati-instances.xml"))

umatiManager.ImplementNodeCallback = Sub(context, node)
    Dim variableNode = TryCast(node, OpcVariableNode)

    If variableNode Is Nothing OrElse variableNode.Name <> "2:Status" Then
        Return
    End If

    Dim objectNode = TryCast(variableNode.Parent, OpcObjectNode)

    If objectNode IsNot Nothing AndAlso objectNode.TypeDefinitionId = lampTypeId Then
        variableNode.ReadVariableValueCallback = Function(readContext, value) _
                New OpcVariableValue(Of Object)(random.Next(minValue:=0, maxValue:=2))
    End If
End Sub
```

In a custom node manager, you override the `OpcNodeManager.ImplementNode` method for this logic instead. The SDK calls the method for each node that is imported or created through `CreateNodes`. After the server has started, it also calls the method for each node that a client adds to the address space of the node manager. Call the base implementation in the method. Among other things, it adds the references to the interfaces (HasInterface) that a node implements:

```csharp
protected override void ImplementNode(IOpcNode node)
{
    // Keeps the default implementation, e.g. the references to the interfaces of the node.
    base.ImplementNode(node);
 
    // Implement your Node(s) here.
}
```

```vbnet
Protected Overrides Sub ImplementNode(ByVal node As IOpcNode)
    'Keeps the default implementation, e.g. the references to the interfaces of the node.
    MyBase.ImplementNode(node)

    'Implement your Node(s) here.
End Sub

```

For the UMATI NodeSet shown above, the implementation in a custom node manager looks like this:

```csharp
private static readonly OpcNodeId LampTypeId = "ns=2;i=1041";
private readonly Random random = new Random();

protected override void ImplementNode(IOpcNode node)
{
    if (node is OpcVariableNode variableNode && variableNode.Name == "2:Status") {
        if (variableNode?.Parent is OpcObjectNode objectNode && objectNode.TypeDefinitionId == LampTypeId) {
            variableNode.ReadVariableValueCallback = (context, value) => new OpcVariableValue<object>(this.random.Next(0, 2));
        }
    }
}

```

```vbnet
Private Shared ReadOnly LampTypeId As OpcNodeId = "ns=2;i=1041"
Private random As New Random()

Protected Overrides Sub ImplementNode(node As IOpcNode)
    Dim variableNode = TryCast(node, OpcVariableNode)
    
    If variableNode IsNot Nothing AndAlso variableNode.Name = "2:Status" Then
        Dim objectNode = TryCast(variableNode.Parent, OpcObjectNode)
        If objectNode IsNot Nothing AndAlso objectNode.TypeDefinitionId = LampTypeId Then
            variableNode.ReadVariableValueCallback = Function(context, value) New OpcVariableValue(Of Object)(Me.random.Next(0, 2))
        End If
    End If
End Sub
```

## Companion specifications

Types used: `OpcServer`, `OpcNodeManager`, `OpcNodeSetManager`, `OpcNamespace`, `OpcObjectNode`, `OpcNodeModel`

You can import and implement a companion specification like any other NodeSet. For many companion specifications, there is also a separate package of the SDK (Companion SDK), for example `Opc.UaFx.Advanced.Machinery` for OPC UA for Machinery (OPC 40001-1) or `Opc.UaFx.Advanced.ISA95.JobControl` for ISA-95 Job Control (OPC 10031-4). The [OPC UA Companion SDKs](https://www.traeger.de/en/products/development/opcua/companions) product page lists the available Companion SDKs. Using OPC UA for Machinery as an example, the following sections show what such a package includes and which steps you have to do by hand without one.

### What a Companion SDK includes

| Component | Content, using `Opc.UaFx.Advanced.Machinery` as an example |
|---|---|
| Embedded NodeSet | The NodeSet of the specification is embedded in the assembly; you neither have to obtain it nor ship it. |
| Dependencies | The server class `OpcMachineryServer` includes the node managers of all required specifications (DI, IA, and Machinery). |
| Constants | `OpcMachineryNodes` contains the namespace URI, the namespace (`Namespace`, `GetName`), and the `OpcNodeId` of all types and instances of the specification, for example MachineIdentificationType or `MachinesNode`. |
| Typed node classes | A class for each object type, for example `OpcMachineIdentificationNode`, with a property for each child node (`Manufacturer`, SerialNumber, YearOfConstruction, and so on). When you create it, the class creates the child nodes with the name, namespace, and data type from the specification. |
| Interfaces | The interfaces of the specification are implemented as .NET interfaces. The SDK creates the corresponding HasInterface references automatically when it implements the nodes. |
| Typed methods | Methods of the specification are implemented as overridable methods with typed parameters. Without an implementation, they throw a `NotImplementedException`. |
| Node manager | `OpcMachineryNodeManager` imports the embedded NodeSet, resolves the namespace, and provides the `Machines` folder. |
| Client class | `OpcMachineryClient` registers the data types of the specification and its dependencies and resolves the namespaces after connecting, so that the constants from `OpcMachineryNodes` also apply in the client. |

### Machine with a Companion SDK

The following example provides a machine with its identification according to OPC UA for Machinery. The `OpcMachineIdentificationNode` class creates the child nodes of the identification itself. You only set the values:

```csharp
using (var server = new OpcMachineryServer("opc.tcp://localhost:4840/")) {
    server.Start();
 
    OpcMachineryNodeManager machinery = server.NodeManagers
            .OfType<OpcMachineryNodeManager>()
            .First();
 
    var machine = new OpcObjectNode(machinery.Machines, "CNC-042");
    var identification = new OpcMachineIdentificationNode(machine, "Identification");
 
    identification.Manufacturer.Value = "Example Machines Ltd.";
    identification.SerialNumber.Value = "SN-0042";
    identification.ProductInstanceUri.Value = "urn:example:cnc-0042";
 
    machinery.AddNode(server.SystemContext, machine);
}
```

```vbnet
Using server = New OpcMachineryServer("opc.tcp://localhost:4840/")
    server.Start()

    Dim machinery As OpcMachineryNodeManager = server.NodeManagers _
            .OfType(Of OpcMachineryNodeManager)() _
            .First()

    Dim machine = New OpcObjectNode(machinery.Machines, "CNC-042")
    Dim identification = New OpcMachineIdentificationNode(machine, "Identification")

    identification.Manufacturer.Value = "Example Machines Ltd."
    identification.SerialNumber.Value = "SN-0042"
    identification.ProductInstanceUri.Value = "urn:example:cnc-0042"

    machinery.AddNode(server.SystemContext, machine)
End Using
```

Unless you specify otherwise, a node class of a Companion SDK creates all child nodes of its type, including the optional ones. An `OpcNodeModel` controls which optional child nodes it creates. The [Node models](https://www.traeger.de/en/docs/development/opcua/server/node-models) page describes this.

### Machine without a Companion SDK

Without a Companion SDK, the same server requires these steps:

1. Obtain the NodeSet files of the specification and all its dependencies, ship them with the application, and load them in the correct order (DI, IA, Machinery).
2. Resolve the namespaces of the specifications by their URI to build the `OpcNodeId` and `OpcName` of the types and instances.
3. Create the instance and set its type definition (`TypeDefinitionId`).
4. Create each child node yourself with the name, namespace, and data type from the specification.
5. Add the references to the interfaces of the type (HasInterface) by hand.

```csharp
public class HandmadeMachineManager : OpcNodeManager
{
    private static readonly OpcNodeId HasInterface = OpcNodeId.Parse("i=17603");
 
    public HandmadeMachineManager()
        : base("http://example.com/machines/")
    {
    }
 
    public static OpcServer CreateServer()
    {
        var nodeSets = OpcNodeSetManager.Create(
                OpcNodeSet.Load(@"NodeSets\Opc.Ua.Di.NodeSet2.xml"),
                OpcNodeSet.Load(@"NodeSets\Opc.Ua.IA.NodeSet2.xml"),
                OpcNodeSet.Load(@"NodeSets\Opc.Ua.Machinery.NodeSet2.xml"));
 
        return new OpcServer("opc.tcp://localhost:4840/", nodeSets, new HandmadeMachineManager());
    }
 
    protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
    {
        var di = OpcNamespace.Create("http://opcfoundation.org/UA/DI/");
        var machinery = OpcNamespace.Create("http://opcfoundation.org/UA/Machinery/");
        di.Resolve(this);
        machinery.Resolve(this);
 
        // Machinery: Machines folder (i=1001).
        var machine = new OpcObjectNode("CNC-042");
 
        references.Add(
                machine,
                machinery.GetId(1001),
                direction: OpcReferenceDirection.ChildToParent,
                referenceType: OpcReferenceType.Organizes);
 
        // Machinery: MachineIdentificationType (i=1012).
        var identification = new OpcObjectNode(machine, machinery.GetName("Identification"));
        identification.TypeDefinitionId = machinery.GetId(1012);
 
        // Mandatory properties: names, namespaces and data types from the specification.
        var manufacturer = new OpcTextPropertyNode(
                identification,
                di.GetName("Manufacturer"),
                value: new OpcText("Example Machines Ltd."));
 
        var serialNumber = new OpcPropertyNode<string>(
                identification,
                di.GetName("SerialNumber"),
                id: OpcNodeId.Null,
                value: "SN-0042");
 
        var instanceUri = new OpcPropertyNode<string>(
                identification,
                di.GetName("ProductInstanceUri"),
                id: OpcNodeId.Null,
                value: "urn:example:cnc-0042");
 
        // Interfaces of the type: IMachineVendorNameplateType (i=1010),
        // IMachineTagNameplateType (i=1011).
        references.Add(
                identification,
                machinery.GetId(1010),
                direction: OpcReferenceDirection.ParentToChild,
                referenceTypeId: HasInterface);
 
        references.Add(
                identification,
                machinery.GetId(1011),
                direction: OpcReferenceDirection.ParentToChild,
                referenceTypeId: HasInterface);
 
        yield return machine;
    }
}
```

```vbnet
Public Class HandmadeMachineManager
    Inherits OpcNodeManager

    Private Shared ReadOnly HasInterface As OpcNodeId = OpcNodeId.Parse("i=17603")

    Public Sub New()
        MyBase.New("http://example.com/machines/")
    End Sub

    Public Shared Function CreateServer() As OpcServer
        Dim nodeSets = OpcNodeSetManager.Create( _
                OpcNodeSet.Load("NodeSets\Opc.Ua.Di.NodeSet2.xml"), _
                OpcNodeSet.Load("NodeSets\Opc.Ua.IA.NodeSet2.xml"), _
                OpcNodeSet.Load("NodeSets\Opc.Ua.Machinery.NodeSet2.xml"))

        Return New OpcServer("opc.tcp://localhost:4840/", nodeSets, New HandmadeMachineManager())
    End Function

    Protected Overrides Iterator Function CreateNodes( _
            ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
        Dim di = OpcNamespace.Create("http://opcfoundation.org/UA/DI/")
        Dim machinery = OpcNamespace.Create("http://opcfoundation.org/UA/Machinery/")
        di.Resolve(Me)
        machinery.Resolve(Me)

        'Machinery: Machines folder (i=1001).
        Dim machine = New OpcObjectNode("CNC-042")

        references.Add( _
                machine, _
                machinery.GetId(1001), _
                direction:=OpcReferenceDirection.ChildToParent, _
                referenceType:=OpcReferenceType.Organizes)

        'Machinery: MachineIdentificationType (i=1012).
        Dim identification = New OpcObjectNode(machine, machinery.GetName("Identification"))
        identification.TypeDefinitionId = machinery.GetId(1012)

        'Mandatory properties: names, namespaces and data types from the specification.
        Dim manufacturer = New OpcTextPropertyNode( _
                identification, _
                di.GetName("Manufacturer"), _
                value:=New OpcText("Example Machines Ltd."))

        Dim serialNumber = New OpcPropertyNode(Of String)( _
                identification, _
                di.GetName("SerialNumber"), _
                id:=OpcNodeId.Null, _
                value:="SN-0042")

        Dim instanceUri = New OpcPropertyNode(Of String)( _
                identification, _
                di.GetName("ProductInstanceUri"), _
                id:=OpcNodeId.Null, _
                value:="urn:example:cnc-0042")

        'Interfaces of the type: IMachineVendorNameplateType (i=1010),
        'IMachineTagNameplateType (i=1011).
        references.Add( _
                identification, _
                machinery.GetId(1010), _
                direction:=OpcReferenceDirection.ParentToChild, _
                referenceTypeId:=HasInterface)

        references.Add( _
                identification, _
                machinery.GetId(1011), _
                direction:=OpcReferenceDirection.ParentToChild, _
                referenceTypeId:=HasInterface)

        Yield machine
    End Function
End Class

```

The identifiers of the nodes (for example, `i=1001` for the `Machines` folder or `i=1012` for MachineIdentificationType) come from the NodeSet of the specification. The SDK offers no constant for the HasInterface reference type. The example therefore uses its `OpcNodeId` (`i=17603`).

### Implement methods

The effort also differs for methods of the specification. Without a Companion SDK, you look up the imported method node by its name and implement it through a command (`IOpcMethodCommand`). You take the order and types of the arguments from the specification. The arguments arrive as untyped lists:

```csharp
nodeSets.ImplementNodeCallback = (context, node) => {
    var method = node as OpcMethodNode;
 
    if (method != null && method.Name.Value == "TransferToDevice")
        method.Command = new TransferToDeviceCommand();
};
...
public sealed class TransferToDeviceCommand : IOpcMethodCommand
{
    public bool CanExecute(OpcContext context)
    {
        return true;
    }
 
    public bool CanUserExecute(OpcContext context)
    {
        return true;
    }
 
    public void Execute(
            OpcMethodContext context,
            IList<object> inputArguments,
            IList<object> outputArguments)
    {
        // Order and types of the arguments as defined by the specification.
        outputArguments[0] = 1; // TransferID
        outputArguments[1] = 0; // InitTransferStatus
    }
}

```

```vbnet
nodeSets.ImplementNodeCallback = Sub(context, node)
    Dim method = TryCast(node, OpcMethodNode)

    If method IsNot Nothing AndAlso method.Name.Value = "TransferToDevice" Then
        method.Command = New TransferToDeviceCommand()
    End If
End Sub
...
Public NotInheritable Class TransferToDeviceCommand
    Implements IOpcMethodCommand

    Public Function CanExecute(ByVal context As OpcContext) As Boolean _
            Implements IOpcMethodCommand.CanExecute
        Return True
    End Function

    Public Function CanUserExecute(ByVal context As OpcContext) As Boolean _
            Implements IOpcMethodCommand.CanUserExecute
        Return True
    End Function

    Public Sub Execute( _
            ByVal context As OpcMethodContext, _
            ByVal inputArguments As IList(Of Object), _
            ByVal outputArguments As IList(Of Object)) _
            Implements IOpcMethodCommand.Execute
        'Order and types of the arguments as defined by the specification.
        outputArguments(0) = 1 'TransferID
        outputArguments(1) = 0 'InitTransferStatus
    End Sub
End Class
```

With a Companion SDK, you only override the typed method of the node class, here using the `TransferToDevice` method from OPC UA for Devices (DI) as an example:

```csharp
public class DeviceTransferServices : OpcTransferServicesNode
{
    public DeviceTransferServices(IOpcNode parent)
        : base(parent, "TransferServices")
    {
    }
 
    public override void TransferToDevice(
            OpcMethodContext methodContext,
            out int transferID,
            out int initTransferStatus)
    {
        transferID = 1;
        initTransferStatus = 0;
    }
}
```

```vbnet
Public Class DeviceTransferServices
    Inherits OpcTransferServicesNode

    Public Sub New(ByVal parent As IOpcNode)
        MyBase.New(parent, "TransferServices")
    End Sub

    Public Overrides Sub TransferToDevice( _
            ByVal methodContext As OpcMethodContext, _
            ByRef transferID As Integer, _
            ByRef initTransferStatus As Integer)
        transferID = 1
        initTransferStatus = 0
    End Sub
End Class

```

### What the Companion SDK takes care of

| Task | Without Companion SDK | With Companion SDK |
|---|---|---|
| NodeSets and dependencies | obtain the files, ship them, and load them in the correct order | embedded; the server class includes all dependencies |
| Namespaces | know the URIs and resolve each namespace with `OpcNamespace.Resolve` | `OpcMachineryNodes.Namespace`, `GetName`, and constants for every `OpcNodeId` |
| Instance of a type | create the node, set `TypeDefinitionId`, and create each child node yourself with name, namespace, and data type | `new OpcMachineIdentificationNode(parent, name)` creates the child nodes |
| Optional child nodes | decide on and create each one individually | all by default; specific selection through an `OpcNodeModel` |
| Interfaces | add HasInterface references by hand | automatically from the implemented .NET interfaces |
| Values | find child nodes by name or `OpcNodeId` and set the value | typed properties, for example `Manufacturer.Value` |
| Methods | look up method nodes, `IOpcMethodCommand` with untyped arguments | override the typed method |
| Attributes from the specification (description, access rights, default values) | copy them by hand | through `OpcNodeModel.Of` from the type declaration |
| Client | resolve namespaces and register data types yourself | the client class registers the data types and resolves the namespaces |

### Companion SDK in the client

A client also benefits from a Companion SDK. The client class (here `OpcMachineryClient`) registers the data types of the specification. The client then reads structures and enumerations of the specification directly as .NET types. After connecting, the client class also resolves the namespaces. The constants from `OpcMachineryNodes` then also apply to the connected server:

```csharp
using (var client = new OpcMachineryClient("opc.tcp://localhost:4840/")) {
    client.Connect();
 
    OpcNodeInfo machines = client.BrowseNode(OpcMachineryNodes.MachinesNode);
 
    foreach (OpcNodeInfo machine in machines.Children())
        Console.WriteLine(machine.DisplayName.Value);
}

```

```vbnet
Using client = New OpcMachineryClient("opc.tcp://localhost:4840/")
    client.Connect()

    Dim machines As OpcNodeInfo = client.BrowseNode(OpcMachineryNodes.MachinesNode)

    For Each machine As OpcNodeInfo In machines.Children()
        Console.WriteLine(machine.DisplayName.Value)
    Next
End Using
```

Typed node classes such as `OpcMachineIdentificationNode` are nodes of the server. A client works with the nodes of a companion server through the usual methods for browsing, reading, and writing.
