Skip to content

Commit f6eb72e

Browse files
Copilotmarcschier
andauthored
docs: document three approaches for decoding ComplexTypes from OPC UA servers (#3664)
Document three approaches for working with custom OPC UA types in ComplexTypes.md Agent-Logs-Url: https://github.com/OPCFoundation/UA-.NETStandard/sessions/40780404-d86e-40c1-b099-e2400be745a4 Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@hotmail.com>
1 parent 64ca85f commit f6eb72e

1 file changed

Lines changed: 193 additions & 3 deletions

File tree

Docs/ComplexTypes.md

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,189 @@ if (StatusCode.IsGood(response.Results[0]))
164164
}
165165
```
166166

167+
## Approaches for Working with Custom Types
168+
169+
There are three ways to work with custom data types from an OPC UA server. The right approach depends on your use case.
170+
171+
### Approach 1: Hand-Written IEncodeable + EncodeableFactory Registration
172+
173+
If you have already implemented (or generated) a class that implements `IEncodeable`, you can register it with the session's `EncodeableFactory`. The stack will then automatically decode incoming `ExtensionObject` values into instances of your type.
174+
175+
#### Step 1 – Implement IEncodeable
176+
177+
```csharp
178+
// Required namespaces:
179+
// using Opc.Ua;
180+
// using System.Runtime.Serialization;
181+
182+
[DataContract(Namespace = "http://www.siemens.com/simatic-s7-opcua")]
183+
public class UDT_SemVer : IEncodeable, IJsonEncodeable
184+
{
185+
public UDT_SemVer() { Initialize(); }
186+
187+
[OnDeserializing]
188+
private void Initialize(StreamingContext context = default) { }
189+
190+
[DataMember(Name = "Major", IsRequired = true, Order = 1)]
191+
public byte Major { get; set; }
192+
193+
[DataMember(Name = "Minor", IsRequired = true, Order = 2)]
194+
public byte Minor { get; set; }
195+
196+
[DataMember(Name = "Patch", IsRequired = true, Order = 3)]
197+
public byte Patch { get; set; }
198+
199+
// TypeId must match the DataType NodeId on the server.
200+
// The "DT_" prefix is a Siemens S7 OPC UA convention for data type nodes;
201+
// the quoted name is the Siemens-specific identifier format.
202+
public ExpandedNodeId TypeId =>
203+
new ExpandedNodeId(
204+
"nsu=http://www.siemens.com/simatic-s7-opcua;s=DT_\"UDT_SemVer\"");
205+
206+
// BinaryEncodingId must match the Binary Encoding NodeId on the server.
207+
// The "TE_" prefix is the Siemens S7 OPC UA convention for type encoding nodes.
208+
public ExpandedNodeId BinaryEncodingId =>
209+
new ExpandedNodeId(
210+
"nsu=http://www.siemens.com/simatic-s7-opcua;s=TE_\"UDT_SemVer\"");
211+
212+
public ExpandedNodeId XmlEncodingId => ExpandedNodeId.Null;
213+
214+
// Return Null if the server does not expose a JSON encoding NodeId for this type.
215+
// If the server provides a JSON encoding node, set this to match it.
216+
public ExpandedNodeId JsonEncodingId => ExpandedNodeId.Null;
217+
218+
public void Encode(IEncoder encoder)
219+
{
220+
encoder.WriteByte("Major", Major);
221+
encoder.WriteByte("Minor", Minor);
222+
encoder.WriteByte("Patch", Patch);
223+
}
224+
225+
public void Decode(IDecoder decoder)
226+
{
227+
Major = decoder.ReadByte("Major");
228+
Minor = decoder.ReadByte("Minor");
229+
Patch = decoder.ReadByte("Patch");
230+
}
231+
232+
public bool IsEqual(IEncodeable encodeable) =>
233+
encodeable is UDT_SemVer other &&
234+
Major == other.Major && Minor == other.Minor && Patch == other.Patch;
235+
236+
public void EncodeJson(IJsonEncoder encoder)
237+
{
238+
encoder.WriteByte("Major", Major);
239+
encoder.WriteByte("Minor", Minor);
240+
encoder.WriteByte("Patch", Patch);
241+
}
242+
243+
public void DecodeJson(IJsonDecoder decoder)
244+
{
245+
Major = decoder.ReadByte("Major");
246+
Minor = decoder.ReadByte("Minor");
247+
Patch = decoder.ReadByte("Patch");
248+
}
249+
250+
public object Clone() => MemberwiseClone();
251+
}
252+
```
253+
254+
> **Important**: `BinaryEncodingId` must match the **Binary Encoding** NodeId
255+
> returned by the server (visible in the OPC UA address space as the
256+
> `DataTypeEncodingType` child node named `"Default Binary"`). This is what the
257+
> decoder uses to look up the registered type.
258+
259+
#### Step 2 – Register the Type Before Reading
260+
261+
Register the type with the session's factory **before** reading any values that use it. The simplest way is to add the `Type` directly:
262+
263+
```csharp
264+
// Create and connect the session
265+
var session = await Session.Create(...);
266+
267+
// Register the custom type with the session's encodeable factory
268+
session.Factory.Builder
269+
.AddEncodeableType(typeof(UDT_SemVer))
270+
.Commit();
271+
272+
// Now read – the stack decodes the ExtensionObject automatically
273+
DataValue result = await session.ReadValueAsync(
274+
"ns=3;s=\"H35dispenser\".\"ID\".\"ElectricalVersion\"");
275+
276+
if (result.WrappedValue.TryGet(out ExtensionObject extObject) &&
277+
extObject.Body is UDT_SemVer semVer)
278+
{
279+
Console.WriteLine($"Major={semVer.Major} Minor={semVer.Minor} Patch={semVer.Patch}");
280+
}
281+
```
282+
283+
You can also register all types from an entire assembly at once:
284+
285+
```csharp
286+
session.Factory.Builder
287+
.AddEncodeableTypes(typeof(UDT_SemVer).Assembly)
288+
.Commit();
289+
```
290+
291+
Or register multiple types individually in a single builder chain:
292+
293+
```csharp
294+
session.Factory.Builder
295+
.AddEncodeableType(typeof(UDT_SemVer))
296+
.AddEncodeableType(typeof(UDT_AnotherType))
297+
.Commit();
298+
```
299+
300+
> **Note**: `ComplexTypeSystem.LoadAsync()` is **not** required when using this
301+
> approach. If you call `LoadAsync()` after registering your own types, the
302+
> ComplexTypeSystem will skip types that are already registered in the factory.
303+
304+
### Approach 2: Source-Generated IEncodeable (Recommended for New Code)
305+
306+
Starting with version 1.6, you can annotate a POCO class with `[DataType]` and
307+
optional `[DataTypeField]` attributes and the OPC UA source generator will
308+
automatically generate the `IEncodeable` implementation for you. This eliminates
309+
the need to hand-write `Encode`, `Decode`, `IsEqual`, and `Clone` methods.
310+
311+
See [Source-Generated Data Types](SourceGeneratedDataTypes.md) for full details.
312+
313+
```csharp
314+
using Opc.Ua;
315+
316+
[DataType(Namespace = "http://www.siemens.com/simatic-s7-opcua",
317+
BinaryEncodingId = "s=TE_\"UDT_SemVer\"")]
318+
public partial class UDT_SemVer
319+
{
320+
[DataTypeField(Order = 1)]
321+
public byte Major { get; set; }
322+
323+
[DataTypeField(Order = 2)]
324+
public byte Minor { get; set; }
325+
326+
[DataTypeField(Order = 3)]
327+
public byte Patch { get; set; }
328+
}
329+
```
330+
331+
After adding the source generator NuGet package, the generated registration
332+
extension method can be used:
333+
334+
```csharp
335+
session.Factory.Builder
336+
.AddMyNamespaceDataTypes()
337+
.Commit();
338+
```
339+
340+
### Approach 3: Runtime IStructure (No Pre-defined Types Required)
341+
342+
If you do not know the type structure at compile time, or prefer not to create
343+
.NET types for every server type, use `ComplexTypeSystem` to load the type
344+
definitions at runtime and access fields via the `IStructure` interface. This
345+
requires no hand-written types.
346+
347+
See the [Basic Usage](#basic-usage) and [Advanced Usage](#advanced-usage)
348+
sections below for examples.
349+
167350
## Advanced Usage
168351

169352
### Loading Specific Types
@@ -542,9 +725,15 @@ await complexTypeSystem.LoadAsync();
542725

543726
**Solutions**:
544727

545-
- Ensure `LoadAsync()` was called before reading the value
546-
- Verify the type is actually loaded: `complexTypeSystem.GetDefinedTypes()`
547-
- Check if the server's type definition is complete and valid
728+
- If using `ComplexTypeSystem`: ensure `LoadAsync()` was called before reading the value,
729+
verify the type is actually loaded with `complexTypeSystem.GetDefinedTypes()`,
730+
and check if the server's type definition is complete and valid.
731+
- If using a hand-written `IEncodeable` (Approach 1): ensure the type is registered with
732+
`session.Factory.Builder.AddEncodeableType(typeof(YourType)).Commit()` **before** reading.
733+
The `BinaryEncodingId` on your type must exactly match the Binary Encoding NodeId reported
734+
by the server — use a generic OPC UA client (e.g. UA Expert) to inspect the correct NodeId.
735+
- If using source-generated types (Approach 2): ensure the generated `Add...DataTypes()`
736+
extension method is called on `session.Factory.Builder` before reading.
548737

549738
### Performance Issues
550739

@@ -662,6 +851,7 @@ TypeInfo TypeInfo { get; } // Type info of the field
662851

663852
## See Also
664853

854+
- [Source-Generated Data Types](SourceGeneratedDataTypes.md) - Auto-generate IEncodeable implementations from annotated POCO classes
665855
- [Platform Build Documentation](PlatformBuild.md) - Information about building and versioning
666856
- [Observability Documentation](Observability.md) - Information about logging and telemetry
667857
- [Console Reference Client](../Applications/ConsoleReferenceClient/README.md) - Example client implementation

0 commit comments

Comments
 (0)