Skip to content

Commit 34ed571

Browse files
marcschierCopilot
andauthored
Add structure-backed OptionSet DataType support in ComplexTypeSystem (#3672)
Concrete sub-types of the abstract OptionSet DataType (i=12755, e.g. AccessRights, CarExtras, OptionSetBase) were silently skipped by ComplexTypeSystem.LoadBaseStructureDataTypesAsync when only an EnumDefinition or OptionSetValues property was available. This change registers them with a new runtime class Opc.Ua.Encoders.OptionSet that reuses the generated Opc.Ua.OptionSet wire format (Value/ValidBits ByteStrings) and self-registers via IEncodeableType. Key changes: - New Stack/Opc.Ua/Types/OptionSet.cs with EnumDefinition-driven bit accessors (by name and index), GetSetFieldNames helper, and a fixed ByteLength derived from the highest declared bit per OPC UA Part 3 paragraph 8.40 / 3.2.8. SetBit throws ArgumentOutOfRangeException outside that range and always materializes Value/ValidBits to the fixed length. - New AddOptionSetType entry point on IComplexTypeBuilder (source-breaking addition for custom implementations); DefaultComplexTypeBuilder implements it. The Reflection.Emit ComplexTypeBuilder throws NotSupportedException. - ComplexTypeSystem detects OptionSet descendants via a HasSubtype walk and resolves the EnumDefinition from DataTypeDefinition or the OptionSetValues property fallback before the old skip path. - Removed stale V1.04 OptionSet limitation remark from ComplexTypeSystem. - 10 unit tests in Tests/Opc.Ua.Client.Tests/ComplexTypes/DefaultOptionSetTests.cs covering factory registration, bit accessors, binary round-trip via ExtensionObject, hand-crafted Part 6 wire format, Clone/CreateInstance semantics, and the ByteLength invariant. - Live-verified against opc.tcp://DESKTOP-BGFJS91:48010 (UA C++ SDK demo): AccessRights/CarExtras/OptionSetBase now appear in the registered custom-type list; no skip-log entries; LoadAsync(throwOnError:true) returns true. - Docs/ComplexTypes.md Known Limitations reworded; Docs/MigrationGuide.md gains an OptionSet DataType support subsection. UInteger-backed OptionSet DataTypes remain intentionally opaque (carried as their unsigned integer in a Variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6c8c1a6 commit 34ed571

16 files changed

Lines changed: 922 additions & 22 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ DocProject/Help/html
145145

146146
# Click-Once directory
147147
publish/
148+
publish-aot/
148149

149150
# Publish Web Output
150151
*.[Pp]ublish.xml

Applications/ConsoleReferenceClient/ClientSamples.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
using System.Collections;
3232
using System.Collections.Generic;
3333
using System.Diagnostics;
34-
using System.Diagnostics.CodeAnalysis;
3534
using System.IO;
3635
using System.Linq;
3736
using System.Text;
@@ -1109,7 +1108,7 @@ public async Task LoadTypeSystemAsync(ComplexTypeSystem complexTypeSystem, Cance
11091108
var stopWatch = new Stopwatch();
11101109
stopWatch.Start();
11111110

1112-
await complexTypeSystem.LoadAsync(throwOnError: true, ct: ct).ConfigureAwait(false);
1111+
bool loaded = await complexTypeSystem.LoadAsync(throwOnError: true, ct: ct).ConfigureAwait(false);
11131112

11141113
stopWatch.Stop();
11151114

@@ -1118,6 +1117,13 @@ public async Task LoadTypeSystemAsync(ComplexTypeSystem complexTypeSystem, Cance
11181117
complexTypeSystem.GetDefinedTypes().Count,
11191118
stopWatch.ElapsedMilliseconds);
11201119

1120+
if (!loaded)
1121+
{
1122+
throw new ServiceResultException(
1123+
StatusCodes.BadTypeMismatch,
1124+
"ComplexTypeSystem.LoadAsync did not load all custom types.");
1125+
}
1126+
11211127
if (m_verbose)
11221128
{
11231129
m_logger.LogInformation("Custom types defined for this session:");
@@ -1566,10 +1572,6 @@ private static ArrayOf<BrowseDescription> CreateBrowseDescriptionCollectionFromN
15661572
/// <param name="session">The session to use for exporting.</param>
15671573
/// <param name="nodes">The list of nodes to export.</param>
15681574
/// <param name="filePath">The path where the NodeSet2 XML file will be saved.</param>
1569-
[RequiresUnreferencedCode(
1570-
"Uses XmlSerializer which requires unreferenced code.")]
1571-
[RequiresDynamicCode(
1572-
"Uses XmlSerializer which requires unreferenced code.")]
15731575
public void ExportNodesToNodeSet2(ISession session, IList<INode> nodes, string filePath)
15741576
{
15751577
m_logger.LogInformation("Exporting {Count} nodes to {FilePath}...", nodes.Count, filePath);
@@ -1606,10 +1608,6 @@ public void ExportNodesToNodeSet2(ISession session, IList<INode> nodes, string f
16061608
/// <returns>A dictionary mapping namespace URI to the file path of the exported NodeSet2 file.</returns>
16071609
/// <exception cref="ArgumentNullException">Thrown when session, nodes, or outputDirectory is null.</exception>
16081610
/// <exception cref="ArgumentException">Thrown when outputDirectory is empty or whitespace.</exception>
1609-
[RequiresUnreferencedCode(
1610-
"Uses XmlSerializer which requires unreferenced code.")]
1611-
[RequiresDynamicCode(
1612-
"Uses XmlSerializer which requires unreferenced code.")]
16131611
public async Task<IReadOnlyDictionary<string, string>> ExportNodesToNodeSet2PerNamespaceAsync(
16141612
ISession session,
16151613
IList<INode> nodes,

Applications/ConsoleReferenceClient/ConsoleReferenceClient.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
<PackageReference Include="System.CommandLine" />
2121
<PackageReference Include="Serilog" />
2222
<PackageReference Include="Serilog.Extensions.Logging" />
23-
<PackageReference Include="Serilog.Expressions" />
2423
<PackageReference Include="Serilog.Sinks.Console" />
2524
<PackageReference Include="Serilog.Sinks.File" />
2625
<PackageReference Include="Serilog.Sinks.Debug" />

Applications/ConsoleReferenceClient/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,11 @@ r is VariableNode v &&
526526

527527
if (exportNodes)
528528
{
529-
await samples
530-
.ExportNodesToNodeSet2PerNamespaceAsync(uaClient.Session, allNodes, Environment.CurrentDirectory, cancellationToken)
529+
await samples.ExportNodesToNodeSet2PerNamespaceAsync(
530+
uaClient.Session,
531+
allNodes,
532+
Environment.CurrentDirectory,
533+
cancellationToken)
531534
.ConfigureAwait(false);
532535
}
533536
}

Applications/ConsoleReferencePublisher/ConsoleReferencePublisher.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
<PackageReference Include="System.Private.Uri" />
2424
<PackageReference Include="Serilog" />
2525
<PackageReference Include="Serilog.Extensions.Logging" />
26-
<PackageReference Include="Serilog.Expressions" />
2726
<PackageReference Include="Serilog.Sinks.Console" />
2827
<PackageReference Include="Serilog.Sinks.File" />
2928
<PackageReference Include="Serilog.Sinks.Debug" />

Applications/ConsoleReferenceServer/ConsoleReferenceServer.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
3030
<PackageReference Include="System.CommandLine" />
3131
<PackageReference Include="Serilog" />
32-
<PackageReference Include="Serilog.Expressions" />
3332
<PackageReference Include="Serilog.Sinks.Console" />
3433
<PackageReference Include="Serilog.Extensions.Logging" />
3534
<PackageReference Include="Serilog.Sinks.File" />

Applications/ConsoleReferenceServer/ConsoleUtils.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
using Opc.Ua;
4141
using Serilog;
4242
using Serilog.Events;
43-
using Serilog.Templates;
4443
#if NET5_0_OR_GREATER
4544
using Microsoft.Extensions.Configuration;
4645
#endif
@@ -176,9 +175,8 @@ public void ConfigureLogging(
176175
if (!string.IsNullOrWhiteSpace(outputFilePath))
177176
{
178177
loggerConfiguration.WriteTo.File(
179-
new ExpressionTemplate(
180-
"{UtcDateTime(@t):yyyy-MM-dd HH:mm:ss.fff} [{@l:u3}] {@m}\n{@x}"),
181178
Utils.ReplaceSpecialFolderNames(outputFilePath),
179+
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
182180
restrictedToMinimumLevel: (LogEventLevel)fileLevel,
183181
rollOnFileSizeLimit: true
184182
);

Applications/ConsoleReferenceSubscriber/ConsoleReferenceSubscriber.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
<PackageReference Include="System.Private.Uri" />
2424
<PackageReference Include="Serilog" />
2525
<PackageReference Include="Serilog.Extensions.Logging" />
26-
<PackageReference Include="Serilog.Expressions" />
2726
<PackageReference Include="Serilog.Sinks.Console" />
2827
<PackageReference Include="Serilog.Sinks.File" />
2928
<PackageReference Include="Serilog.Sinks.Debug" />

Docs/ComplexTypes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,9 @@ TypeInfo TypeInfo { get; } // Type info of the field
837837

838838
## Known Limitations
839839

840-
1. **OptionSet Support**: OPC UA 1.04 OptionSet types do not automatically create enumeration flags
840+
1. **OptionSet Support**: Concrete Structure-backed sub-types of the abstract `OptionSet` DataType (`i=12755`, e.g. `AccessRights`, `CarExtras`) are automatically registered by the default `ComplexTypeSystem` builder as `Opc.Ua.Encoders.OptionSet` runtime instances, driven by either the `EnumDefinition` carried in `DataTypeDefinition` or a fallback synthesized from the `OptionSetValues` property. The runtime class exposes the two canonical `Value` / `ValidBits` ByteStrings plus bit accessors keyed by field name or bit index. Per Part 3 §8.40 / §3.2.8, the overall ByteString length is fixed by the sub-type's declared bits (exposed as `ByteLength`); setting a bit outside that range throws `ArgumentOutOfRangeException`. Remaining limitations:
841+
- UInteger-backed OptionSet DataTypes (DataTypes deriving from an unsigned integer with `IsOptionSet=true`) continue to be represented as their underlying unsigned integer in a `Variant` — no per-bit metadata is surfaced.
842+
- The legacy Reflection.Emit builder in `Opc.Ua.Client.ComplexTypes` throws `NotSupportedException` for OptionSet sub-types; switch to the default builder (`new ComplexTypeSystem(session)`) for OptionSet support.
841843
2. **Legacy Dictionary Support**: Some OPC UA 1.03 structured types that cannot be mapped to OPC UA 1.04 definitions are ignored
842844
3. **Type Modifications**: Once loaded, types cannot be dynamically updated during a session. Reconnect to reload modified types.
843845

Docs/MigrationGuide.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
- [ExtensionObject array helpers changed](#extensionobject-array-helpers-changed)
4444
- [Complex Types](#complex-types)
4545
- [ComplexTypes moved to Opc.Ua.Client assembly](#complextypes-moved-to-opcuaclient-assembly)
46+
- [OptionSet DataType support](#optionset-datatype-support)
4647
- [Other Breaking Changes](#other-breaking-changes)
4748
- [Migrating from 1.05.377 to 1.05.378](#migrating-from-105377-to-105378)
4849
- [Asynchronous as default](#asynchronous-as-default)
@@ -616,6 +617,17 @@ Core complex type interfaces and default (non-reflection-emit) implementations m
616617
Namespace remains `Opc.Ua.Client.ComplexTypes`. If you used the default constructors without specifying the builder, and want to use the Reflection.Emit based type builders,
617618
you need to change your code to call `ComplexTypeSystem.Create(...)` instead of `new ComplexTypeSystem(...)` which now uses the new default builder not supporting Reflection.Emit.
618619

620+
#### OptionSet DataType support
621+
622+
Concrete Structure-backed sub-types of the abstract `OptionSet` DataType (`i=12755`) are now automatically registered by the default `ComplexTypeSystem` builder with a new runtime class `Opc.Ua.Encoders.OptionSet` (in `Stack/Opc.Ua.Types`). Bit-field metadata is resolved from `DataTypeDefinition` (`EnumDefinition`) or, as a fallback, synthesized from the `OptionSetValues` property (`LocalizedText[]`).
623+
624+
Impact on existing code:
625+
626+
- **Source-breaking for custom `IComplexTypeBuilder` implementations**: a new member `AddOptionSetType(QualifiedName, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, EnumDefinition)` was added to `IComplexTypeBuilder`. Custom implementations must provide it.
627+
- The Reflection.Emit builder in `Opc.Ua.Client.ComplexTypes` throws `NotSupportedException` from `AddOptionSetType`; callers relying on the Reflection.Emit path for OptionSet sub-types should switch to the default builder (`new ComplexTypeSystem(session)`).
628+
- No wire-format changes: encoders/decoders continue to route through `IEncodeableFactory``IEncodeableType.CreateInstance`, which now yields `Opc.Ua.Encoders.OptionSet` for registered sub-types.
629+
- UInteger-backed OptionSet DataTypes remain treated as their underlying unsigned integer in a `Variant` (unchanged).
630+
619631
### Other Breaking Changes
620632

621633
- **Session/Browser state format**: Persistence switched from `DataContractSerializer` XML to `BinaryEncoder`. Delete old persisted files and re-establish sessions.

0 commit comments

Comments
 (0)