-
Notifications
You must be signed in to change notification settings - Fork 62
Remove Mapper system (Tag<M,T>
, IPlcMapper
, DintPlcMapper
, TagReal
etc).
#406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thanks for the great write up!
I think that I made a mistake when I supported more than the atomic types
in the core DLL.
The core should support the base protocol. This includes connection
handling, packing requests, handling fragmented requests and responses, and
protocol-level error handling. And it should have provided a set of hooks
to call for encode/decode. At this level requests and responses are just
bags of bytes.
The only data convenience functions should have been the atomic types. And
those should have been in a separate layer.
Unfortunately I started to support strings and that opened up a big can of
worms. Any kind of structured data should not be the core's concern.
Strings raise the difficulty even higher because they are inconsistent even
across the same hardware.
Strings should not be supported in the core or the core wrapper. They make
more sense to support as a library of example code. Even then you need to
know the context. E.g. I've found that the same string type can be
returned in at least three different formats with Omron PLCs.
And do not forget about BOOL arrays! Those are really complicated because
even the meaning of the tag path changes depending on the manufacturer!
'MyBOOLArray[3]" means very different things on a Rockwell PLC vs an Omron
PLC.
I wish I could do it over without breaking everything!
Best,
Kyle
… —
Reply to this email directly, view it on GitHub
<#406>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4LC7PTWQPQ2O23ATNITDZMZCCNAVCNFSM6AAAAABLAK5SCSVHI2DSMVQWIX3LMV43ASLTON2WKOZSGQYTGMRUHEZDSOA>
.
You are receiving this because you are subscribed to this thread.
Message ID: ***@***.***>
|
I have some concerns with this. (Note: Please correct me if I misunderstood anything in your explanation.) UDTsWe're currently using the custom mappers to handle UDTs quite extensively. How do you imagine this would impact how UDTs are handled? Would something in this new version of libplctag.NET handle UDTs itself in a way that wouldn't necessitate the mappers? If not, we would need to use this potential I'm concerned with needing more dependencies if the latter were the case. I would prefer that it remains part of the main library to needing another library to maintain. WrapperJust to give a little more information regarding your mention of wrappers, we do have our own wrapper around libplctag.NET, but for the purpose of being able to support other PLC manufacturers and protocols like Siemens and OPC UA. We have an We define our tags in an XML file with a generic name that we use throughout our business logic. This name is defined with associated in the XML file, for each PLC driver that we support, with information used to generate the tag with (address, data type, etc.). Then I do some reflection to generate a dynamic typed Here again, we'd either need to rewrite all of this logic to use just what libplctag.NET provides or use this additional Mapper library and again, I'd prefer if this all just remained part of the library. |
Hi @trinnan42 - thanks for the feedback! You are correct in saying you would need to find an alternate implementation for the mapper system - either one you maintain yourself or a separate package. The proposal can be seen in this branch. As I see it, the value of
For point 1, it sounds like you're already doing some logic to convert from a typed value back into For point 2, frankly I'm doing a terrible job at supporting scenarios other than the fairly specific Rockwell/Atomic tag scenario - and I don't really have a way to improve this other than wait for complaints to roll in. Do you mind if I ask what value you get out of the Thanks so much for the detailed response to the post! |
@kyle-github - yep 100%, and I think that the libplctag.NET wrapper using the libplctag name makes some implicit promise about what it does, which can be tricky to get to the bottom of when things go wrong. |
Well, to try to explain with more detail what I'm doing without being able to share actual code:
I guess I'm not exactly sure myself what value I'm getting out of Since I'm not familiar with libplctag itself, I guess what I'm not sure about is how I would do things without It seems I'd have to replace this code with something like a switch statement that just uses the base We're also doing a lot with arrays of tags and UDTs. That support is provided by the PlcMapper system, right? Otherwise we'd be looping through and reading the tag with these base Set and Get methods? I think the PlcMapper system is indeed preventing us from writing some of that code ourselves. We're also using the code from the I was somewhat watching the recent problems someone was having with Omron, is that part of what's prompting this? The differences between how the different PLC manufacturers handle the specifics and the mappers not handling things properly between this differences? We do intend to support Omron as some of our customers do require their PLCs, though we haven't made any attempt to implement this so far. I was planning on attempting to use OPC UA (and that might be our path forward for all PLCs given the cybersecurity pushes for secure protocols that we're getting from some customers). It seems Rockwell is slow to adopt OPC UA and sounds like they want to charge extra for it, unlike Siemens who just include it in their CPUs. |
Thanks @trinnan42 - this is great info!
100% correct.
100% correct and this is where the Mapper system shines. Yes you would need to find some other way to do this.
100% correct.
Solving most of these issues would have been simpler if we instead used inheritance. In retrospect this would have been a better choice.
It certainly is another example of the issue that library consumers face but we have had these problems for years.
This might not be the point you're making but potentially there is a case for remaining; in that you could argue being "official" it is more likely to get exposure, trust and momentum around it, and therefore would be maintained at a higher level.
You can absolutely sidestep the Mapper system (even as libplctag.NET currently stands today) and do something like this: enum MyTagType { SINT, INT, DINT /* etc */ }
class MyTag
{
private readonly Tag _tag;
private readonly Func<dynamic> decoder;
private readonly Action<dynamic> encoder;
public MyTag(MyTagType tagType)
{
_tag = new Tag()
{
PlcType = PlcType.ControlLogix,
Protocol = Protocol.ab_eip,
};
switch (tagType)
{
case MyTagType.SINT:
_tag.ElementSize = 1;
decoder = () => _tag.GetInt8(0);
encoder = (value) => _tag.SetInt8(0, value);
break;
case MyTagType.INT:
_tag.ElementSize = 2;
decoder = () => _tag.GetInt16(0);
encoder = (value) => _tag.SetInt16(0, value);
break;
case MyTagType.DINT:
_tag.ElementSize = 4;
decoder = () => _tag.GetInt32(0);
encoder = (value) => _tag.SetInt32(0, value);
break;
}
}
public Task InitializeAsync() => _tag.InitializeAsync();
public Task WriteAsync() => _tag.WriteAsync();
public Task ReadAsync() => _tag.ReadAsync();
public object Value { get => decoder(); set => encoder(value); }
public string Gateway { get => _tag.Gateway; set => _tag.Gateway = value; }
public string Path { get => _tag.Path; set => _tag.Path = value; }
public string Name { get => _tag.Name; set => _tag.Name = value; }
} Which would be used like this: var myTag = new MyTag(MyTagType.DINT)
{
Name = "PROGRAM:SomeProgram.SomeDINT",
Gateway = "10.10.10.10",
Path = "1,0",
};
await myTag.ReadAsync();
var originalValue = myTag.Value;
Console.WriteLine($"Original value: {originalValue}");
int newValue = 3737;
myTag.Value = newValue;
await myTag.WriteAsync();
Console.WriteLine($"New value: {newValue}"); |
@timyhac Your explanations are much appreciated! I've got a better understanding now. This certainly alleviates my concerns. |
As a recent user of libplctag.NET, I too have to quote this:
Accordingly, I developed many custom mappers in order to work with all my custom datatypes and even for Omron system structure (_sAXIS_REF, etc.), Omron being the brand I use. How I "share" tag values in my application is actually a mess, basically I register them in a list and access their values with a LINQ method finding them by name. I couldn't come up with a better idea at the time, and the I stayed with that. Following this with interest. |
Thanks for your input @MountainKing91! |
@timyhac do you do this only by github search or is there anything else? I.e. user sharing their application or something? |
@MountainKing91 - just github/google. |
Although issue #406 says to **remove** these classes, start the process by deprecating them instead.
@trinnan42, @MountainKing91 and @MitchRazga thankyou so much for your engagement and input! I believe that removing the Mapper system and associated classes is still the right call to benefit the libplctag community long term, but have decided to phase out the mapper system in two stages to ease the transition:
As you suggested, much of the issue is that the examples and documentation promote the use of the mapper system - so stage 1 will mitigate that part of the problem - while allowing current users to gradually transition. For stage 2, I have not yet put in place an alternative for current users to migrate to, but I am thinking it will be a separate package but a drop-in replacement - it would be a separate package, and would not be an "official" .NET wrapper. |
Let me know your thoughts on how to set up a separate "extras" package. I am really interested in doing that for the core DLL. I want to pull out the examples, tools and tests (which are horribly mixed up now) from the main library. |
Heya @kyle-github The examples and tests would stay in the libplctag.NET github repository, but I was thinking that the classes shipped with the nuget package that I just deprecated would be moved to a separate repository and released as a separate nuget package from there. The purpose being to make it clear that it is not a "libplctag" initiative. A separate but related thought was that there could be a repository which does attempt to document the behaviour of the various devices the libplctag community uses. |
Hi guys, I'm looking at the examples, trying to move past using the mappers. I see in the examples folder that there are now two classes BaseTag.cs and Definitions.cs. Is this intended to be an alternative to mappers or am I missing something? Ty! |
Yes exactly - in the top post I said a little bit about it under the "What's the alternative?" heading in the top post on this issue. |
For anyone googling moving away from mappers... ExampleListPlc.cs in the .NET Core examples folder is what you need. |
Hi, new to this library and have been experimenting with it and saw that the intent was to deprecate the mapper system and that alternatives to it were being investigated it. I'm currently working with ControlLogix and probably the easiest alternative is to just use marshalling. If other platforms use fixed-size data types, then it's probably easily applicable to those as well. I made some extension methods for the Tag class for illustration purposes that I'll share: public static T GetValue<T>(this Tag tag) where T : struct
{
byte[] buffer = tag.GetBuffer();
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
//ToInt64 is used because it's assumed the code will compile and run on 64-bit platform.
//If it's going to run on a 32-platform, ToInt32 should be used.
T retVal = Marshal.PtrToStructure<T>(new IntPtr(handle.AddrOfPinnedObject().ToInt64()))!;
return (retVal);
}
finally
{
handle.Free();
}
} The key to all of this is the Tag t = new Tag()
{
Name = "SomeDintTag"
//additional properties
};
t.Read();
int i = t.GetValue<int>(); Even though strings are technically primitives in .NET, PtrToStructure will fail because strings aren't fixed length so it doesn't know what to do with them. Additionally, strings in Logix are UDTs so they have to be handled specially anyways. Which brings us to... Marshalling UDTsYou can marshal UDTs by declaring structures that implement the following rules:
Take for example the following UDT
There's 6 total bytes, but Logix will send 8. The first block will contain Field 1 in the first two bytes + two bytes of padding, and then Field 2 will be sent in the second block. The corresponding struct is: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SomeUDT
{
public short Field1;
public int Field2;
} No special accommodation needs to be made for padding, it's handled automatically. Logix strings can be converted with the following struct: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct LogixString
{
public int Length;
[MarshalAs(UnmanagedType.ByValStr, SizeConst =82)]
public string Data;
} Note that in this case a .NET string type can be used because the property is decorated with the LogixString ls = tag.GetValue<LogixString>(); Then the Data field can be accessed to get the actual string value. Or to make it even simpler, an implicit operator overload can be added to the LogixString struct: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct LogixString
{
public int Length;
[MarshalAs(UnmanagedType.ByValStr, SizeConst =82)]
public string Data;
public static implicit operator string(LogixString s) => s.Data;
} And then used as follows: string s = tag.GetValue<LogixString>(); Complex UDTs can also be converted. Here's a (rather large) struct that's based off a real UDT from a real world PLC program currently in production. I've tested this live and all data fields are correctly converted, including empty array indices: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct DataRecordUDT
{
public int Field1;
public int Field2;
public int Field3;
public int Field4;
public int Field5;
public int Field6;
public int Field7;
public short Field8;
public LogixString Field9;
public short Field10;
public short Field11;
public float Field12;
public float Field13;
public float Field14;
public short Field15;
public float Field16;
public float Field17;
public float Field18;
public float Field19;
public short Field20;
public float Field21;
public float Field22;
public float Field23;
public float Field24;
public float Field25;
public float Field26;
public float Field27;
public int Field28;
public int Field29;
public int Field30;
public int Field31;
public int Field32;
public int Field33;
public int Field34;
public int Field35;
public int Field36;
public int Field37;
public int Field38;
public int Field39;
public int Field40;
public int Field41;
public int Field42;
public int Field43;
public int Field44;
public int Field45;
public int Field46;
public int Field47;
public int Field48;
public int Field49;
public int Field50;
public int Field51;
public int Field52;
public int Field53;
public int Field54;
public int Field55;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public LogixString[] Array1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public short[] Array3;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array4;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array5;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public short[] Array6;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public LogixString[] Array7;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public short[] Array8;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public short[] Array9;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array10;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array11;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array12;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] Array13;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public short[] Array14;
public float Field56;
public int Field57;
} Apologies for how huge that is, but I really wanted to drive home how well this method works. Nested UDTs, arrays, arrays of UDTs are all correctly converted. When handling arrays nested in a UDT, the BOOLs in UDTs Ruin EverythingBoolean values in UDTs are quite special in how they're handled in Logix and so special consideration has to be made when creating a corresponding struct. Bools in Logix are stored as bits and when transmitted in a UDT they're packed into bytes. This makes it a bit cumbersome but not impossible to deal with. Consider the following UDT:
This gets transmitted as 8 bytes total. The bools are packed into a single byte + 3 bytes of padding, then 4 bytes for the DINT. A corresponding struct would be: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SomeOtherUDT
{
public byte Bools;
public int Field4
} If all 3 bools are on, then the byte value will read as 7 (bit 0, 1, and 2). Different methods can be used to determine which bits are set, like using a BitArray or shifting: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SomeOtherUDT
{
public byte Bools;
public int Field4
public bool Field1
{
get
{
BitArray ba = new BitArray(new byte[] { Bools });
return ba[0];
}
}
public bool Field2
{
get { return (Bools & (1 << 1)) != 0; }
}
public bool Field3
{
get { return (Bools & (1 << 2)) != 0; }
}
} If boolean fields are interwoven between multiple fields in a UDT, then the corresponding struct will have to have a byte for each block of consecutive bools. A pain, but manageable. Here's a quick example of what I mean. UDT:
Corresponding struct: [StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct SomeUDT
{
public byte Bools1;
public int Field3;
public byte Bools2;
public float Field6;
public bool Field7;
public int Field8
public bool Field9;
} It may seem confusing that Field7 and Field9 are typed as bool, but that's because there's only 1 boolean field in the UDT between the other data types, they're padded out to 4 bytes, and Windows bools are 4-bytes long so the mapping will work. The annoyances of handling booleans can be mitigated by careful design of UDTs. Putting all bools at the start or end of a UDT can arguably make it easier to reason about them when creating a struct. Marshalling ArraysAll of the above works with single tag values, but until now I ignored array tags because those have to be handled differently. Arrays in .NET are reference types, so Marshal.PtrToStructure can't be used to directly convert arrays from Logix into .NET arrays. Instead, a little bit of special handling must be done so I made an extension method just for arrays: public static T[] GetArray<T>(this Tag tag) where T : struct
{
byte[] buffer = tag.GetBuffer();
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
int elementSize = Marshal.SizeOf<T>();
T[] destination = new T[buffer.Length / elementSize];
//Note ToInt64 assuming a 64-bit platform
IntPtr source = new IntPtr(handle.AddrOfPinnedObject().ToInt64());
for (int i = 0; i < destination.Length; i++)
{
destination[i] = Marshal.PtrToStructure<T>(source);
source = IntPtr.Add(source, elementSize);
}
return (destination);
}
finally
{
handle.Free();
}
} Basically, an array is declared and then each element is set one at a time by looping through blocks of bytes. This method will handle both atomic types and UDTs. Usage is as follows: Tag tag1 = //initialization
Tag tag2 = //initialization
tag1.Read();
tag2.Read();
float[] floatValues = tag1.GetArray<float>();
SomeUDT[] udtValues = tag2.GetArray<SomeUDT>(); Writing to TagsWriting to a tag is essentially the same as above but in reverse, so I don't think I need to go into much detail. Instead, here are the corresponding extension methods I came up with: public static void SetArray<T>(this Tag tag, T[] array) where T : struct
{
ArgumentOutOfRangeException.ThrowIfZero(array.Length, nameof(array));
byte[] buffer = new byte[Marshal.SizeOf<T>() * array.Length];
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
//ToInt64 assuming 64-bit platform
IntPtr bufferAddress = new IntPtr(handle.AddrOfPinnedObject().ToInt64());
for (int i = 0; i < array.Length; i++)
{
Marshal.StructureToPtr(array[i], bufferAddress, false);
bufferAddress = IntPtr.Add(bufferAddress, Marshal.SizeOf<T>());
}
tag.SetBuffer(buffer);
}
finally
{
handle.Free();
}
}
public static void SetValue<T>(this Tag tag, T value) where T : struct
{
byte[] buffer = new byte[Marshal.SizeOf<T>()];
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
//ToInt64 assuming 64-bit platform
Marshal.StructureToPtr(value!, new IntPtr(handle.AddrOfPinnedObject().ToInt64()), false);
tag.SetBuffer(buffer);
}
finally
{
handle.Free();
}
} I'll admit that I haven't been able to extensively test writing. I've only been able to access PLCs that are "live," so I don't have one to play with where I can write arbitrary values and see what happens. However, I've been able to test and see that the byte buffer array appears to be set correctly. Usage is to just call the Set* method with your data type and then tag.Write(); Existing code in the libplctag library allows reading and writing to offsets, and that should be something easily integrated into these extension methods. They ultimately map to addresses, so an offset parameter could be added to the address before marshalling. Finally, I'll also point out I've done no performance benchmarking. I don't know how well this works with large volumes of data or a large number of read/writes. For my purposes it's performed as well as it's needed to. Misc. Notes
|
Hi @zmilliron - this is a fantastic contribution, thanks so much for sharing! If you're up for it, it would be great to add your marshalling concept as another alternative in the Examples. |
Uh oh!
There was an error while loading. Please reload this page.
A few years after the release of the official libplctag.NET wrapper, a pattern has emerged that all library consumers face a key problem;
In this ticket I will be making the argument that the Mapper system (
Tag<M,T>
,IPlcMapper
, etc) provided by libplctag.NET has magnified this problem by obscuring/hiding it from library users.This is to the detriment of those users, reduces the ability of the libplctag community to assist those users, and increases the burden on library authors - and therefore should be removed.
Introduction
libplctag handles the details of communicating with PLCs over Modbus or Ethernet/IP Explicit Messaging, so you can easily access the value of PLC tags.
This statement is mostly true; while it is correct that you (most of the time) do not need to understand the protocol itself, you do need to understand the device's Modbus or Ethernet/IP API, as not all devices are the same.
This is similar to using a HTTP REST API - while you do not need to understand the details of TCP/IP or even HTTP, you definitely need to know what data to send and how to interpret the response.
For example; you need to know how to configure the EthernetIP or Modbus requests by using an appropriate tag "name", path/gateway, and other attributes, and how the device will structure its response (a byte array) to encodes tag values.
For basic cases such as Atomic tag types (e.g. DINT, REAL) - this is usually straightforward and you only need to use a single Data Accessor method to extract/write the value, furthermore these are usually common across devices and for tag names.
UDTs, STRINGs, BOOLs and arrays however, are non-trivial.
In general, you will need prior knowledge of how these tags are structured.
This knowledge needs to come from device manuals or from reverse engineering efforts.
The good: it handles a common case well!
In
Tag<M,T>
, theM
is aPlcMapper
which converts between a byte array (the tag buffer) and theT
.NET type and makes it possible to implement the Decode/Encode logic for a data type once, and then re-use it for 1D, 2D and 3D arrays as well as the atomic type.For this use-case it excels.
This system can even be used for UDTs with static sizes.
The value is strongly-typed, which holds significant value in the .NET ecosystem and particularly C#.
It is entirely possible that this serves the silent majority of users perfectly - they have no complaints, and we do not hear from them.
They have used the system as it was intended and moved on with their life.
It's utility stems from being able to instantiate a strongly-typed Tag, simply by providing a Mapper class type (and most of the time this is built-in).
And even this can be hidden because we also ship "simple" classes.
The complexity cliff
Tag<M,T>
makes a (somewhat implicit) promise that you as a consumer do not need to understand how PLC data is encoded in the buffer, as thePlcMapper
handles that for you.However, over time it has become clear that there are additional capabilities that PLCs offer, and that library consumers need, that the Mapper system doesn't fulfil.
At the time of development, the Mapper system was consistent with the core libplctag API, and was universally applicable (at least within the confines of libplctag.NET developers' limited exposure to other PLC vendors).
The key insight was that single values (whether they be atomic or UDTs) and arrays of values can use the same decoding logic.
The logical conclusion is that much of the setup of tags can be contained within
PlcMapper
classes, andTag<M,T>
merely needs to expose the Read/Write commands and events.This abstraction falls off a "cliff" as soon as you go outside the simplest of cases.
But the promise remains - the PlcMapper is meant to do it, and it does not - therefore it is a bug in the
PlcMapper
orTag<M,T>
.Ultimately, using libplctag does not preclude you (the library consumer) from understanding the device's Ethernet/IP API.
Consumers will encounter this working
MyBoolArray
andMyBoolArray[0]
return different data on Omron).If we want to continue to make the promise that consumers do not need to understand their device's Ethernet/IP API, the only path forward for this project is to wait for complaints to arrive, and incrementally add handlers for specific cases into the mappers.
The mappers would effectively become a documentation store for this knowledge.
Its another API to learn, support, test and maintain
The good news is that consumers are not locked into using the built-in mappers.
If they are able to understand how their devices behave, they can add custom mappers.
However, this is another API that consumers must learn.
The Mapper system uses some somewhat advanced C# language features which can be tricky for early-career developers to understand and implement.
If there are features that consumers want (such as value change detection), consumers could spend a considerable amount of time trying to determine how libplctag.NET supports it (which it may not), rather than just implementing it themself.
Furthermore:
It would be simpler if that layer was removed, and consumers directly interfaced with
Tag
- it confronts them with the complexity of the underlying API and does not hide any of the "foot-guns" (which exist whether they can see them or not).Many consumers still add their own wrappers
A study of applications or libraries that are consuming libplctag.NET shows that most will create yet another wrapper around
Tag<M,T>
.This implies that it is not the optimal level of abstraction and doesn't add much value.
Some examples:
What's the alternative?
Use an abstract base class and handle the specific data types with derived classes.
This has several advantages over the
Tag<M,T>
+PlcMapper
system:The issue with this is that there is not an obvious universal interface that would serve as the base class.
As an example, should the Tag value be exposed as a .NET type such as
int
or should it beobject
- or should it be exposed at all?What if the type was an array, and instead of exposing the entire array, the library consumer would prefer to force their application code to go through a method with an indexer (so that it can raise events or run other logic).
Summary
While
Tag<M,T>
has utility for a common use-case, the API it exposes is not the lowest-common denominator.Having it is a burden on library consumers and maintainers alike.
I recommend:
ITag
,Tag<M,T>
, all Mappers and Simple Tag classes, and also not supplying an alternative like an abstract base type.Tag<M,T>
and a demonstration of an alternative like an abstract base mapper type to the Examples folder.If this happens, the libplctag.NET would be closer to a "wrapper" in spirit, rather than what could be argued to be an opinionated framework for working with libplctag.
I would be happy to offer a separate Nuget package for the Mapper system, but not as an "official" package by the libplctag organization, instead released under a "timyhac" brand such as
timyhac.libplctag.Mappers
I've created this as a Github issue to open it up for community feedback. I realize that there are many users of the Mapper system at this point and I want to take into account the impact on them.
The text was updated successfully, but these errors were encountered: