Opc.Ua.Expressions 1.1.21

dotnet add package Opc.Ua.Expressions --version 1.1.21
NuGet\Install-Package Opc.Ua.Expressions -Version 1.1.21
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Opc.Ua.Expressions" Version="1.1.21" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Opc.Ua.Expressions --version 1.1.21
#r "nuget: Opc.Ua.Expressions, 1.1.21"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Opc.Ua.Expressions as a Cake Addin
#addin nuget:?package=Opc.Ua.Expressions&version=1.1.21

// Install Opc.Ua.Expressions as a Cake Tool
#tool nuget:?package=Opc.Ua.Expressions&version=1.1.21

Introduction

The OPC UA Expressions is a library that can maybe save the world or not.

Installing the package

Install-Package Opc.Ua.Expressions -Version 1.0.1

What do I do now

First of you create the model you want to read from your OPC UA server.

public class Universe {
    public List<Star> Stars { get; set; }
    public List<Planet> Planets { get; set; }

    public long Age { get; set; }
    public Planet BestPlanet { get; set; }
}

public class Planet {
    public string Name {get; set;}
    public long Population { get; set; }
}

public class Star {
    public bool IsHabitable { get; set; }
}

This is the minimal you have to do to read the structured data from an OPC device. Next I will show how to actually use this model.

  1. Add the using statement:
using Opc.Ua.Expressions;
  1. Create the client.
using Opc.Ua.Client;
using Opc.Ua.Expressions;

Session session = ... // your OPC UA session instance

// Create the client
OpcUaClient client = new OpcUaClient(session);
  1. Use the client to read/write an address
string name = client.ReadValueAsync<Universe, string>(x => x.Planets[2].Name);

// The line above is the same as if you read the following NodeId
// ns=3;s="Universe"."Planets"[2]."Name"

This way you never need to type an address again. Everything is strongly typed using expressions.

NOTE: The library does not create or mantain your OPC UA session. It uses your session. When your session is no longer valid you will need to create a new client or replace the session.

Features

Read/Write complex types using expressions

Not only do we support interaction with simple types like int, float ... but the library also supports complex types and lists. Using the same model as defined above we can do the following:

// Read the second planet from a list
Planet planet = await client.ReadValueAsync<Universe, Planet>(x => x.Planets[2]);

// Read all planets
List<Planet> planets =  await client.ReadValueAsync<Universe, List<Planet>>(x => x.Planets);
// Read all planets as an array
Planet[] planets =  await client.ReadValueAsync<Universe, Planet[]>(x => x.Planets);

// Writing a complex object
var result = await client.WriteValueAsync(x => x.Stars[0], new Star() { IsHabitable = false });

// Writing a list
var result = await client.WriteValueAsync(x => x.Stars, new List<Star>() { new Star() { IsHabitable = false }});

NOTE: When writing a list you must always write the complete list. So if the OPC UA server defines a collection of 10 items then you must always write a list of 10 items.

Subscribing to complex types using expressions

Subscription does not change much from the way the OPC UA foudation has implemented it. The difference is that expressions are supported.

// Create the subscription
Subscription subscription = new Subscription(session.DefaultSubscription)
{
    PublishingInterval = 1000
};

// Add the subscription to the session
client.Session.AddSubscription(subscription);
// Apply changes
await subscription.CreateAsync();
// Create the monitored item
var monitoredItem = await client.SubscribeAsync<Universe, bool>(subscription, x => x.Stars[2].IsHabitable, applyChanges: true);
// Subscribe to any changes for this item
monitoredItem.Notification += MonitoredItem_Notification;

private async void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
    // This manual mapping step is needed for now
    bool isHabitable = await client.MapNotificationValueAsync<bool>(monitoredItem);
}

Just like when reading and writing are complex types supported with subscriptions:

var monitoredItem = await client.SubscribeAsync<Universe, bool>(subscription, x => x.Stars[2], applyChanges: true);
// Subscribe to any changes for this item
monitoredItem.Notification += MonitoredItem_Notification;

private async void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
{
    // This manual mapping step is needed for now
    Star star = await client.MapNotificationValueAsync<Star>(monitoredItem);
}

For every change in the complex type (or a collection) the event will be triggered. This way you must not place subscriptions on every address but only on the parent address.

To unsubscribing can be done the following way:

await client.Unsubscribe(subscription, monitoredItem, applyChanges: true);

The apply changes parameter in both subscribe and unsubscribe is if the change should be pushed to the server or not. You can unsubscribe/subscribe multiple tags and only push these changes at the end.

As this uses the build-in OPC UA subscriptions you must still take into account the limitations of your specific OPC UA server.

Group multiple write operations using transactions

When writing it can be usefull to group multiple operations as one transaction.

using Opc.Ua.Expressions.Transactions;

Transaction transaction = client.Begintransaction();

