# Historical data

> How-to guide: Store historical values of nodes and provide them to clients, with your own history provider or the OpcNodeHistorian.

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

**Sample applications:**

- Server.HDA ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server.HDA), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server.HDA)): This sample demonstrates how to setup an OPC UA server to provide HDA to its nodes provided.
- Server.HDA.Sql ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Server.HDA.Sql), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Server.HDA.Sql)): This sample demonstrates how to setup an OPC UA server to provide HDA via SQL to its nodes provided.
- Client.HDA ([C#](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/cs/Basic/Client.HDA), [VB.NET](https://github.com/Traeger-GmbH/opcuanet-samples/tree/master/vb/Basic/Client.HDA)): This sample demonstrates how to perform history data access (HDA) on the data provided by the OPC UA server.

Types used: `OpcServer`, `OpcNodeManager`, `IOpcNode`, `OpcHistoryValue`, `OpcHistoryModificationInfo`, `OpcValueCollection`, `OpcStatusCollection`, `OpcDeleteHistoryOptions`, `OpcReadHistoryOptions`, `IOpcNodeHistoryProvider`, `OpcNodeHistorian`, `OpcNodeHistory&lt;T&gt;`, `OpcModifiedNodeHistory`, `IOpcEventHistoryProvider`

According to the OPC UA specification, every node of the Variable category can record the values of its Value attribute over time. Each time the Value attribute changes, the server stores the new value together with the timestamp of the Value attribute. These pairs of value and timestamp are called **historical data**. The server decides where it stores the data. The client uses the `IsHistorizing` attribute of a node to find out whether the server provides historical data for that node, that is, whether it stores value changes in a history. A client can read, update, replace, delete, or create historical data. Reading is the most common case.

The server manages historical data either directly in the node manager of the node, through the node historian in memory, or through a custom historian. In OPC UA, historical values always use their timestamp as the key. A timestamp is therefore unique among all historical values of a node. It identifies exactly one value and its quality information, that is, its status information. Historical data stored this way is also divided into plain and modified historical data. From a database point of view, the modified data is a kind of changelog. With this changelog, you can process historical data that was valid before the original historical data was changed. It also makes every change to the historical data traceable. For example, if a historical value is replaced, the server stores the previous value in the modified history. The server also stores a historical value that is removed from the history in the modified history. In addition, the modified history stores the kind of change, the timestamp of the change, and the name of the user who made the change.

## Enable recording

For a client to read the (modified) historical data of a node, the node must be a variable node. In addition, recording of historical data must be enabled and access to it must be granted:

**With `OpcNodeHistorian`**: The historian enables recording of historical data and grants access to it:

```csharp
// "this" points to the Node-Manager of the node.
var machineIsRunningHistorian = new OpcNodeHistorian(this, machineIsRunningNode);
```

```vbnet
'"Me" points to the Node-Manager of the node.
Dim machineIsRunningHistorian = New OpcNodeHistorian(Me, machineIsRunningNode)
```

**Manually**: Enable recording of historical data and grant access to it:

```csharp
machineIsRunningNode.AccessLevel |= OpcAccessLevel.HistoryReadOrWrite;
machineIsRunningNode.UserAccessLevel |= OpcAccessLevel.HistoryReadOrWrite;
 
machineIsRunningNode.IsHistorizing = true;
```

```vbnet
machineIsRunningNode.AccessLevel = machineIsRunningNode.AccessLevel Or OpcAccessLevel.HistoryReadOrWrite
machineIsRunningNode.UserAccessLevel = machineIsRunningNode.UserAccessLevel Or OpcAccessLevel.HistoryReadOrWrite

machineIsRunningNode.IsHistorizing = True
```

## Record value changes automatically

To have the server monitor changes to the Value attribute of a variable node and transfer them to a store for historical values:

**With `OpcNodeHistorian`**: Configure the historian to update the history automatically:

```csharp
machineIsRunningHistorian.AutoUpdateHistory = true;

```

```vbnet
machineIsRunningHistorian.AutoUpdateHistory = True

```

**Manually**: Subscribe to the `BeforeApplyChanges` event of the variable node to monitor changes to the Value attribute:

```csharp
machineIsRunningNode.BeforeApplyChanges += HandleBeforeApplyChanges;
...
private void HandleBeforeApplyChanges(object sender, EventArgs e)
{
    // Update (modified) Node History here.
}
```

```vbnet
machineIsRunningNode.BeforeApplyChanges = AddressOf HandleBeforeApplyChanges;
...
Private Sub HandleBeforeApplyChanges(ByVal sender As Object, ByVal e As EventArgs)
    'Update (modified) Node History here.
End Sub

```

## Answer read requests

To provide the historical data to the client:

**Use a history provider**: If you use an `IOpcNodeHistoryProvider` such as the `OpcNodeHistorian`, register it with the server through the node manager:

```csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function
```

**Use your own history provider**: A custom `IOpcNodeHistoryProvider` answers the request with its `ReadHistory` method. If the `timeFlowsBackward` parameter is set, the client expects the values in reverse chronological order. The `maxCount` parameter states the maximum number of values the client requests (`null` if it sets no limit). The server delivers the values to the client page by page. The method can therefore also provide them gradually (for example, with `yield return`):

```csharp
public IEnumerable<OpcHistoryValue> ReadHistory(
        OpcContext context,
        DateTime? startTime,
        DateTime? endTime,
        bool timeFlowsBackward,
        uint? maxCount,
        OpcReadHistoryOptions options)
{
    // Read (modified) Node History here.
}
```

```vbnet
Public Function ReadHistory( _
        ByVal context As OpcContext, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal timeFlowsBackward As Boolean, _
        ByVal maxCount As UInteger?, _
        ByVal options As OpcReadHistoryOptions) As IEnumerable(Of OpcHistoryValue) _
        Implements IOpcNodeHistoryProvider.ReadHistory
    'Read (modified) Node History here.
End Function
```

As of version 2.59, the server calculates values at specific times and aggregated values from the bounding values of the requested time range. Raw reads with `ReturnBounds` also return these bounding values. Therefore also return the value directly before and the value directly after the requested time range; otherwise the client receives `BadNoData`.

**Manage the history in the node manager**: If the node manager handles the history of its nodes itself, implement the `ReadHistory` method:

```csharp
protected override IEnumerable<OpcHistoryValue> ReadHistory(
        OpcContext context,
        IOpcNode node,
        DateTime? startTime,
        DateTime? endTime,
        bool timeFlowsBackward,
        uint? maxCount,
        OpcReadHistoryOptions options)
{
    // Read (modified) Node History here.
}
```

```vbnet
Protected Overrides Function ReadHistory( _
        ByVal context As OpcContext, _
        ByVal node As IOpcNode, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal timeFlowsBackward As Boolean, _
        ByVal maxCount As UInteger?, _
        ByVal options As OpcReadHistoryOptions) As IEnumerable(Of OpcHistoryValue)
    'Read (modified) Node History here.
End Function

```

## Aggregated values and values at specific times

Types used: `OpcNodeManager`, `IOpcNodeHistoryProvider`, `OpcNodeHistorian`, `OpcAggregateType`, `OpcHistoryValue`

A server that provides the raw values of a node also answers requests for aggregated values and for values at specific times, without any further code. This applies to an `IOpcNodeHistoryProvider`, the `OpcNodeHistorian`, and the `ReadHistory` method of the node manager. The OPC UA .NET SDK reads the raw values of the requested time range through the same `ReadHistory` method and calculates the result itself:

- **Aggregated values** (Processed): For each interval of the time range, the server calculates the requested aggregate, for example, average, minimum, maximum, or count (see `OpcAggregateType`). If a client requests an aggregate that the server does not support, it receives the status `BadAggregateNotSupported` for this node.
- **Values at specific times** (`AtTime`): If a raw value exists at the requested time, the server returns it. If the time lies between two raw values, the server interpolates linearly between them. After the last raw value, it returns the last value. If no raw value exists before the time, the status is `BadNoData`. If the server skipped raw values with a bad status during the calculation, it marks the result as uncertain (`UncertainDataSubNormal`).

So you only implement reading the raw values, as shown in [Answer read requests](https://www.traeger.de/en/docs/development/opcua/server/historical-data#answer-read-requests). A client then requests the calculated values like this, for example (see also [Historical data](https://www.traeger.de/en/docs/development/opcua/client/historical-data) in the client chapter):

```csharp
DateTime endTime = DateTime.UtcNow;
DateTime startTime = endTime.AddHours(-8);
 
var averagePerHour = client.ReadNodeHistoryProcessed(
        startTime,
        endTime,
        OpcAggregateType.Average,
        aggregateInterval: 3600000,
        nodeId: "ns=2;s=Machine/Temperature");
 
OpcHistoryValue atNoon = client.ReadNodeHistoryAtTime(
        time: DateTime.UtcNow.Date.AddHours(12),
        nodeId: "ns=2;s=Machine/Temperature");
```

```vbnet
Dim endTime As DateTime = DateTime.UtcNow
Dim startTime As DateTime = endTime.AddHours(-8)

Dim averagePerHour = client.ReadNodeHistoryProcessed( _
        startTime, _
        endTime, _
        OpcAggregateType.Average, _
        aggregateInterval:=3600000, _
        nodeId:="ns=2;s=Machine/Temperature")

Dim atNoon As OpcHistoryValue = client.ReadNodeHistoryAtTime( _
        time:=DateTime.UtcNow.Date.AddHours(12), _
        nodeId:="ns=2;s=Machine/Temperature")
```

## Insert values

If a client wants to create historical data of a node, the server must store the new values both in the history and in the modified history:

**Use a history provider**: If you use an `IOpcNodeHistoryProvider` such as the `OpcNodeHistorian`, register it with the server through the node manager:

```csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function
```

**Use your own history provider**: A custom `IOpcNodeHistoryProvider` uses its `CreateHistory` method for this:

```csharp
public OpcStatusCollection CreateHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Create (modified) Node History here.
}
```

```vbnet
Public Function CreateHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Create (modified) Node History here.
End Function

```

**Manage the history in the node manager**: If the node manager handles the history of its nodes itself, implement the `CreateHistory` method:

```csharp
protected override OpcStatusCollection CreateHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Create (modified) Node History here.
}
```

```vbnet
Protected Overrides Function CreateHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Create (modified) Node History here.
End Function

```

## Delete values

If a client wants to delete historical data of a node, the server must move the values to delete into the modified history and delete them from the underlying history. The server can remove modified history values that should be deleted directly from the modified history:

**Use a history provider**: If you use an `IOpcNodeHistoryProvider` such as the `OpcNodeHistorian`, register it with the server through the node manager:

```csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function
```

**Use your own history provider**: A custom `IOpcNodeHistoryProvider` uses one of its `DeleteHistory` methods for this:

```csharp
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        IEnumerable<DateTime> times)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
public OpcStatusCollection DeleteHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        DateTime? startTime,
        DateTime? endTime,
        OpcDeleteHistoryOptions options)
{
    // Delete Node History entries and add them to the modified Node History here.
}
```

```vbnet
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal times As IEnumerable(Of DateTime)) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Public Function DeleteHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal options As OpcDeleteHistoryOptions) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function

```

**Manage the history in the node manager**: If the node manager handles the history of its nodes itself, implement the `DeleteHistory` methods:

```csharp
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        IEnumerable<DateTime> times)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Delete Node History entries and add them to the modified Node History here.
}
 
protected override OpcStatusCollection DeleteHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        DateTime? startTime,
        DateTime? endTime,
        OpcDeleteHistoryOptions options)
{
    // Delete Node History entries and add them to the modified Node History here.
}
```

```vbnet
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal times As IEnumerable(Of DateTime)) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function
 
Protected Overrides Function DeleteHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal startTime As DateTime?, _
        ByVal endTime As DateTime?, _
        ByVal options As OpcDeleteHistoryOptions) As OpcStatusCollection
    'Delete Node History entries and add them to the modified Node History here.
End Function

```

## Replace values

If a client wants to replace historical data of a node, the server must move the values to replace into the modified history and replace them in the underlying history:

**Use a history provider**: If you use an `IOpcNodeHistoryProvider` such as the `OpcNodeHistorian`, register it with the server through the node manager:

```csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function
```

**Use your own history provider**: A custom `IOpcNodeHistoryProvider` uses its `ReplaceHistory` method for this:

```csharp
public OpcStatusCollection ReplaceHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Replace Node History entries and add them to the modified Node History here.
}
```

```vbnet
Public Function ReplaceHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Replace Node History entries and add them to the modified Node History here.
End Function

```

**Manage the history in the node manager**: If the node manager handles the history of its nodes itself, implement the `ReplaceHistory` method:

```csharp
protected override OpcStatusCollection ReplaceHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Replace Node History entries and add them to the modified Node History here.
}
```

```vbnet
Protected Overrides Function ReplaceHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Replace Node History entries and add them to the modified Node History here.
End Function

```

## Insert or replace values (Update)

A client can create historical data of a node if it does not exist yet, or replace it if it already exists. In OPC UA, this is called an update. The server must add entries that do not exist to the history and to the modified history. It must replace existing entries in the history and add them to the modified history:

**Use a history provider**: If you use an `IOpcNodeHistoryProvider` such as the `OpcNodeHistorian`, register it with the server through the node manager:

```csharp
protected override IOpcNodeHistoryProvider RetrieveNodeHistoryProvider(IOpcNode node)
{
    if (node == machineIsRunningNode)
        return machineIsRunningHistorian;
 
    return base.RetrieveNodeHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveNodeHistoryProvider( _
        ByVal node As IOpcNode) As IOpcNodeHistoryProvider
    If node Is machineIsRunningNode Then
        Return machineIsRunningHistorian
    End If

    Return MyBase.RetrieveNodeHistoryProvider(node)
End Function
```

**Use your own history provider**: A custom `IOpcNodeHistoryProvider` uses its `UpdateHistory` method for this:

```csharp
public OpcStatusCollection UpdateHistory(
        OpcContext context,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Update (modified) Node History entries here.
}
```

```vbnet
Public Function UpdateHistory( _
        ByVal context As OpcContext, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Update (modified) Node History entries here.
End Function

```

**Manage the history in the node manager**: If the node manager handles the history of its nodes itself, implement the `UpdateHistory` method:

```csharp
protected override OpcStatusCollection UpdateHistory(
        IOpcNode node,
        OpcHistoryModificationInfo modificationInfo,
        OpcValueCollection values)
{
    // Update (modified) Node History entries here.
}

```

```vbnet
Protected Overrides Function UpdateHistory( _
        ByVal node As IOpcNode, _
        ByVal modificationInfo As OpcHistoryModificationInfo, _
        ByVal values As OpcValueCollection) As OpcStatusCollection
    'Update (modified) Node History entries here.
End Function

```

## Manage the history in memory

The `OpcNodeHistory<T>` class manages the data of the history in memory, and up to version 2.58 also the data of the modified history. As of version 2.59, the `OpcModifiedNodeHistory` class manages the modified history. The `OpcNodeHistorian` also uses these classes. The `OpcNodeHistory<T>` class provides several methods for the common scenarios of accessing historical data. Its constructors set the size of the history, that is, its capacity; this also applies to the `OpcNodeHistorian`. By default, the capacity is -1, that is, the history is unlimited. As of version 2.59, a capacity of 0 or less than -1 throws an `ArgumentOutOfRangeException`. This also applies to the `modifiedCapacity` parameter of the `OpcNodeHistorian` constructors. You can also preload the history and monitor it through several events.

Define a history depending on the kind of historical data:

**Simple history**: Use the `OpcHistoryValue` class as the type parameter:

```csharp
var history = new OpcNodeHistory<OpcHistoryValue>();

```

```vbnet
Dim history = New OpcNodeHistory(Of OpcHistoryValue)()

```

**Modified history**: As of version 2.59, use the `OpcModifiedNodeHistory` class. It holds entries of type `OpcModifiedHistoryValue` and also accepts several modifications of a value with the same timestamp. The entries are sorted by timestamp; for the same timestamp, the newest modification comes first. When the capacity is reached, the class removes the oldest modification. The `OpcNodeHistorian.ModifiedHistory` property is of this type. Up to version 2.58, use `OpcNodeHistory<T>` with the type parameter `OpcModifiedHistoryValue`:

```csharp
var modifiedHistory = new OpcNodeHistory<OpcModifiedHistoryValue>();
```

```vbnet
Dim modifiedHistory = New OpcNodeHistory(Of OpcModifiedHistoryValue)()
```

### Scenarios with the in-memory node history

With the `OpcNodeHistory<T>` class, you implement the common history scenarios Read, Create, Delete, Replace, and Update as follows:

**Scenario: Create History**:

```csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (MatchesValueType(value)) {
        if (history.Contains(value.Timestamp)) {
            result.Update(OpcStatusCode.BadEntryExists);
        }
        else {
            history.Add(value);
 
            var modifiedValue = value.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryInserted);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
```

```vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If MatchesValueType(value) Then
        If history.Contains(value.Timestamp) Then
            result.Update(OpcStatusCode.BadEntryExists)
        Else
            history.Add(value)

            Dim modifiedValue = value.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryInserted)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

```

**Scenario: Delete History**:

**By timestamp**:

```csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, times.Count());
 
int index = 0;
 
foreach (var time in times) {
    var result = results[index++];
 
    if (this.history.Contains(time)) {
        var value = this.history[time];
        this.history.RemoveAt(time);
 
        var modifiedValue = value.CreateModified(modificationInfo);
        this.modifiedHistory.Add(modifiedValue);
    }
    else {
        result.Update(OpcStatusCode.BadNoEntryExists);
    }
}
 
return results;
```

```vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, times.Count())

Dim index As Integer = 0

For Each time In times
    Dim result = results(Math.Min(System.Threading.Interlocked.Increment(index), index - 1))

    If Me.history.Contains(time) Then
        Dim value = Me.history(time)
        Me.history.RemoveAt(time)

        Dim modifiedValue = value.CreateModified(modificationInfo)
        Me.modifiedHistory.Add(modifiedValue)
    Else
        result.Update(OpcStatusCode.BadNoEntryExists)
    End If
Next

Return results

```

**By values**:

```csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var timestamp = OpcHistoryValue.Create(values[index]).Timestamp;
    var result = results[index];
 
    if (history.Contains(timestamp)) {
        var value = history[timestamp];
        history.RemoveAt(timestamp);
 
        var modifiedValue = value.CreateModified(modificationInfo);
        modifiedHistory.Add(modifiedValue);
    }
    else {
        result.Update(OpcStatusCode.BadNoEntryExists);
    }
}
 
return results;
```

```vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim timestamp = OpcHistoryValue.Create(values(index)).Timestamp
    Dim result = results(index)

    If history.Contains(timestamp) Then
        Dim value = history(timestamp)
        history.RemoveAt(timestamp)

        Dim modifiedValue = value.CreateModified(modificationInfo)
        modifiedHistory.Add(modifiedValue)
    Else
        result.Update(OpcStatusCode.BadNoEntryExists)
    End If
Next

Return results

```

**By time range**:

```csharp
var results = new OpcStatusCollection();
 
bool isModified = (options & OpcDeleteHistoryOptions.Modified)
        == OpcDeleteHistoryOptions.Modified;
 
if (isModified) {
    modifiedHistory.RemoveRange(startTime, endTime);
}
else {
    var values = history.Enumerate(startTime, endTime).ToArray();
    history.RemoveRange(startTime, endTime);
 
    for (int index = 0; index < values.Length; index++) {
        var value = values[index];
        modifiedHistory.Add(value.CreateModified(modificationInfo));
 
        results.Add(OpcStatusCode.Good);
    }
}
 
return results;
```

```vbnet
Dim results = New OpcStatusCollection()
Dim isModified As Boolean = (options And OpcDeleteHistoryOptions.Modified) _
        = OpcDeleteHistoryOptions.Modified

If isModified Then
    modifiedHistory.RemoveRange(startTime, endTime)
Else
    Dim values = history.Enumerate(startTime, endTime).ToArray()
    history.RemoveRange(startTime, endTime)

    For index As Integer = 0 To values.Length - 1
        Dim value = values(index)
        modifiedHistory.Add(value.CreateModified(modificationInfo))

        results.Add(OpcStatusCode.Good)
    Next
End If

Return results

```

**Scenario: Replace History**:

```csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (this.MatchesNodeValueType(value)) {
        if (this.history.Contains(value.Timestamp)) {
            var oldValue = this.history[value.Timestamp];
            history.Replace(value);
 
            var modifiedValue = oldValue.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryReplaced);
        }
        else {
            result.Update(OpcStatusCode.BadNoEntryExists);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
```

```vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If Me.MatchesNodeValueType(value) Then
        If Me.history.Contains(value.Timestamp) Then
            Dim oldValue = Me.history(value.Timestamp)
            history.Replace(value)

            Dim modifiedValue = oldValue.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryReplaced)
        Else
            result.Update(OpcStatusCode.BadNoEntryExists)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

```

**Scenario: Update History**:

```csharp
var results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count);
 
for (int index = 0; index < values.Count; index++) {
    var result = results[index];
    var value = OpcHistoryValue.Create(values[index]);
 
    if (MatchesValueType(value)) {
        if (history.Contains(value.Timestamp)) {
            var oldValue = this.history[value.Timestamp];
            history.Replace(value);
 
            var modifiedValue = oldValue.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryReplaced);
        }
        else {
            history.Add(value);
 
            var modifiedValue = value.CreateModified(modificationInfo);
            modifiedHistory.Add(modifiedValue);
 
            result.Update(OpcStatusCode.GoodEntryInserted);
        }
    }
    else {
        result.Update(OpcStatusCode.BadTypeMismatch);
    }
}
 
return results;
```

```vbnet
Dim results = OpcStatusCollection.Create(OpcStatusCode.Good, values.Count)

For index As Integer = 0 To values.Count - 1
    Dim result = results(index)
    Dim value = OpcHistoryValue.Create(values(index))

    If MatchesValueType(value) Then
        If history.Contains(value.Timestamp) Then
            Dim oldValue = Me.history(value.Timestamp)
            history.Replace(value)

            Dim modifiedValue = oldValue.CreateModified(modificationInfo)
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryReplaced)
        Else
            history.Add(value)

            Dim modifiedValue = value.CreateModified(modificationInfo)            
            modifiedHistory.Add(modifiedValue)

            result.Update(OpcStatusCode.GoodEntryInserted)
        End If
    Else
        result.Update(OpcStatusCode.BadTypeMismatch)
    End If
Next

Return results

```

**Scenario: Read History**:

```csharp
bool isModified = (options & OpcReadHistoryOptions.Modified)
        == OpcReadHistoryOptions.Modified;
 
OpcHistoryValue[] values;
 
if (isModified) {
    values = modifiedHistory
            .Enumerate(startTime, endTime)
            .Cast<OpcHistoryValue>()
            .ToArray();
}
else {
    values = history
            .Enumerate(startTime, endTime)
            .ToArray();
}
 
// Return the values in reverse order in case the time flows backward.
if (timeFlowsBackward)
    Array.Reverse(values);
 
return values;
```

```vbnet
Dim isModified As Boolean = (options And OpcReadHistoryOptions.Modified) _
        = OpcReadHistoryOptions.Modified

Dim values As OpcHistoryValue()

If isModified Then
    values = modifiedHistory _
            .Enumerate(startTime, endTime) _
            .Cast(Of OpcHistoryValue)() _
            .ToArray()
Else
    values = history _
            .Enumerate(startTime, endTime) _
            .ToArray()
End If

'Return the values in reverse order in case the time flows backward.
If timeFlowsBackward Then
    Array.Reverse(values)
End If

Return values
```

## Provide the event history

Types used: `OpcNodeManager`, `OpcObjectNode`, `IOpcEventHistoryProvider`, `OpcEvent`, `OpcEventFilter`

Besides the values of a variable, a server can also provide the events of a node over time. Clients query them with a HistoryRead for events. To do this, set the `IsHistorizingEvents` property on the node that reports the events. This is usually an `OpcObjectNode` (see [Provide events on a node](https://www.traeger.de/en/docs/development/opcua/server/events#provide-events-on-a-node)). Also provide an implementation of the `IOpcEventHistoryProvider` interface. Its `ReadEventHistory` method returns the events of a time range. The `timeFlowsBackward` and `maxCount` parameters have the same meaning as when reading values. If a bound of the time range is not specified, the parameter contains `null` or `DateTime.MinValue`. From the returned events, the SDK takes only the fields the client requested (SelectClause of the filter). It does not evaluate a condition of the filter (WhereClause). A simple in-memory event history can look like this:

```csharp
public class MachineEventHistory : IOpcEventHistoryProvider
{
    private readonly List<OpcEvent> events = new List<OpcEvent>();
 
    public MachineEventHistory(OpcObjectNode node)
    {
        this.Node = node;
        this.Node.IsHistorizingEvents = true;
    }
 
    public OpcObjectNode Node { get; }
 
    public void Add(OpcEvent historyEvent)
    {
        lock (this.events)
            this.events.Add(historyEvent);
    }
 
    public IEnumerable<OpcEvent> ReadEventHistory(
            OpcContext context,
            OpcEventFilter filter,
            DateTime? startTime,
            DateTime? endTime,
            bool timeFlowsBackward,
            uint? maxCount)
    {
        // In case the time flows backward the start time is the later one.
        DateTime? from = timeFlowsBackward ? endTime : startTime;
        DateTime? to = timeFlowsBackward ? startTime : endTime;
 
        var result = new List<OpcEvent>();
 
        lock (this.events) {
            foreach (var item in this.events) {
                if (IsSet(from) && item.Time < from)
                    continue;
 
                if (IsSet(to) && item.Time > to)
                    continue;
 
                result.Add(item);
            }
        }
 
        if (timeFlowsBackward)
            result.Reverse();
 
        if (maxCount != null && result.Count > maxCount.Value)
            result.RemoveRange((int)maxCount.Value, result.Count - (int)maxCount.Value);
 
        return result;
    }
 
    private static bool IsSet(DateTime? time)
    {
        return time != null && time != DateTime.MinValue;
    }
}
```

```vbnet
Public Class MachineEventHistory
    Implements IOpcEventHistoryProvider

    Private ReadOnly events As New List(Of OpcEvent)()

    Public Sub New(ByVal node As OpcObjectNode)
        Me.Node = node
        Me.Node.IsHistorizingEvents = True
    End Sub

    Public ReadOnly Property Node As OpcObjectNode Implements IOpcEventHistoryProvider.Node

    Public Sub Add(ByVal historyEvent As OpcEvent)
        SyncLock Me.events
            Me.events.Add(historyEvent)
        End SyncLock
    End Sub

    Public Function ReadEventHistory( _
            ByVal context As OpcContext, _
            ByVal filter As OpcEventFilter, _
            ByVal startTime As DateTime?, _
            ByVal endTime As DateTime?, _
            ByVal timeFlowsBackward As Boolean, _
            ByVal maxCount As UInteger?) As IEnumerable(Of OpcEvent) _
            Implements IOpcEventHistoryProvider.ReadEventHistory
        'In case the time flows backward the start time is the later one.
        Dim from As DateTime? = If(timeFlowsBackward, endTime, startTime)
        Dim [to] As DateTime? = If(timeFlowsBackward, startTime, endTime)

        Dim result = New List(Of OpcEvent)()

        SyncLock Me.events
            For Each item In Me.events
                If IsSet(from) AndAlso item.Time < from.Value Then Continue For
                If IsSet([to]) AndAlso item.Time > [to].Value Then Continue For

                result.Add(item)
            Next
        End SyncLock

        If timeFlowsBackward Then
            result.Reverse()
        End If

        If maxCount.HasValue AndAlso result.Count > maxCount.Value Then
            result.RemoveRange(CInt(maxCount.Value), result.Count - CInt(maxCount.Value))
        End If

        Return result
    End Function

    Private Shared Function IsSet(ByVal time As DateTime?) As Boolean
        Return time.HasValue AndAlso time.Value <> DateTime.MinValue
    End Function
End Class

```

Register the event history with the server through the node manager. If the node itself implements the `IOpcEventHistoryProvider` interface, the server uses it even without this method:

```csharp
protected override IOpcEventHistoryProvider RetrieveEventHistoryProvider(IOpcNode node)
{
    if (node == this.machineEventHistory.Node)
        return this.machineEventHistory;
 
    return base.RetrieveEventHistoryProvider(node);
}

```

```vbnet
Protected Overrides Function RetrieveEventHistoryProvider( _
        ByVal node As IOpcNode) As IOpcEventHistoryProvider
    If node Is Me.machineEventHistory.Node Then
        Return Me.machineEventHistory
    End If

    Return MyBase.RetrieveEventHistoryProvider(node)
End Function
```

To record an event, create a snapshot with the `CreateEvent` method of the event node when you report it, and store the snapshot in the history:

```csharp
activatedEvent.Message = "Machine started";
activatedEvent.ReportEvent(this.SystemContext);
 
// Stores a snapshot of the event in the event history.
this.machineEventHistory.Add(activatedEvent.CreateEvent(this.SystemContext));
```

```vbnet
activatedEvent.Message = "Machine started"
activatedEvent.ReportEvent(Me.SystemContext)

'Stores a snapshot of the event in the event history.
Me.machineEventHistory.Add(activatedEvent.CreateEvent(Me.SystemContext))
```

The [Read the event history](https://www.traeger.de/en/docs/development/opcua/client/historical-data#read-the-event-history) section in the client chapter describes how a client reads the event history.
