# Read and write values

> How-to guide: Provide the values of nodes in the server and handle read and write access from clients.

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

**Sample applications:**

- Server ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server)): This sample demonstrates how to implement a primitive OPC UA server.
- Client ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Client), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Client)): This sample demonstrates how to implement a primitive OPC UA client.

## Read values

Types used: `OpcServer`, `OpcVariableNode`, `OpcDataVariableNode`, `OpcReadAttributeValueCallback`, `OpcAttributeValue`, `OpcReadAttributeValueContext`, `OpcReadVariableValueCallback`, `OpcReadVariableValueContext`, `OpcVariableValue`, `OpcText`

An `OpcNode` defines its metadata through attributes. In general, all nodes provide attributes such as Name, DisplayName, or Description, while only variable nodes provide the Value attribute. By default, the node instances store the attribute values internally. If the value should come from another data source, define callback methods that provide the values. The signature of the ReadVariableValue callback method differs from the other ReadAttributeValue callback methods. For the Value attribute, the SDK expects an instance of `OpcVariableValue` instead of an instance of `OpcAttributeValue`. Besides the value itself, it contains a timestamp that states when the source determined the value (`SourceTimestamp`) and status information about the quality of the value. The server calls the read callbacks on every read operation in which a client reads the metadata through the Read service. When a client browses, however, the server returns the internally stored values (for example the DisplayName) without calling the callbacks. For the Description attribute, the value is of type `OpcText`.

**Set the initial value of the Value attribute of a variable node**:

```csharp
var machineIsRunningNode = new OpcDataVariableNode<bool>("IsRunning", false);
```

```vbnet
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)("IsRunning", False)
```

**Set the value of the Value attribute of a variable node**:

```csharp
machineIsRunningNode.Value = true;
```

```vbnet
machineIsRunningNode.Value = True
```

**Set the value of the Description attribute**:

```csharp
machineIsRunningNode.Description = "My description";
```

```vbnet
machineIsRunningNode.Description = "My description"
```

**Notify all clients with an active subscription about the attribute changes and apply the changes**:

```csharp
machineIsRunningNode.ApplyChanges(server.SystemContext);
```

```vbnet
machineIsRunningNode.ApplyChanges(server.SystemContext)
```

**Determine the value of the Description attribute from a data source other than the internal one**:

```csharp
machineIsRunningNode.ReadDescriptionCallback = HandleReadDescription;
...
private OpcAttributeValue<OpcText> HandleReadDescription(
        OpcReadAttributeValueContext context,
        OpcAttributeValue<OpcText> value)
{
    return ReadDescriptionFromDataSource(context.Node) ?? value;
}
```

```vbnet
machineIsRunningNode.ReadDescriptionCallback = AddressOf HandleReadDescription
...
Private Function HandleReadDescription( _
        ByVal context As OpcReadAttributeValueContext, _
        ByVal value As OpcAttributeValue(Of OpcText)) As OpcAttributeValue(Of OpcText)
    Return If(ReadDescriptionFromDataSource(context.Node), value)
End Function

```

**Determine the value of the Value attribute of a variable node from a data source other than the internal one**:

```csharp
machineIsRunningNode.ReadVariableValueCallback = HandleReadVariableValue;
...
private OpcVariableValue<object> HandleReadVariableValue(
        OpcReadVariableValueContext context,
        OpcVariableValue<object> value)
{
    return ReadValueFromDataSource(context.Node) ?? value;
}

```

```vbnet
machineIsRunningNode.ReadVariableValueCallback = AddressOf HandleReadVariableValue
...
Private Function HandleReadVariableValue( _
        ByVal context As OpcReadVariableValueContext, _
        ByVal value As OpcVariableValue(Of Object)) As OpcVariableValue(Of Object)
    Return If(ReadValueFromDataSource(context.Node), value)
End Function

```

## Write values

Types used: `OpcServer`, `OpcVariableNode`, `OpcDataVariableNode`, `OpcWriteAttributeValueCallback`, `OpcAttributeValue`, `OpcWriteAttributeValueContext`, `OpcWriteVariableValueCallback`, `OpcWriteVariableValueContext`, `OpcVariableValue`, `OpcText`