transaction.Write<Universe, string>(x => x.BestPlanet.Name, "earth");
transaction.Write<Universe, bool>(x => x.Stars[0].IsHabitable, true);

var result = await transaction.CommitAsync();

// Using the following extension method (namespace Opc.Ua.Expressions) you can easly check if all operations where good
bool success = result.IsGood();

Optional configuration

Why many words when few do trick...

Attributes

If you have a model and your property name does not match the name in the OPC UA server you can add the following attribute:

using Opc.Ua.Expressions.Attributes;

[OpcAttribute("sName")]
public string Name { get; set; }

This can be useful when your naming conventions do not match the conventions used in the OPC UA server. Or when the language is different.

If you want to set a fixed address to a property for reading the "Current Time" from the server:

using Opc.Ua.Expressions.Attributes;

[OpcAddressAttribute("i=2258")]
public DateTime CurrentTime { get; set; }

NOTE: this can only be used when reading the property directly. NOT when reading the parent of the property.

In the examples above we used the name "Universe" as root object in our address bu what if this name does not match what is defined in the OPC UA server?

using Opc.Ua.Expressions.Attributes;

// configure a different root name 
[OpcRootAttribute("BigUniverse")]
public class Universe {
    ...
}

// Addresses will now look like this:
// ns=3;s="BigUniverse"."Planets"[2]."Name"

Configuration method

Instead of using attributes you can configure everything using a configuration object that is added as a second parameter when creating your client. This is not required

public OpcUaClient(Session session, ClientTypeConfiguration configuration = null) 
{
    ...
}

// Access the configuration later using the property
client.Configuration ...

Example:

var session = ...
var configuration = new ClientTypeConfiguration();

configuration.RegisterType<Universe>();
// configure one property
configuration.RegisterType<Star>()
    .ForProperty(x => x.IsHabitable)
    .UseName("Is_Habitable"); // When the server defines a different name

// configure multiple properties
configuration.RegisterType<Planet>(tc => {
    tc.ForProperty(x => x.Population).UseName("Planet_Population");
    tc.ForProperty(x => x.Name).UseName("Planet_Name");
});

var client = new OpcUaClient(session, configuration);
  • The configuration is optional. When a type is unknown it will be added automatically and any attributes will be applied.
  • Priority: property < attribute < configuration
  • The configuration can be accessed and changed later.
  • The configuration has no alternative for the OpcRootAttribute. This may change in later updates.

Custom type converters

TODO

Global type configuration

implement ITypeConverter and register it with configuration.RegisterType<Star>.UseConverter<StarConverter>(). Now this converter will be used whenever the Star type is encountered.

Interface on type

implement IConvertibleType on the type you want to support. No other configuration is needed. Example:

public class RecordControl : IConvertibleType
{
    public int RecordStatus { get; set; }
    public int UserID { get; set; }
    public DateTime ChangedDateTime { get; set; }

    public DateTime Changed => ChangedDateTime;

    public void Encode(ITypeEncoder encoder)
    {
        encoder.Write(nameof(RecordStatus), RecordStatus);
        encoder.Write(nameof(UserID), UserID);
        encoder.Write(nameof(ChangedDateTime), ChangedDateTime);
    }

    public void Decode(ITypeDecoder decoder)
    {
        RecordStatus = decoder.Read<int>(nameof(RecordStatus));
        UserID = decoder.Read<int>(nameof(UserID));
        ChangedDateTime = decoder.Read<DTL>(nameof(ChangedDateTime));
    }
}

using configuration for one property

TODO

use .ForProperty(...).UseConverter<...>(); or

... .ForProperty(...).UseConversion(
(decoder) => { return new DateTime(); },
(encoder, value) => { encoder.Write("YEAR", ((DateTime)value).Year); }
);

Good luck

Build and Test

  • Take the project
  • Build the project
  • Run the project
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
.NET Framework net48 is compatible.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Opc.Ua.Expressions:

Package Downloads
OpcUa.ExpressionServer

Simulation using an SQL database

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.21 125 2/29/2024
1.1.20 78 2/29/2024
1.1.19 78 2/29/2024
1.1.18 105 2/8/2024
1.1.17 267 7/14/2023
1.1.16 123 6/28/2023
1.1.15 207 4/24/2023
1.1.14 179 4/21/2023
1.1.13 165 4/21/2023
1.1.12 174 3/29/2023
1.1.11 383 12/1/2022
1.1.10 338 11/5/2022
1.1.9 331 11/5/2022
1.1.8 313 11/5/2022
1.1.6 322 11/3/2022
1.1.5 336 10/5/2022
1.1.4 329 10/5/2022
1.1.3 398 8/17/2022
1.1.2 368 8/16/2022
1.1.1 363 8/16/2022
1.1.0-alpha.5 145 7/13/2022

Added non-generic type registration