OPC UA industrial library for executing commands, reads and writes on OPC UA servers
Here's a minimal example to connect to an OPC UA server, read a node, and disconnect:
using Atc.Opc.Ua.Services;
using Microsoft.Extensions.Logging;
// Create logger
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<OpcUaClient>();
// Create client
using var client = new OpcUaClient(logger);
// Connect
var serverUri = new Uri("opc.tcp://opcuaserver.com:48010");
var (connected, connectError) = await client.ConnectAsync(serverUri, CancellationToken.None);
if (!connected)
{
Console.WriteLine($"Connection failed: {connectError}");
return;
}
// Read a node variable
var (succeeded, nodeVariable, readError) = await client.ReadNodeVariableAsync(
"ns=2;s=Demo.Dynamic.Scalar.Float",
includeSampleValue: true,
CancellationToken.None);
if (succeeded && nodeVariable != null)
{
Console.WriteLine($"Value: {nodeVariable.Value}");
}
else
{
Console.WriteLine($"Read failed: {readError}");
}
// Disconnect
await client.DisconnectAsync(CancellationToken.None);After installing the latest nuget package, the OpcUaClient can be wired up with dependency injection:
services.AddTransient<IOpcUaClient, OpcUaClient>(s =>
{
var loggerFactory = s.GetRequiredService<ILoggerFactory>();
return new OpcUaClient(loggerFactory.CreateLogger<OpcUaClient>());
});Then use it in your services:
public class MyService
{
private readonly IOpcUaClient opcUaClient;
public MyService(IOpcUaClient opcUaClient)
{
this.opcUaClient = opcUaClient;
}
public async Task ReadDataAsync(CancellationToken cancellationToken)
{
var serverUri = new Uri("opc.tcp://opcuaserver.com:48010");
// Connect
var (connected, error) = await opcUaClient.ConnectAsync(
serverUri,
cancellationToken);
if (!connected)
{
// Handle error
return;
}
// Read node
var (succeeded, nodeVariable, readError) = await opcUaClient.ReadNodeVariableAsync(
"ns=2;s=Demo.Dynamic.Scalar.Float",
includeSampleValue: true,
cancellationToken);
// Process data...
// Disconnect
await opcUaClient.DisconnectAsync(cancellationToken);
}
}By default, the OpcUaClient will create its own self-signed certificate to present to external OPC UA Servers. However, if you have your own certificate to utilize, the service can be configured using the available OpcUaSecurityOptions. This class facilitates the configuration of certificate stores, the application certificate, and other essential security settings for secure communication.
These settings can be wired up from an appsettings.json file or manually constructed in code. Another constructor overload in OpcUaClient is available for injecting an instance of IOptions<OpcUaSecurityOptions>.
An example of this configuration in appsettings.json could look like the following.
Note: The example values below will be the default values, if they are not provided. Except subjectName, which will be something like 'OpcUaClient [RANDOM_SERIAL_NUMBER]' for the self-signed certificate generated.
{
"OpcUaSecurityOptions": {
"PkiRootPath": "opc/pki",
"ApplicationCertificate": {
"StoreType": "Directory",
"StorePath": "own",
"SubjectName": "CN=YourApp"
},
"RejectedCertificates": {
"StoreType": "Directory",
"StorePath": "rejected"
},
"TrustedIssuerCertificates": {
"StoreType": "Directory",
"StorePath": "issuers"
},
"TrustedPeerCertificates": {
"StoreType": "Directory",
"StorePath": "trusted"
},
"AddAppCertToTrustedStore": true,
"AutoAcceptUntrustedCertificates": false,
"MinimumCertificateKeySize": 1024,
"RejectSha1SignedCertificates": true,
"RejectUnknownRevocationStatus": true
}
}and from your C# code:
services
.AddOptions<OpcUaSecurityOptions>()
.Bind(configuration.GetRequiredSection(nameof(OpcUaSecurityOptions)))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddTransient<IOpcUaClient, OpcUaClient>();Then use with proper async patterns:
public async Task ConnectSecurelyAsync(CancellationToken cancellationToken)
{
var serverUri = new Uri("opc.tcp://opcuaserver.com:48010");
var (connected, error) = await _opcUaClient.ConnectAsync(
serverUri,
userName: "myuser",
password: "mypassword",
cancellationToken);
// ... use client
}You can customize the client application name and the OPC UA session timeout via OpcUaClientOptions. Bind them from configuration or construct them programmatically.
Example appsettings.json:
{
"OpcUaClientOptions": {
"ApplicationName": "MyOpcUaClientApp",
"SessionTimeoutMilliseconds": 1800000
}
}And wire them up:
services
.AddOptions<OpcUaClientOptions>()
.Bind(configuration.GetRequiredSection(nameof(OpcUaClientOptions)))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddTransient<IOpcUaClient, OpcUaClient>();Defaults (if not provided):
ApplicationName: "OpcUaClient"SessionTimeoutMilliseconds: 1,800,000 (30 minutes)
When enabled, the client monitors the connection using OPC UA keep-alive and will attempt a background reconnect after a configurable number of consecutive failures. You can tune or disable the behavior via OpcUaClientKeepAliveOptions.
Example appsettings.json:
{
"OpcUaClientKeepAliveOptions": {
"Enable": true,
"IntervalMilliseconds": 15000,
"MaxFailuresBeforeReconnect": 3,
"ReconnectPeriodMilliseconds": 10000
}
}And wire them up:
services
.AddOptions<OpcUaClientKeepAliveOptions>()
.Bind(configuration.GetRequiredSection(nameof(OpcUaClientKeepAliveOptions)))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddTransient<IOpcUaClient, OpcUaClient>();When working with OpcUaClient, follow these async/await best practices:
-
✅ Always pass CancellationToken: All async methods require a
CancellationToken. UseCancellationToken.Noneonly for non-cancellable operations.// Good - from method parameter public async Task ProcessAsync(CancellationToken cancellationToken) { await client.ConnectAsync(serverUri, cancellationToken); } // Good - with timeout using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await client.ConnectAsync(serverUri, cts.Token);
-
✅ Use
usingstatements: Ensure proper disposal of the client to release resources.using var client = new OpcUaClient(logger); await client.ConnectAsync(serverUri, cancellationToken); // Client is automatically disposed
-
✅ Handle connection errors: Always check the
Succeededflag from async operations.var (succeeded, nodeVariable, error) = await client.ReadNodeVariableAsync( nodeId, includeSampleValue: true, cancellationToken); if (!succeeded) { _logger.LogError("Read failed: {Error}", error); // Handle error appropriately }
-
✅ Disconnect explicitly: Call
DisconnectAsyncbefore disposal for clean shutdown.await client.DisconnectAsync(cancellationToken);
-
✅ Reuse client instances: For multiple operations, reuse the same connected client rather than creating new instances.
The Atc.Opc.Ua.CLI tool is available through a cross platform command line application.
The tool can be installed as a .NET global tool by the following command
dotnet tool install --global atc-opc-uaor by following the instructions here to install a specific version of the tool.
A successful installation will output something like
The tool can be invoked by the following command: atc-opc-ua
Tool 'atc-opc-ua' (version '2.0.xxx') was successfully installed.The tool can be updated by the following command
dotnet tool update --global atc-opc-uaSince the tool is published as a .NET Tool, it can be launched from anywhere using any shell or command-line interface by calling atc-opc-ua. The help information is displayed when providing the --help argument to atc-opc-ua
atc-opc-ua --help
USAGE:
atc-opc-ua.exe [OPTIONS] <COMMAND>
EXAMPLES:
atc-opc-ua.exe testconnection -s opc.tcp://opcuaserver.com:48010
atc-opc-ua.exe testconnection -s opc.tcp://opcuaserver.com:48010 -u username -p password
atc-opc-ua.exe node read object -s opc.tcp://opcuaserver.com:48010 -n "ns=2;s=Demo.Dynamic.Scalar"
atc-opc-ua.exe node read variable single -s opc.tcp://opcuaserver.com:48010 -n "ns=2;s=Demo.Dynamic.Scalar.Float"
atc-opc-ua.exe node read variable multi -s opc.tcp://opcuaserver.com:48010 -n "ns=2;s=Demo.Dynamic.Scalar.Float" -n "ns=2;s=Demo.Dynamic.Scalar.Int32"
atc-opc-ua.exe node scan -s opc.tcp://opcuaserver.com:48010 --starting-node-id "ns=2;s=Demo.Dynamic.Scalar" --object-depth 2 --variable-depth 1
OPTIONS:
-h, --help Prints help information
-v, --version Prints version information
COMMANDS:
testconnection Tests if a connection can be made to a given server
node Operations related to nodesThe scan command builds an object/variable tree starting from a specified node (default: ObjectsFolder). Include / exclude filters are applied DURING traversal so unwanted branches are skipped early (reducing server browse load) rather than pruned afterwards:
atc-opc-ua node scan -s opc.tcp://opcuaserver.com:48010 --starting-node-id "ns=2;s=Demo.Dynamic.Scalar" --object-depth 2 --variable-depth 1 --include-sample-valuesKey options:
--starting-node-idStarting object node (defaults to ObjectsFolder when omitted/empty).--object-depthMaximum depth of object traversal (0 = only starting object). Default 1.--variable-depthMaximum depth for nested variable browsing (0 = only direct variables). Default 0.--include-sample-valuesIf set, attempts to read a representative value for variables.--include-object-node-idOne or more object NodeIds to explicitly include (acts as allow‑list). When provided, objects not listed are skipped during traversal (unless explicitly excluded).--exclude-object-node-idOne or more object NodeIds to exclude.--include-variable-node-idOne or more variable NodeIds to explicitly include.--exclude-variable-node-idOne or more variable NodeIds to exclude.
In conflicts (the same id both included and excluded) exclusion wins. When an include list is present it acts as a whitelist and nodes not listed are never browsed deeper.
Example restricting to a single variable while excluding an object:
atc-opc-ua node scan -s opc.tcp://opcuaserver.com:48010 --starting-node-id "ns=2;s=Demo.Dynamic.Scalar" --include-variable-node-id "ns=2;s=Demo.Dynamic.Scalar.Float" --exclude-object-node-id "ns=2;s=Unwanted.Object"- .NET 9 SDK or later
- OPCFoundation.NetStandard.Opc.Ua 1.5.377.21