# NodeSets

> Anleitung: Informationsmodelle als NodeSet in den Server importieren und deren Nodes implementieren.

**Beispielanwendungen:**

- 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)): Importiert eigene Node-Typen aus einem NodeSet und stellt Instanzen davon über eigene Node-Klassen bereit.
- 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)): Importiert das NodeSet der PackML-Companion-Spezifikation und implementiert dessen Objekte und Methoden für mehrere Maschinen.
- 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)): Importiert die NodeSets der Companion-Spezifikationen für Werkzeugmaschinen (umati) und meldet Alarme, die ein Client bestätigen kann.
- 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)): Importiert das NodeSet einer SIMATIC S7-1500 und tritt wie deren Server auf, etwa um Clients ohne die Hardware zu testen.

NodeSets beschreiben den Inhalt des Adressraums eines Servers in XML (eXtensible Markup Language). Die Beschreibung definiert Datentypen mit ihrer transportierten logischen Struktur (datenabhängige Informationen) und ihrer physischen Struktur (im Speicher). Außerdem enthält ein NodeSet die Definitionen von Node-Typen und von konkreten Node-Instanzen. Das Stammelement `UANodeSet` beschreibt zudem die Beziehungen (engl. References) zwischen den Nodes. Das betrifft nicht nur die Nodes aus diesem NodeSet, sondern auch Nodes aus anderen NodeSets und aus der Spezifikation.

