Skip to content

ID Converters

This page covers the serialization and type-conversion infrastructure generated for typed IDs.

Overview

Every typed ID gets a System.ComponentModel.TypeConverter automatically. Additional converters are opt-in via the IdConverters flags enum:

[TypedId(Converters = IdConverters.JsonConverter | IdConverters.EfCoreValueConverter)]
public readonly partial struct CustomerId;
Flag Generated Class Purpose
(always) {TypeName}TypeConverter ASP.NET model binding, WPF, WinForms
JsonConverter {TypeName}SystemTextJsonConverter System.Text.Json serialization
EfCoreValueConverter EfCoreValueConverter EF Core database persistence

IdConverters Flags Enum

[Flags]
public enum IdConverters
{
    None              = 0,
    JsonConverter     = 1 << 0,
    EfCoreValueConverter = 1 << 1,
}

Combine flags with | to generate multiple converters:

[TypedId(Converters = IdConverters.JsonConverter | IdConverters.EfCoreValueConverter)]
public readonly partial struct ProductId;

Assembly-level profiles

Use [assembly: TypedIdProfile(Converters = IdConverters.JsonConverter)] to apply converters to all IDs in the assembly without repeating the flag on each struct. Use named profiles like [assembly: TypedIdProfile("persistence", ...)] for different converter sets.

TypeConverter (Always Generated)

A nested System.ComponentModel.TypeConverter class is always generated, regardless of IdConverters flags. This enables:

  • ASP.NET model binding — route parameters and query strings are automatically converted
  • PropertyGrid support — WinForms/WPF designer integration
  • TypeDescriptor infrastructure — any framework using TypeDescriptor.GetConverter()

The struct is decorated with [TypeConverter(typeof({TypeName}TypeConverter))].

Generated Code

Generated TypeConverter (Guid-backed)
[TypeConverter(typeof(UserIdTypeConverter))]
public partial struct UserId
{
    public partial class UserIdTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
        {
            return sourceType == typeof(Guid)
                || sourceType == typeof(string)
                || base.CanConvertFrom(context, sourceType);
        }

        public override object? ConvertFrom(
            ITypeDescriptorContext? context, CultureInfo? culture, object value)
        {
            return value switch
            {
                Guid guidValue => new UserId(guidValue),
                string stringValue when !string.IsNullOrEmpty(stringValue)
                    && Guid.TryParse(stringValue, out var result) => new UserId(result),
                _ => base.ConvertFrom(context, culture, value),
            };
        }

        // ConvertTo handles UserId → Guid and UserId → string
    }
}

Backing Type Support

Each backing type supports conversion from its native type and string:

Backing Type CanConvertFrom ConvertFrom
Guid Guid, string Guid directly, string via Guid.TryParse
Int int, string int directly, string via int.TryParse
Long long, string long directly, string via long.TryParse
String string string directly

ASP.NET Usage

With the TypeConverter in place, typed IDs work in ASP.NET route parameters and query strings without additional configuration:

// The TypeConverter handles string → UserId conversion automatically
app.MapGet("/users/{id}", (UserId id) => ...);

JsonConverter (Opt-in)

When IdConverters.JsonConverter is set, a nested System.Text.Json.Serialization.JsonConverter<T> class is generated. The struct is decorated with [JsonConverter(typeof({TypeName}SystemTextJsonConverter))].

Read/Write Mapping

Backing Type JSON Type Read Write
Guid string reader.GetGuid() writer.WriteStringValue(value.Value)
Int number reader.GetInt32() writer.WriteNumberValue(value.Value)
Long number reader.GetInt64() writer.WriteNumberValue(value.Value)
String string reader.GetString()! writer.WriteStringValue(value.Value)

Dictionary Key Support

On .NET 6+, ReadAsPropertyName and WriteAsPropertyName are also generated, enabling typed IDs as JSON dictionary keys:

var lookup = new Dictionary<CustomerId, Order> { ... };
var json = JsonSerializer.Serialize(lookup);
// Keys are serialized as strings: { "a1b2c3d4-...": { ... } }

Generated Code

Generated JsonConverter (Guid-backed)
[JsonConverter(typeof(CustomerIdSystemTextJsonConverter))]
public partial struct CustomerId
{
    public partial class CustomerIdSystemTextJsonConverter
        : JsonConverter<CustomerId>
    {
        public override CustomerId Read(
            ref Utf8JsonReader reader, Type typeToConvert,
            JsonSerializerOptions options) => new(reader.GetGuid());

        public override void Write(
            Utf8JsonWriter writer, CustomerId value,
            JsonSerializerOptions options)
            => writer.WriteStringValue(value.Value);

        // .NET 6+: ReadAsPropertyName / WriteAsPropertyName
    }
}

Usage

var id = CustomerId.New();
var json = JsonSerializer.Serialize(id);     // "\"a1b2c3d4-...\""
var back = JsonSerializer.Deserialize<CustomerId>(json);

EfCoreValueConverter (Opt-in)

When IdConverters.EfCoreValueConverter is set, a nested ValueConverter<T, TBacking> class is generated for use with Entity Framework Core.

Generated Code

Generated EF Core ValueConverter (Guid-backed)
public partial struct CustomerId
{
    public partial class EfCoreValueConverter
        : ValueConverter<CustomerId, Guid>
    {
        public EfCoreValueConverter()
            : this(null) { }

        public EfCoreValueConverter(ConverterMappingHints? mappingHints = null)
            : base(id => id.Value, value => new CustomerId(value), mappingHints)
        { }
    }
}

DbContext Configuration

public class AppDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>()
            .Property(e => e.Id)
            .HasConversion<CustomerId.EfCoreValueConverter>();
    }
}

Convention-based configuration

You can register the converter globally using EF Core conventions instead of configuring each entity individually:

protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<CustomerId>()
        .HaveConversion<CustomerId.EfCoreValueConverter>();
}

Combining Converters

Use the | operator to generate multiple converters on a single ID:

// Full-stack ID: JSON API + EF Core persistence + ASP.NET binding
[TypedId(Converters = IdConverters.JsonConverter | IdConverters.EfCoreValueConverter)]
public readonly partial struct OrderId;

The TypeConverter is always included on top of any explicit flags, so the above generates three nested classes:

  1. OrderIdTypeConverter — ASP.NET model binding
  2. OrderIdSystemTextJsonConverter — JSON serialization
  3. EfCoreValueConverter — EF Core persistence