An `OpcNode` defines its metadata through attributes. In general, all nodes provide attributes such as Name, DisplayName, or Description, while only variable nodes provide the Value attribute. By default, the node instances store the attribute values internally. If the value should be stored in another data source, define callback methods that store the values. The signature of the WriteVariableValue callback method differs from the other WriteAttributeValue callback methods. For the Value attribute, the SDK uses an instance of `OpcVariableValue` instead of an instance of `OpcAttributeValue`. Besides the value itself, it contains a timestamp that states when the source determined the value (`SourceTimestamp`) and status information about the quality of the value. The server calls the write callbacks on every write operation in which a client writes metadata. This happens through the Write service.

**Set the initial value of the Value attribute of a variable node**:

```csharp
var machineIsRunningNode = new OpcDataVariableNode<bool>("IsRunning", false);
```

```vbnet
Dim machineIsRunningNode = New OpcDataVariableNode(Of Boolean)("IsRunning", False)
```

**Set the value of the Value attribute of a variable node**:

```csharp
machineIsRunningNode.Value = true;
```

```vbnet
machineIsRunningNode.Value = True
```

**Set the value of the Description attribute**:

```csharp
machineIsRunningNode.Description = "My description";
```

```vbnet
machineIsRunningNode.Description = "My description"
```

**Notify all clients with an active subscription about the attribute changes and apply the changes**:

```csharp
machineIsRunningNode.ApplyChanges(server.SystemContext);
```

```vbnet
machineIsRunningNode.ApplyChanges(server.SystemContext)
```

**Store the value of the Description attribute in a data source other than the internal one**:

```csharp
machineIsRunningNode.WriteDescriptionCallback = HandleWriteDescription;
...
private OpcAttributeValue<OpcText> HandleWriteDescription(
        OpcWriteAttributeValueContext context,
        OpcAttributeValue<OpcText> value)
{
    return WriteDescriptionToDataSource(context.Node, value) ?? value;
}
```

```vbnet
machineIsRunningNode.WriteDescriptionCallback = AddressOf HandleWriteDescription
...
Private Function HandleWriteDescription( _
        ByVal context As OpcWriteAttributeValueContext, _
        ByVal value As OpcAttributeValue(Of OpcText)) As OpcAttributeValue(Of OpcText)
    Return If(WriteDescriptionToDataSource(context.Node, value), value)
End Function

```

**Store the value of the Value attribute of a variable node in a data source other than the internal one**:

```csharp
machineIsRunningNode.WriteVariableValueCallback = HandleWriteVariableValue;
...
private OpcVariableValue<object> HandleWriteVariableValue(
        OpcWriteVariableValueContext context,
        OpcVariableValue<object> value)
{
    return WriteValueToDataSource(context.Node, value) ?? value;
}
```

```vbnet
machineIsRunningNode.WriteVariableValueCallback = AddressOf HandleWriteVariableValue
...
Private Function HandleWriteVariableValue( _
        ByVal context As OpcWriteVariableValueContext, _
        ByVal value As OpcVariableValue(Of Object)) As OpcVariableValue(Of Object)
    Return If(WriteValueToDataSource(context.Node, value), value)
End Function

```

## Sample values only on demand

Types used: `OpcNodeManager`, `OpcMonitoredItem`, `OpcMonitoredItemEventArgs`, `OpcAttribute`

If the values come from a PLC or another field device, polling all variables cyclically is often unnecessary. Through the `MonitoredItemCreated`, `MonitoredItemModified`, and `MonitoredItemDeleted` events, the node manager learns which of its nodes clients are currently subscribed to. The event data contains the `OpcMonitoredItem` with the NodeId, `Attribute`, `SamplingInterval`, and the `Session` of the client, as well as the affected node (`Node`). For requests that create or remove several MonitoredItems at once, the `MonitoredItemsCreated`, `MonitoredItemsModified`, and `MonitoredItemsDeleted` events are also available.

