@@ -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