Companion-Spezifikationen erweitern nicht nur die allgemeine Definition des Adressraums, sondern liefern unter anderem auch eigene Datentypen und Node-Typen. Zu jeder Companion-Spezifikation gibt es deshalb ein oder mehrere NodeSets. Damit ein Server eine Companion-Spezifikation erfüllt, muss er die dort definierten Typen und Verhaltensweisen umsetzen. Was dabei ein Companion SDK übernimmt, zeigt der Abschnitt [Companion-Spezifikationen](https://www.traeger.de/docs/development/opcua/server/nodesets#companion-spezifikationen).

NodeSets beschreiben den Adressraum auch bei der Projektierung von Steuerungen. So lässt sich die gesamte projektierte Konfiguration einer Steuerung aus einer Projektierungssoftware wie TIA Portal exportieren. Mit diesem Export lässt sich der OPC UA Server der Steuerung initialisieren.

Unabhängig von der Quelle gilt: Wenn ein Server ein NodeSet verwenden soll, muss er die Logik bereitstellen, um den darin beschriebenen Adressraum zu importieren und zu implementieren. Wie das funktioniert, zeigen die nächsten Abschnitte.

## NodeSets importieren

Beteiligte Typen: `OpcNodeSet`, `OpcNodeSetManager`, `OpcNodeManager`, `OpcNodeSetImportOptions`

Mit dem `OpcNodeSetManager` importieren Sie die Nodes, die ein NodeSet beschreibt, 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

```

Beim Aufruf von `OpcNodeSetManager.Create` können Sie ein oder mehrere NodeSets angeben. Der dabei erstellte `OpcNodeManager` importiert die NodeSets. Beim Start des Servers erzeugt er die darin definierten Nodes im Adressraum des Servers. Neben einem Dateipfad nimmt die Methode `OpcNodeSet.Load` auch einen `Stream` entgegen, zum Beispiel für ein NodeSet, das als Ressource in die Anwendung eingebettet ist. Mit den Methoden `GetNodes` und `GetNode` werten Sie die im NodeSet beschriebenen Nodes zudem aus:

```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
```

Wie der Node-Manager die importierten Nodes anlegt, steuert die Eigenschaft `ImportOptions` des `OpcNodeSetManager` (standardmäßig `OpcNodeSetImportOptions.None`):

- `CompleteDataTypeDefinitionFields`: ergänzt die DataTypeDefinition eines Strukturtyps um die Felder seiner Basistypen.
- `CompleteDataTypeDefinitionDefaultEncoding`: ergänzt eine fehlende `DefaultEncodingId` in der DataTypeDefinition.
- `CreateNonGenericNodeInstances`: erzeugt für die importierten Nodes die typisierten Klassen des OPC UA .NET SDK statt generischer Instanzen.

Die beiden ersten Optionen wirken auf alle NodeSets des Servers:

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

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

Wenn stattdessen ein benutzerdefinierter Node-Manager ein NodeSet importieren soll, überschreiben Sie die Methode `ImportNodes` der Klasse `OpcNodeManager`:

```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

```

## Importierte NodeSets mit Logik verbinden

Beteiligte Typen: `OpcNodeManager`, `IOpcNode`

Häufig genügt der Import eines NodeSets allein nicht, weil noch die Logik fehlt, die das zugrunde liegende System anbindet. Diese Logik bildet zum Beispiel das Lesen eines Nodes auf das Lesen eines Wortes in einem Datenbaustein ab.
Gleiches gilt für das Schreiben eines Nodes.

Wenn Sie den Node-Manager über `OpcNodeSetManager.Create` erzeugen, legen Sie die Logik über die Eigenschaft `ImplementNodeCallback` des `OpcNodeSetManager` fest. Das SDK ruft den Callback für jeden importierten Node auf. Für ein UMATI-NodeSet könnte die Logik zur Simulation des Status einer Lampe zum Beispiel so aussehen:

```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 einem benutzerdefinierten Node-Manager überschreiben Sie für diese Logik stattdessen die Methode `OpcNodeManager.ImplementNode`. Das SDK ruft die Methode für jeden importierten oder über `CreateNodes` erzeugten Node auf. Nach dem Start des Servers ruft es sie zudem für jeden Node auf, den ein Client dem Adressraum des Node-Managers hinzufügt. Rufen Sie in der Methode die Basisimplementierung auf. Sie ergänzt unter anderem die Verweise auf die Schnittstellen (HasInterface), die ein Node implementiert:

```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

```

Für das oben gezeigte UMATI-NodeSet sieht die Implementierung in einem benutzerdefinierten Node-Manager so aus:

```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-Spezifikationen

Beteiligte Typen: `OpcServer`, `OpcNodeManager`, `OpcNodeSetManager`, `OpcNamespace`, `OpcObjectNode`, `OpcNodeModel`

Eine Companion-Spezifikation lässt sich wie jedes andere NodeSet importieren und implementieren. Für viele Companion-Spezifikationen gibt es zusätzlich ein eigenes Paket des SDK (Companion SDK), zum Beispiel `Opc.UaFx.Advanced.Machinery` für OPC UA for Machinery (OPC 40001-1) oder `Opc.UaFx.Advanced.ISA95.JobControl` für ISA-95 Job Control (OPC 10031-4). Welche Companion SDKs verfügbar sind, zeigt die Produktseite [OPC UA Companion SDKs](https://www.traeger.de/products/development/opcua/companions). Die folgenden Abschnitte zeigen am Beispiel von OPC UA for Machinery, was ein solches Paket mitbringt und welche Schritte ohne Paket von Hand notwendig sind.

### Was ein Companion SDK mitbringt

| Bestandteil | Inhalt am Beispiel `Opc.UaFx.Advanced.Machinery` |
|---|---|
| Eingebettetes NodeSet | Das NodeSet der Spezifikation ist in die Assembly eingebettet; es muss weder beschafft noch mit ausgeliefert werden. |
| Abhängigkeiten | Die Server-Klasse `OpcMachineryServer` bringt die Node-Manager aller benötigten Spezifikationen mit (DI, IA und Machinery). |
| Konstanten | `OpcMachineryNodes` enthält die Namespace-URI, den Namespace (`Namespace`, `GetName`) sowie die `OpcNodeId` aller Typen und Instanzen der Spezifikation, zum Beispiel MachineIdentificationType oder `MachinesNode`. |
| Typisierte Node-Klassen | Zu jedem Objekttyp eine Klasse, zum Beispiel `OpcMachineIdentificationNode`, mit einer Eigenschaft je Kind-Node (`Manufacturer`, SerialNumber, YearOfConstruction und so weiter). Beim Erzeugen legt die Klasse die Kind-Nodes mit Namen, Namespace und Datentyp aus der Spezifikation an. |
| Schnittstellen | Die Schnittstellen der Spezifikation sind als .NET-Schnittstellen umgesetzt. Die zugehörigen HasInterface-Verweise legt das SDK beim Implementieren der Nodes automatisch an. |
| Typisierte Methoden | Methoden der Spezifikation sind als überschreibbare Methoden mit typisierten Parametern umgesetzt. Ohne Implementierung lösen sie eine `NotImplementedException` aus. |
| Node-Manager | `OpcMachineryNodeManager` importiert das eingebettete NodeSet, löst den Namespace auf und stellt den Ordner `Machines` bereit. |
| Client-Klasse | `OpcMachineryClient` registriert die Datentypen der Spezifikation und ihrer Abhängigkeiten und löst nach dem Verbinden die Namespaces auf, sodass die Konstanten aus `OpcMachineryNodes` auch im Client gelten. |

### Maschine mit Companion SDK

Das folgende Beispiel stellt eine Maschine mit ihrer Identifikation nach OPC UA for Machinery bereit. Die Kind-Nodes der Identifikation legt die Klasse `OpcMachineIdentificationNode` selbst an. Sie setzen nur noch die Werte:

```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
```

Ohne weitere Angabe legt eine Node-Klasse eines Companion SDK alle Kind-Nodes ihres Typs an, auch die optionalen. Welche optionalen Kind-Nodes entstehen, steuert ein `OpcNodeModel`. Das beschreibt die Seite [Node-Modelle](https://www.traeger.de/docs/development/opcua/server/node-models).

### Maschine ohne Companion SDK

Ohne Companion SDK sind für denselben Server diese Schritte notwendig:

1. Die NodeSet-Dateien der Spezifikation und aller Abhängigkeiten beschaffen, mit der Anwendung ausliefern und in der richtigen Reihenfolge laden (DI, IA, Machinery).
2. Die Namespaces der Spezifikationen über ihre URI auflösen, um die `OpcNodeId` und `OpcName` der Typen und Instanzen zu bilden.
3. Die Instanz anlegen und ihre Typdefinition (`TypeDefinitionId`) setzen.
4. Jeden Kind-Node mit dem Namen, dem Namespace und dem Datentyp aus der Spezifikation selbst anlegen.
5. Die Verweise auf die Schnittstellen des Typs (HasInterface) von Hand eintragen.

```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

```

Die Bezeichner der Nodes (zum Beispiel `i=1001` für den Ordner `Machines` oder `i=1012` für MachineIdentificationType) stammen aus dem NodeSet der Spezifikation. Für den Verweistyp HasInterface bietet das SDK keine Konstante. Das Beispiel verwendet deshalb dessen `OpcNodeId` (`i=17603`).

### Methoden implementieren

Auch bei Methoden der Spezifikation unterscheidet sich der Aufwand. Ohne Companion SDK suchen Sie den importierten Methoden-Node über seinen Namen und implementieren ihn über einen Befehl (`IOpcMethodCommand`). Reihenfolge und Typen der Argumente entnehmen Sie der Spezifikation. Die Argumente kommen als untypisierte Listen an:

```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
```

Mit Companion SDK genügt es, die typisierte Methode der Node-Klasse zu überschreiben, hier am Beispiel der Methode `TransferToDevice` aus OPC UA for Devices (DI):

```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

```

### Was das Companion SDK übernimmt

| Aufgabe | ohne Companion SDK | mit Companion SDK |
|---|---|---|
| NodeSets und Abhängigkeiten | Dateien beschaffen, ausliefern und in der richtigen Reihenfolge laden | eingebettet; die Server-Klasse bringt alle Abhängigkeiten mit |
| Namespaces | URIs kennen und je Namespace mit `OpcNamespace.Resolve` auflösen | `OpcMachineryNodes.Namespace`, `GetName` und Konstanten für alle `OpcNodeId` |
| Instanz eines Typs | Node anlegen, `TypeDefinitionId` setzen, jeden Kind-Node mit Name, Namespace und Datentyp selbst anlegen | `new OpcMachineIdentificationNode(parent, name)` legt die Kind-Nodes an |
| Optionale Kind-Nodes | einzeln entscheiden und anlegen | standardmäßig alle; gezielte Auswahl über ein `OpcNodeModel` |
| Schnittstellen | HasInterface-Verweise von Hand eintragen | automatisch aus den implementierten .NET-Schnittstellen |
| Werte | Kind-Nodes über Name oder `OpcNodeId` finden und den Wert setzen | typisierte Eigenschaften, zum Beispiel `Manufacturer.Value` |
| Methoden | Methoden-Nodes suchen, `IOpcMethodCommand` mit untypisierten Argumenten | typisierte Methode überschreiben |
| Attribute aus der Spezifikation (Beschreibung, Zugriffsrechte, Standardwerte) | von Hand übernehmen | über `OpcNodeModel.Of` aus der Typdeklaration |
| Client | Namespaces auflösen und Datentypen selbst registrieren | die Client-Klasse registriert die Datentypen und löst die Namespaces auf |

### Companion SDK im Client

Auch ein Client profitiert von einem Companion SDK. Die Client-Klasse (hier `OpcMachineryClient`) registriert die Datentypen der Spezifikation. Damit liest der Client Strukturen und Enumerationen der Spezifikation direkt als .NET-Typen. Nach dem Verbinden löst die Client-Klasse außerdem die Namespaces auf. Die Konstanten aus `OpcMachineryNodes` gelten danach auch für den verbundenen 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
```

Typisierte Node-Klassen wie `OpcMachineIdentificationNode` sind Nodes des Servers. Ein Client arbeitet mit den Nodes eines Companion-Servers über die üblichen Methoden zum Browsen, Lesen und Schreiben.