Several clients can subscribe to the same node, and one client can subscribe to it more than once. The following example therefore counts the subscriptions per node. It uses the attribute to exclude subscriptions to events:

```csharp
public class MachineNodeManager : OpcNodeManager
{
    private readonly Dictionary<OpcNodeId, int> monitored
            = new Dictionary<OpcNodeId, int>();
 
    public MachineNodeManager()
        : base("http://example.com/machine")
    {
        this.MonitoredItemCreated += this.HandleMonitoredItemCreated;
        this.MonitoredItemDeleted += this.HandleMonitoredItemDeleted;
    }
 
    public bool IsMonitored(OpcNodeId nodeId)
    {
        lock (this.monitored)
            return this.monitored.ContainsKey(nodeId);
    }
 
    private void HandleMonitoredItemCreated(
            object sender,
            OpcMonitoredItemEventArgs e)
    {
        if (e.Item.Attribute != OpcAttribute.Value)
            return;
 
        lock (this.monitored) {
            int count;
            this.monitored.TryGetValue(e.Item.NodeId, out count);
            this.monitored[e.Item.NodeId] = count + 1;
        }
    }
 
    private void HandleMonitoredItemDeleted(
            object sender,
            OpcMonitoredItemEventArgs e)
    {
        if (e.Item.Attribute != OpcAttribute.Value)
            return;
 
        lock (this.monitored) {
            int count;
 
            if (!this.monitored.TryGetValue(e.Item.NodeId, out count))
                return;
 
            if (count > 1)
                this.monitored[e.Item.NodeId] = count - 1;
            else
                this.monitored.Remove(e.Item.NodeId);
        }
    }
}

```

```vbnet
Public Class MachineNodeManager
    Inherits OpcNodeManager

    Private ReadOnly monitored As New Dictionary(Of OpcNodeId, Integer)()

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

        AddHandler Me.MonitoredItemCreated, AddressOf Me.HandleMonitoredItemCreated
        AddHandler Me.MonitoredItemDeleted, AddressOf Me.HandleMonitoredItemDeleted
    End Sub

    Public Function IsMonitored(ByVal nodeId As OpcNodeId) As Boolean
        SyncLock Me.monitored
            Return Me.monitored.ContainsKey(nodeId)
        End SyncLock
    End Function

    Private Sub HandleMonitoredItemCreated( _
            ByVal sender As Object, _
            ByVal e As OpcMonitoredItemEventArgs)
        If e.Item.Attribute <> OpcAttribute.Value Then
            Return
        End If

        SyncLock Me.monitored
            Dim count As Integer
            Me.monitored.TryGetValue(e.Item.NodeId, count)
            Me.monitored(e.Item.NodeId) = count + 1
        End SyncLock
    End Sub

    Private Sub HandleMonitoredItemDeleted( _
            ByVal sender As Object, _
            ByVal e As OpcMonitoredItemEventArgs)
        If e.Item.Attribute <> OpcAttribute.Value Then
            Return
        End If

        SyncLock Me.monitored
            Dim count As Integer

            If Not Me.monitored.TryGetValue(e.Item.NodeId, count) Then
                Return
            End If

            If count > 1 Then
                Me.monitored(e.Item.NodeId) = count - 1
            Else
                Me.monitored.Remove(e.Item.NodeId)
            End If
        End SyncLock
    End Sub
End Class
```

