# 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`. |