PLCExtension 1.1.3

dotnet add package PLCExtension --version 1.1.3
                    
NuGet\Install-Package PLCExtension -Version 1.1.3
                    
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="PLCExtension" Version="1.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PLCExtension" Version="1.1.3" />
                    
Directory.Packages.props
<PackageReference Include="PLCExtension" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PLCExtension --version 1.1.3
                    
#r "nuget: PLCExtension, 1.1.3"
                    
#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.
#:package PLCExtension@1.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PLCExtension&version=1.1.3
                    
Install as a Cake Addin
#tool nuget:?package=PLCExtension&version=1.1.3
                    
Install as a Cake Tool

Language

Vietnamese | English

PLCExtension

PLCExtension is a .NET library for mapping PLC data to strongly typed C# properties. Instead of manually reading and writing individual PLC addresses, define a class that inherits from PLCDataContext, map properties with [PLCAddress] or runtime mappings, then load and save data through the object.

The package supports netstandard2.0, net6.0, net7.0, net8.0, net9.0, and net10.0. PLC communication is handled through McpX.

Installation

dotnet add package PLCExtension

Quick Start

using McpXLib;

public class QRScanData : PLCDataContext
{
    public QRScanData(McpX plc) : base(plc) { }

    [PLCAddress("M6011", description: "QR scan command")]
    public bool ScanCommand { get; set; }

    [PLCAddress("M6012", description: "OK status")]
    public bool OkStatus { get; set; }

    [PLCAddress("D6200", length: 100, description: "QR buffer")]
    public string QRBarcode { get; set; } = "";

    [PLCAddress("D6300", description: "Speed")]
    public int RollSpeed { get; set; }

    [PLCAddress("D6310", description: "Length")]
    public float RollLength { get; set; }
}

Usage:

var data = new QRScanData(plc);

await data.LoadAllSequentialAsync();

if (data.ScanCommand)
{
    data.OkStatus = true;
    data.RollSpeed = 500;
    data.RollLength = 120.5f;

    await data.SaveAllSequentialAsync();
}

How PLCDataContext Works

PLCDataContext finds properties that have PLC address metadata, reads the corresponding data from the PLC, converts it to the matching C# type, and sets the value on the object. When writing, it takes the current property value, converts it to the required bit/word representation, and writes it back to the PLC.

PLC address metadata can come from:

  • A [PLCAddress] attribute directly on the property.
  • A dictionary passed to PLCDataContext(McpX, Dictionary<string, PLCAddressAttribute>).

If a property has both an attribute and a runtime mapping, the runtime mapping takes priority. Missing runtime mapping fields fall back to the attribute when available.

Attribute-Based Mapping

public class MachineStatusData : PLCDataContext
{
    public MachineStatusData(McpX plc) : base(plc) { }

    [PLCAddress("M100", description: "Machine is running")]
    public bool IsRunning { get; set; }

    [PLCAddress("D200", length: 20, description: "Product code")]
    public string ProductCode { get; set; } = "";

    [PLCAddress("D230", readOnly: true, description: "Read-only counter")]
    public int TotalCounter { get; set; }
}

Attribute parameters:

Parameter Description
address PLC address, for example M100 or D200. The first character is parsed as McpXLib.Enums.Prefix.
length Number of words to read/write. If 0, the library calculates the length from the property type.
description Optional description for documenting the mapping.
readOnly If true, all save methods and WriteValueAsync() skip this property when writing.

Runtime Mapping

Use runtime mapping when PLC addresses need to be configured outside the codebase or vary by machine, line, or PLC version.

public class MachineStatusData : PLCDataContext
{
    public MachineStatusData(McpX plc, Dictionary<string, PLCAddressAttribute> mapping)
        : base(plc, mapping) { }

    public bool IsRunning { get; set; }
    public string ProductCode { get; set; } = "";
    public int TotalCounter { get; set; }
}

Example mapping.json:

{
  "IsRunning": {
    "Address": "M100",
    "Length": 0,
    "Description": "Machine is running",
    "ReadOnly": false
  },
  "ProductCode": {
    "Address": "D200",
    "Length": 20,
    "Description": "Product code",
    "ReadOnly": false
  },
  "TotalCounter": {
    "Address": "D230",
    "Length": 0,
    "Description": "Read-only counter",
    "ReadOnly": true
  }
}

Load the mapping:

using Newtonsoft.Json;

var json = File.ReadAllText("mapping.json");
var mapping = JsonConvert.DeserializeObject<Dictionary<string, PLCAddressAttribute>>(json)
              ?? new Dictionary<string, PLCAddressAttribute>();

var data = new MachineStatusData(plc, mapping);
await data.LoadAllSequentialAsync();

Read and Save API

The load and save APIs are intentionally paired:

Method Description
LoadAllSequentialAsync() Reads all mapped properties one by one. This is the safest default option.
SaveAllSequentialAsync() Writes all mapped properties one by one. This is the safest default option.
LoadAllParallelAsync() Reads all mapped properties with Parallel.ForEachAsync. Available on net6.0 and later.
SaveAllParallelAsync() Writes all mapped properties with Parallel.ForEachAsync. Available on net6.0 and later.
LoadAllTasksAsync() Creates one read task per mapped property and waits with Task.WhenAll.
SaveAllTasksAsync() Creates one write task per mapped property and waits with Task.WhenAll.
ReadValueAsync(string propertyName) Reads one property from the PLC and returns the value. It does not set the value on the object.
WriteValueAsync(string propertyName, object? value) Writes one value to the PLC address mapped to the property.
ToJsonString() Serializes the current object with Newtonsoft.Json. Internal fields such as the PLC device and mapping are ignored.