The queries to the data source are then limited to the nodes for which `IsMonitored` returns `true`. The [Provide data from a device](https://www.traeger.de/en/docs/development/opcua/server/device-gateway) tutorial shows a complete example.

## Process access in batches

Types used: `OpcNodeManager`, `OpcNodeAccessEventArgs`, `OpcNodeAccessToken`, `OpcAttribute`, `OpcResult`

The server calls callbacks such as the `ReadVariableValueCallback` per node. If a client reads 500 variables in one request, this results in 500 individual calls to the data source. For such cases, the node manager provides the `BeforeRead`, `AfterRead`, `BeforeWrite`, and `AfterWrite` events. They occur once per request, and their `Tokens` contain all accesses of the request to nodes of this node manager. Each `OpcNodeAccessToken` names the node (`Node`), the attribute (`Attribute`), an optional range (`Range`), the value (`Value`), and the result of the access (`Result`).

- `BeforeRead` occurs before the server reads the attributes. The server returns values that the handler writes to the nodes here in the same request.
- `AfterRead` occurs after the server has read all attributes.
- `BeforeWrite` occurs before the server writes the values to the nodes.
- `AfterWrite` occurs after the server has written the values. `Result` then states for each access whether the write succeeded.

In the following example, `Device` stands for the connection to the data source. Before the read, the node manager reads all requested values in one call, and after the write it passes on all successfully written values in one call:

```csharp
public class DeviceNodeManager : OpcNodeManager
{
    public DeviceNodeManager()
        : base("http://example.com/device")
    {
        this.BeforeRead += this.HandleBeforeRead;
        this.AfterWrite += this.HandleAfterWrite;
    }
 
    private void HandleBeforeRead(object sender, OpcNodeAccessEventArgs e)
    {
        var nodes = new List<OpcDataVariableNode>();
 
        foreach (var token in e.Tokens) {
            var node = token.Node as OpcDataVariableNode;
 
            if (token.Attribute == OpcAttribute.Value && node != null && !nodes.Contains(node))
                nodes.Add(node);
        }
 
        var names = new string[nodes.Count];
 
        for (int index = 0; index < nodes.Count; index++)
            names[index] = nodes[index].SymbolicName;
 
        // Device stands for the connection to the data source:
        // one request for all nodes of the client request.
        object[] values = Device.Read(names);
 
        for (int index = 0; index < nodes.Count; index++)
            nodes[index].Value = values[index];
    }
 
    private void HandleAfterWrite(object sender, OpcNodeAccessEventArgs e)
    {
        var names = new List<string>();
        var values = new List<object>();
 
        foreach (var token in e.Tokens) {
            if (token.Attribute == OpcAttribute.Value && token.Result.IsGood) {
                names.Add(token.Node.SymbolicName);
                values.Add(token.Value.Value);
            }
        }
 
        // One request to the device for all values written by the client.
        Device.Write(names.ToArray(), values.ToArray());
    }
}

```

```vbnet
Public Class DeviceNodeManager
    Inherits OpcNodeManager

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

        AddHandler Me.BeforeRead, AddressOf Me.HandleBeforeRead
        AddHandler Me.AfterWrite, AddressOf Me.HandleAfterWrite
    End Sub

    Private Sub HandleBeforeRead(ByVal sender As Object, ByVal e As OpcNodeAccessEventArgs)
        Dim nodes = New List(Of OpcDataVariableNode)()

        For Each token In e.Tokens
            Dim node = TryCast(token.Node, OpcDataVariableNode)

            If token.Attribute = OpcAttribute.Value AndAlso node IsNot Nothing _
                    AndAlso Not nodes.Contains(node) Then
                nodes.Add(node)
            End If
        Next

        Dim names = New String(nodes.Count - 1) {}

        For index As Integer = 0 To nodes.Count - 1
            names(index) = nodes(index).SymbolicName
        Next

        'Device stands for the connection to the data source:
        'one request for all nodes of the client request.
        Dim values As Object() = Device.Read(names)

        For index As Integer = 0 To nodes.Count - 1
            nodes(index).Value = values(index)
        Next
    End Sub

    Private Sub HandleAfterWrite(ByVal sender As Object, ByVal e As OpcNodeAccessEventArgs)
        Dim names = New List(Of String)()
        Dim values = New List(Of Object)()

        For Each token In e.Tokens
            If token.Attribute = OpcAttribute.Value AndAlso token.Result.IsGood Then
                names.Add(token.Node.SymbolicName)
                values.Add(token.Value.Value)
            End If
        Next

        'One request to the device for all values written by the client.
        Device.Write(names.ToArray(), values.ToArray())
    End Sub
End Class
```

The events apply to all attributes. The example therefore filters for the Value attribute.
