# Source-Generated Data Types The OPC UA .NET Standard stack includes a C# source generator that automatically implements the `IEncodeable` interface for annotated POCO classes and enums. This eliminates the need to hand-write `Encode`, `Decode`, `IsEqual`, and `Clone` methods for custom OPC UA data types. ## Contents - [Quick Start](#quick-start) - [Prerequisites](#prerequisites) - [Project Reference](#project-reference-internal-development) - [NuGet Package](#nuget-package-external-consumers) - [The `DataType` Attribute](#the-datatype-attribute) - [Properties](#properties) - [Namespace Resolution Order](#namespace-resolution-order) - [The `DataTypeField` Attribute](#the-datatypefield-attribute) - [Properties](#properties-1) - [Field Selection Rules](#field-selection-rules) - [Supported Property Types](#supported-property-types) - [Collections](#collections) - [Enums and `IEncodeable` Types](#enums-and-iencodeable-types) - [Unsupported Types](#unsupported-types) - [Class Variants](#class-variants) - [Regular Partial Class](#regular-partial-class) - [Sealed Partial Class](#sealed-partial-class) - [Record Class](#record-class) - [Derived Class](#derived-class-inheritance) - [Internal Class](#internal-class) - [Enum Support](#enum-support) - [Registering Types with the Encodeable Factory](#registering-types-with-the-encodeable-factory) - [Complete Example](#complete-example) - [`StructureHandling` Enum](#structurehandling-enum) - [`DefaultValueHandling` Enum](#defaultvaluehandling-enum) - [How It Works](#how-it-works) - [Example: Configuration with Defaults](#example-configuration-with-defaults) - [Partial Init Properties](#partial-init-properties) - [Example](#example) - [How It Works](#how-it-works-1) - [When to Use](#when-to-use) - [Requirements and Constraints](#requirements-and-constraints) - [Generated File Output](#generated-file-output) - [MSBuild Configuration](#msbuild-configuration) ## Quick Start 1. Mark your class as `partial` and decorate it with `[DataType]`. 2. Add public properties for each field — the generator does the rest. ```csharp using Opc.Ua; namespace MyApp.Configuration { [DataType(Namespace = "urn:mycompany:myapp")] public partial class DeviceConfiguration { public string Name { get; set; } public int Port { get; set; } public bool Enabled { get; set; } } } ``` The source generator will produce a partial class file that implements `IEncodeable`, including: - `TypeId`, `BinaryEncodingId`, `XmlEncodingId` properties - `Encode(IEncoder)` and `Decode(IDecoder)` methods - `IsEqual(IEncodeable)` for value comparison - `Clone()` for deep copy - An activator class (e.g. `DeviceConfigurationActivator`) for type registration ## Prerequisites ### Project Reference (internal development) When developing within the OPC UA .NET Standard repository, reference the source generator project and import its props file: ```xml Analyzer false ``` ### NuGet Package (external consumers) Add the `OPCFoundation.NetStandard.Opc.Ua.SourceGeneration` package: ```bash dotnet add package OPCFoundation.NetStandard.Opc.Ua.SourceGeneration ``` The package is a development dependency, so the command adds a reference that runs the generator at build time and does not flow to consumers of your project: ```xml runtime; build; native; contentfiles; analyzers; buildtransitive all ``` ## The `[DataType]` Attribute Applied to a `partial class`, `partial record class`, or an `enum` to opt in to source generation. ```csharp [AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] public sealed class DataTypeAttribute : Attribute { public string? Namespace { get; set; } public string? DataTypeId { get; set; } public string? BinaryEncodingId { get; set; } public string? XmlEncodingId { get; set; } } ``` ### Properties | Property | Description | |---|---| | `Namespace` | The OPC UA namespace URI for this type. If omitted, falls back to `[DataContract(Namespace=...)]` if present, or `urn:`. | | `DataTypeId` | Node ID string for the data type (e.g. `"i=12345"`, `"s=MyType"`, `"g="`). Must be prefixed with `i=`, `s=`, `g=`, or `b=`. If omitted, defaults to a string identifier with the class name (`"s=DeviceConfiguration"`). | | `BinaryEncodingId` | Optional binary encoding Node ID. Same prefix rules. | | `XmlEncodingId` | Optional XML encoding Node ID. Same prefix rules. | ### Namespace Resolution Order The generator resolves the OPC UA namespace URI in this order: 1. `[DataType(Namespace = "...")]` — explicitly specified 2. `[DataContract(Namespace = "...")]` — from `System.Runtime.Serialization` 3. `urn:` — automatic fallback (e.g. `urn:myapp.configuration`) An explicit `Namespace` expression must be resolvable from the source compilation before generators run. Use a string literal or a `const string` declared in ordinary source or a referenced assembly. Do not reference a `Namespaces` constant emitted from a model file by the same generator run; generated symbols are not available while `[DataType]` attributes are analyzed. An unresolved explicit expression produces `MODELGEN021` instead of silently falling back to a different namespace. ## The `[DataTypeField]` Attribute Applied to properties to control which fields participate in encoding and in what order. ```csharp [AttributeUsage(AttributeTargets.Property)] public sealed class DataTypeFieldAttribute : Attribute { public int Order { get; set; } public string? Name { get; set; } public StructureHandling StructureHandling { get; set; } public DefaultValueHandling DefaultValueHandling { get; set; } public bool IsRequired { get; set; } } ``` ### Properties | Property | Description | |---|---| | `Order` | The encoding/decoding order of this field. Fields are sorted by `Order` before code generation. | | `Name` | The serialized field name. Defaults to the property name if not set. | | `StructureHandling` | For `IEncodeable`-typed fields: controls the encoding strategy. See [StructureHandling](#structurehandling-enum). | | `DefaultValueHandling` | Controls how default values are handled during encode/decode. See [DefaultValueHandling](#defaultvaluehandling-enum). | | `IsRequired` | Indicates whether the field is required. Reserved for future use with optional-field structures. | ### Field Selection Rules - **If any property has `[DataTypeField]`**: only annotated properties are included in encoding. Non-annotated properties are excluded. - **If no properties have `[DataTypeField]`**: all public read-write properties are automatically included, ordered by declaration position. This lets you mix serialized and non-serialized properties in the same class: ```csharp [DataType(Namespace = "urn:myapp")] public partial class ServerSettings { [DataTypeField(Order = 0)] public string EndpointUrl { get; set; } [DataTypeField(Order = 1, Name = "session_timeout")] public uint SessionTimeout { get; set; } // Not annotated — excluded from encoding public string InternalNote { get; set; } } ``` ## Supported Property Types The source generator supports the following property types: | C# Type | OPC UA Type | Encoder Method | |---|---|---| | `bool` | Boolean | `WriteBoolean` | | `sbyte` | SByte | `WriteSByte` | | `byte` | Byte | `WriteByte` | | `short` / `Int16` | Int16 | `WriteInt16` | | `ushort` / `UInt16` | UInt16 | `WriteUInt16` | | `int` / `Int32` | Int32 | `WriteInt32` | | `uint` / `UInt32` | UInt32 | `WriteUInt32` | | `long` / `Int64` | Int64 | `WriteInt64` | | `ulong` / `UInt64` | UInt64 | `WriteUInt64` | | `float` / `Single` | Float | `WriteFloat` | | `double` / `Double` | Double | `WriteDouble` | | `string` | String | `WriteString` | | `DateTime` | DateTime | `WriteDateTime` | | `Guid` / `Uuid` | Guid | `WriteGuid` | | `ByteString` | ByteString | `WriteByteString` | | `NodeId` | NodeId | `WriteNodeId` | | `ExpandedNodeId` | ExpandedNodeId | `WriteExpandedNodeId` | | `StatusCode` | StatusCode | `WriteStatusCode` | | `QualifiedName` | QualifiedName | `WriteQualifiedName` | | `LocalizedText` | LocalizedText | `WriteLocalizedText` | | `ExtensionObject` | ExtensionObject | `WriteExtensionObject` | | `DataValue` | DataValue | `WriteDataValue` | | `Variant` | Variant | `WriteVariant` | | `DiagnosticInfo` | DiagnosticInfo | `WriteDiagnosticInfo` | | `XmlElement` | XmlElement | `WriteXmlElement` | ### Collections - **`ArrayOf`** — use for arrays of any supported type - **`MatrixOf`** — use for multi-dimensional arrays ### Enums and IEncodeable Types - Any C# `enum` type (encoded with `WriteEnumerated`/`ReadEnumerated`) - Any type implementing `IEncodeable` or decorated with `[DataType]` ### Unsupported Types Properties with types not in the above list will produce: - A **warning** if the property is not annotated with `[DataTypeField]` (the property is silently excluded) - An **error** if the property is annotated with `[DataTypeField]` (the type will not generate) ## Class Variants The generator handles several class shapes: ### Regular Partial Class ```csharp [DataType(Namespace = "urn:myapp")] public partial class MyConfig { public string Name { get; set; } public int Value { get; set; } } ``` Generates `virtual` methods for `Encode`, `Decode`, `IsEqual`, and `Clone`. ### Sealed Partial Class ```csharp [DataType(Namespace = "urn:myapp")] public sealed partial class MyConfig { public string Name { get; set; } } ``` Generates non-virtual methods (no `virtual` or `override` keywords). ### Record Class ```csharp [DataType(Namespace = Namespaces.OpcUaXsd)] public partial record class BrowserOptions { [DataTypeField(Order = 0)] public RequestHeader? RequestHeader { get; set; } [DataTypeField(Order = 1)] public uint MaxReferencesReturned { get; set; } } ``` For record types, `Clone()` uses `this with { }` and `IsEqual` delegates to the record's `Equals` implementation. ### Derived Class (Inheritance) When a `[DataType]` class derives from another `IEncodeable` base type, the generator uses `override` instead of `virtual` and calls `base.Encode()`/ `base.Decode()` before encoding the derived fields. `Clone()` delegates to a generated `MemberwiseClone()` that starts from a shallow copy of the whole object — carrying the inherited fields — and then deep copies the derived class's own reference-typed fields, the same shape the model-based generator emits. ### Internal Class Classes declared as `internal` produce `internal` generated members. ## Enum Support Annotate an enum with `[DataType]` to register it as an OPC UA enumerated type: ```csharp [DataType(Namespace = "urn:myapp")] public enum DeviceStatus { Unknown = 0, Online = 1, Offline = 2, Error = 3 } ``` The generator produces an `EnumeratedType` registration and an `EnumDefinition` factory. The enum does **not** need to be `partial`. ## Registering Types with the Encodeable Factory The source generator creates an extension method to register all `[DataType]` types in a namespace with an `IEncodeableFactoryBuilder`. The method is named `AddDataTypes()`: ```csharp // Generated extension method name is derived from the .NET namespace: // For namespace "MyApp.Configuration" → AddMyAppConfigurationDataTypes() MessageContext.Factory.Builder .AddMyAppConfigurationDataTypes() .Commit(); ``` For example, types in `Opc.Ua.Gds.Server` produce a method named `AddOpcUaGdsServerDataTypes()`: ```csharp protected override void OnServerStarting( ApplicationConfiguration configuration) { base.OnServerStarting(configuration); MessageContext.Factory.Builder .AddOpcUaGdsServerDataTypes() .Commit(); } ``` ## Complete Example This example shows a node manager configuration type used in the MemoryBuffer quickstart server: ```csharp using Opc.Ua; namespace MemoryBuffer { [DataType(Namespace = Namespaces.MemoryBuffer)] public partial class MemoryBufferConfiguration { public MemoryBufferConfiguration() { } [DataTypeField(Order = 1, StructureHandling = StructureHandling.Inline)] public ArrayOf Buffers { get; set; } } [DataType(Namespace = Namespaces.MemoryBuffer)] public partial class MemoryBufferInstance { public MemoryBufferInstance() { } [DataTypeField(Order = 1)] public string Name { get; set; } [DataTypeField(Order = 2)] public int TagCount { get; set; } [DataTypeField(Order = 3)] public string DataType { get; set; } } } ``` Key points in this example: - Both classes are `partial` and have parameterless constructors - `StructureHandling = StructureHandling.Inline` on `Buffers` forces exact-type encoding (rather than extension object wrapping) since the element type is known at compile time - `Order` controls the encoding sequence ## `StructureHandling` Enum When a property's type implements `IEncodeable`, the generator must choose between two encoding strategies: | Strategy | Method | Use When | |---|---|---| | Exact type | `WriteEncodeable` / `ReadEncodeable` | The concrete type is always known (sealed, no subtypes) | | Extension Object | `WriteEncodeableAsExtensionObject` / `ReadEncodeableAsExtensionObject` | Subtypes may be substituted at runtime | The `StructureHandling` enum controls this choice: ```csharp public enum StructureHandling { Auto = 0, // Generator decides based on type analysis Inline = 1, // Force WriteEncodeable / ReadEncodeable ExtensionObject = 2 // Force WriteEncodeableAsExtensionObject } ``` | Value | Behavior | |---|---| | `Auto` (default) | If the field type is `sealed` and does not derive from another `IEncodeable`, `WriteEncodeable` is used. Otherwise, `WriteEncodeableAsExtensionObject` is used. | | `Inline` | Forces `WriteEncodeable`/`ReadEncodeable` — the exact type is written inline. Use when the concrete type is always known at compile time. | | `ExtensionObject` | Forces `WriteEncodeableAsExtensionObject`/`ReadEncodeableAsExtensionObject` — the value is wrapped in an `ExtensionObject`. Use when subtypes may be substituted at runtime. | Example: ```csharp [DataType(Namespace = "urn:myapp")] public partial class MyConfig { // Inline: always encodes as EndpointDescription directly [DataTypeField(Order = 0, StructureHandling = StructureHandling.Inline)] public EndpointDescription Endpoint { get; set; } // Auto (default): generator decides based on whether the type is sealed [DataTypeField(Order = 1)] public OperationLimits Limits { get; set; } } ``` ## `DefaultValueHandling` Enum Controls how default values are handled during encode and decode. This is particularly important for configuration types where constructor defaults (e.g., `NonceLength = 32`, `RejectSHA1SignedCertificates = true`) should be preserved when the field is absent from an XML configuration file. ```csharp [Flags] public enum DefaultValueHandling { Exclude = 0, // Omit type defaults on write, keep declared default for missing XML Emit = 1, // Always write, even if default value SetIfMissing = 2, // Always set on read, even if absent Include = Emit | SetIfMissing // Always write AND read (legacy behavior) } ``` OPC 10000-6 (5.3.5, 5.4.1, 5.4.2.1, 5.4.7) defines a missing field as the default value of its *type* (Table 1: `0`, `false`, `null`, ...), not a per-field default. The generated code follows that rule for JSON and keeps a declared-default leniency only for XML decoding: | Value | Encode Behavior | Decode Behavior | |---|---|---| | `Exclude` (default) | Omits the field from XML/Compact JSON only if its value is the type default *and* the declared default (initializer) is the type default too; a field with any other declared default is always written. Binary and Verbose JSON always write. | JSON: a missing field is the type default. XML: a missing field keeps the declared (constructor/initializer) default, so configuration files that predate a field still load. Binary always reads. | | `Emit` | Always writes the field, even if default. | Same as `Exclude` for decode. | | `SetIfMissing` | Omits the field from XML/Compact JSON if its value is the type default. | A missing field is the type default in every encoding (overwrites the declared default). | | `Include` | Always writes. | Always reads and assigns. | Note that the Compact `JsonEncoder` drops a type-default value by itself even when the generated code writes it; since a missing JSON field decodes to the type default, e.g. `int Port = 4840` set to `0` still round trips as `0`. The `XmlEncoder` never writes a `null` reference (string, ByteString, ...), so a reference field with a non-null declared default that is set to `null` reads back from XML as its declared default. Use `SetIfMissing` or `Include` when that matters. ### How It Works The generated code uses two `IEncoder`/`IDecoder` APIs: - **`IEncoder.CanOmitFields`**: Returns `true` for XML and for the Compact JSON encoding, `false` for Binary and for the Verbose (and RawData) JSON encoding, which include all fields (OPC 10000-6 5.4.1). Used by the encode guard. - **`IDecoder.HasField(string)`**: Returns `true` if the field exists in the encoded data. Always `true` for Binary. Checks element/property existence for XML/JSON. Generated encode (when `Emit` is NOT set and the declared default is the type default, or `SetIfMissing` is set): ```csharp if (!encoder.CanOmitFields || Count != 0) encoder.WriteInt32("Count", Count); ``` Generated decode (when `SetIfMissing` is NOT set): ```csharp if (decoder.EncodingType != EncodingType.Xml || decoder.HasField("NonceLength")) NonceLength = decoder.ReadInt32("NonceLength"); ``` ### Example: Configuration with Defaults ```csharp [DataType(Namespace = "urn:myapp")] public partial class SecuritySettings { public SecuritySettings() { NonceLength = 32; RejectExpiredCertificates = true; } // Exclude (default): always written (32 is not the type default); // a field missing from XML keeps 32, a field missing from JSON is 0 [DataTypeField(Order = 0)] public int NonceLength { get; set; } = 32; // Include: always write and read — use for bool fields that default // to true, since false (the type default) is a valid explicit value [DataTypeField(Order = 1, DefaultValueHandling = DefaultValueHandling.Include)] public bool RejectExpiredCertificates { get; set; } // Exclude (default): omit false from XML, preserve false when absent [DataTypeField(Order = 2)] public bool AllowAnonymous { get; set; } } ``` > **Guideline**: Use `DefaultValueHandling.Include` for bool properties that > default to `true` in the constructor. Since `false` is the type default, > `Exclude` would omit an explicit `false` on encode. `Include` ensures the > value always round-trips correctly. ## Partial Init Properties The source generator supports `partial` properties with `init` accessors for immutable record types. This enables record classes with init-only properties to participate in OPC UA binary and XML encoding — the generator creates a private backing field for each such property and assigns to it directly during `Decode()`, bypassing the init-only constraint. ### Example ```csharp [DataType(Namespace = "urn:mycompany:myapp")] public partial record class DeviceConfig { [DataTypeField(Order = 0)] public partial string Name { get; init; } = "Default"; [DataTypeField(Order = 1)] public partial int Port { get; init; } } ``` The generator produces: ```csharp partial record class DeviceConfig : IEncodeable { private string __Name = "Default"; public partial string Name { get => __Name; init => __Name = value; } private int __Port; public partial int Port { get => __Port; init => __Port = value; } public virtual void Decode(IDecoder decoder) { // Assigns to backing field, bypassing init constraint if (decoder.HasField("Name")) __Name = decoder.ReadString("Name"); if (decoder.HasField("Port")) __Port = decoder.ReadInt32("Port"); } // ... } ``` ### How It Works 1. For each property declared as `partial` with an `init` accessor, the generator emits a private backing field named `__`. 2. The partial property implementation delegates `get` and `init` to that backing field. 3. `Decode()` assigns to the backing field directly, which is legal because the field is a regular mutable field — only the public `init` accessor is restricted to object-initializer contexts. 4. `Clone()` for record types uses `this with { }`, which copies init-only properties automatically via the copy constructor. ### When to Use - Use `partial` + `init` properties when you want an immutable public API (callers can only set values at construction time) while still allowing the decoder to populate the object from a binary or XML stream. - This is especially useful for configuration and options types modeled as `record class`. ## Requirements and Constraints 1. **Must be `partial`**: The class must be declared `partial` so the generator can extend it. Non-partial classes produce a compile error. 2. **Parameterless constructor**: The class must have a parameterless constructor (either explicit or implicit). 3. **Read-write properties**: Only properties with both getter and setter are considered. Read-only properties are ignored. 4. **One `[DataType]` per type**: `AllowMultiple = false` — each type can have at most one `[DataType]` attribute. 5. **Unique identifiers**: `DataTypeId` must be unique across all data types in the same namespace. For open namespaces, using a random GUID (`g=`) is recommended. ## Generated File Output The generator produces one `.g.cs` file per .NET namespace. The file name follows the pattern `.Types.g.cs`. For example, types in `Opc.Ua.Gds.Server` produce a file named `OpcUaGdsServer.Types.g.cs` containing: - All partial class bodies (IEncodeable implementation) - Activator classes for each type - A single extension class with the `Add...DataTypes()` registration method ## MSBuild Configuration The source generator supports the following MSBuild properties via the `OPCFoundation.Opc.Ua.SourceGeneration.props` file: | Property | Description | |---|---| | `ModelSourceGeneratorPublicDataTypeExtensions` | When `true`, the generated extension methods and their containing static class are `public` instead of `internal`. |