SaveAllSequentialAsync(), SaveAllParallelAsync(), SaveAllTasksAsync(), and WriteValueAsync() all respect readOnly: true.

Read or write a single property:

var current = await data.ReadValueAsync(nameof(MachineStatusData.TotalCounter));
await data.WriteValueAsync(nameof(MachineStatusData.ProductCode), "ROLL-001");

Choosing a Load/Save Mode

Use the sequential mode unless you know the PLC driver and target PLC can handle concurrent requests.

Mode Read Save When to use
Sequential LoadAllSequentialAsync() SaveAllSequentialAsync() Default mode, predictable order, lowest pressure on PLC communication.
Parallel LoadAllParallelAsync() SaveAllParallelAsync() net6.0+ only; useful when many properties can be processed concurrently.
Tasks LoadAllTasksAsync() SaveAllTasksAsync() Maximum task concurrency; use carefully with PLC request limits.

Supported Data Types

PLCDataContext directly reads and writes the following types:

C# type Default words Notes
bool 1 Uses Read<bool> and Write<bool>.
string 1 if not specified Declare length explicitly to define the buffer size. Each word stores 2 characters.
int 2 Converted through 2 words using BitConverter little-endian layout.
float 2 Read values are rounded to 2 decimal places.
double 4 Converted through 4 words.
decimal 8 Converted through 8 words.
short[] From length Raw word buffer.

PLCTypeHelper defines default lengths for additional types such as short, long, and other arrays, but PLCDataContext.ReadValueAsync and WriteValueAsync currently handle only the types listed above directly.

Property Change Tracking

PLCDataContext implements INotifyPropertyChanged. When LoadAllSequentialAsync(), LoadAllParallelAsync(), or LoadAllTasksAsync() reads a value that differs from the current property value, the object will:

  • Set the new property value.
  • Raise PropertyChanged.
  • Raise PropertyValueChanged with oldValue and newValue.
  • Print a log in the format [CHANGE] Class.Property: old -> new.

Subscribe by property name:

var subscription = data.WhenPropertyChanges<bool>(
    nameof(MachineStatusData.IsRunning),
    isRunning =>
    {
        Console.WriteLine($"IsRunning = {isRunning}");
    });

subscription.Dispose();

Subscribe with old/new values:

data.WhenPropertyChanges<int>(
    nameof(MachineStatusData.TotalCounter),
    (oldValue, newValue) =>
    {
        Console.WriteLine($"Counter: {oldValue} -> {newValue}");
    });

Use an async handler:

data.WhenPropertyChanges<string>(
    nameof(MachineStatusData.ProductCode),
    async code =>
    {
        await SendProductCodeToMesAsync(code);
    });

Debounce noisy PLC updates to avoid repeated API calls:

data.WhenPropertyChangesDebounced<string>(
    nameof(MachineStatusData.ProductCode),
    debounceMs: 300,
    async code =>
    {
        await SendProductCodeToMesAsync(code);
    });

Expression-based overloads are also available:

data.WhenPropertyChanges<string>(
    x => ((MachineStatusData)x).ProductCode,
    code => Console.WriteLine(code));

Extension Methods

PLCDataExtensions provides helper methods for objects that inherit from PLCDataContext:

Method Description
GetPLCProperties() Gets properties with a [PLCAddress] attribute. Note: this method does not read runtime mappings.
CopyValuesFrom(source) Copies values between two objects of the same type, based on attributed properties.
ValuesEqual(other) Compares values between two objects of the same type. Includes special handling for short[].
ExportToDictionary() Exports values to a dictionary where keys are PLC addresses.
ImportFromDictionary(data) Imports values from a dictionary where keys are PLC addresses.

Example:

var snapshot = data.ExportToDictionary();

var other = new QRScanData(plc);
other.CopyValuesFrom(data);

Custom Logging

Override CustomMessage to add a prefix or customize error/change logs:

public class MachineStatusData : PLCDataContext
{
    public MachineStatusData(McpX plc) : base(plc) { }

    protected override string CustomMessage(string message)
        => $"[Line A] {message}";
}

Implementation Notes

  • Each low-level read/write operation uses an internal SemaphoreSlim to lock access to McpX.
  • Addresses must start with a prefix defined in McpXLib.Enums.Prefix, such as M or D.
  • ReadValueAsync(string) returns the value only; it does not update the property on the object.
  • All save methods write mapped properties except those marked readOnly.
  • LoadAllParallelAsync() and SaveAllParallelAsync() are compiled only for net6.0 and later.
  • For string and short[], declare length explicitly to avoid reading or writing incomplete data.
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 is compatible.  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.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.3 40 6/8/2026
1.1.2 35 6/8/2026
1.1.1 43 6/8/2026
1.1.0 77 6/4/2026
1.0.9 81 6/3/2026
1.0.8 83 6/3/2026
1.0.7 85 6/3/2026
1.0.6 81 6/3/2026
1.0.5 101 6/1/2026
1.0.4 89 6/1/2026
1.0.3 89 6/1/2026
1.0.2 98 5/22/2026
1.0.1 93 5/22/2026
1.0.0 93 5/22/2026