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¶
[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¶
[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¶
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:
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:
OrderIdTypeConverter— ASP.NET model bindingOrderIdSystemTextJsonConverter— JSON serializationEfCoreValueConverter— EF Core persistence