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
<PackageReference Include="Opc.Ua.Expressions" Version="1.1.21" />
paket add Opc.Ua.Expressions --version 1.1.21
#r "nuget: Opc.Ua.Expressions, 1.1.21"
// 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.
- Add the using statement:
using Opc.Ua.Expressions;
- 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);
- 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 | Versions 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. |
-
.NETFramework 4.8
- OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes (>= 1.5.373.121)
-
.NETStandard 2.1
- OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes (>= 1.5.373.121)
-
net6.0
- OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes (>= 1.5.373.121)
-
net8.0
- OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes (>= 1.5.373.121)
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 | 235 | 2/29/2024 |
1.1.20 | 122 | 2/29/2024 |
1.1.19 | 91 | 2/29/2024 |
1.1.18 | 138 | 2/8/2024 |
1.1.17 | 331 | 7/14/2023 |
1.1.16 | 150 | 6/28/2023 |
1.1.15 | 252 | 4/24/2023 |
1.1.14 | 219 | 4/21/2023 |
1.1.13 | 201 | 4/21/2023 |
1.1.12 | 188 | 3/29/2023 |
1.1.11 | 439 | 12/1/2022 |
1.1.10 | 362 | 11/5/2022 |
1.1.9 | 358 | 11/5/2022 |
1.1.8 | 340 | 11/5/2022 |
1.1.6 | 346 | 11/3/2022 |
1.1.5 | 361 | 10/5/2022 |
1.1.4 | 354 | 10/5/2022 |
1.1.3 | 423 | 8/17/2022 |
1.1.2 | 399 | 8/16/2022 |
1.1.1 | 390 | 8/16/2022 |
1.1.0-alpha.5 | 159 | 7/13/2022 |
Added non-generic type registration