Introduction
EF Core value converters transform property values between the CLR type and the database type. They enable storing complex types as strings, enums as text, collections as JSON, and custom types as primitive values. When a value converter is configured but not applied, queries fail with type mismatch errors, data is saved in the wrong format, or the converter is silently ignored due to configuration conflicts, shadow properties overriding the conversion, or the converter being defined after the model is cached.
Symptoms
- Enum stored as integer despite
HasConversion<string>()configuration - Custom type saved as fully qualified name instead of converted value
- Query fails with "cannot convert" database error
- Migration generates column with wrong type
- Value converter works for inserts but not for queries
- Owned type properties not converted
Error output:
``
Npgsql.PostgresException (0x80004005): 42804: column "status" is of type
integer but expression is of type text
Common Causes
HasConversionconfigured after model is built (cached model)- Property type does not match the type the converter expects
- Column type explicitly set to conflicting type with
HasColumnType - Converter defined on wrong entity type in inheritance hierarchy
- Value converter registered but
OnModelCreatingnot called for that entity - JSON columns using converter instead of native JSON support
Step-by-Step Fix
- 1.Configure value converter correctly in OnModelCreating:
- 2.```csharp
- 3.public class ApplicationDbContext : DbContext
- 4.{
- 5.protected override void OnModelCreating(ModelBuilder modelBuilder)
- 6.{
- 7.// Enum to string conversion
- 8.modelBuilder.Entity<Order>()
- 9..Property(o => o.Status)
- 10..HasConversion<string>() // Simple built-in conversion
- 11..HasMaxLength(20);
// Custom value converter modelBuilder.Entity<Product>() .Property(p => p.Price) .HasConversion( v => (int)(v.Amount * 100), // CLR to DB: decimal to cents v => new Money(v / 100m)); // DB to CLR: cents to Money
// Using ValueConverter class var addressConverter = new ValueConverter<Address, string>( v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions?)null) ?? new Address());
modelBuilder.Entity<Customer>() .Property(c => c.Address) .HasConversion(addressConverter) .HasColumnType("nvarchar(max)"); } }
// Value object public class Money { public decimal Amount { get; } public Money(decimal amount) => Amount = amount; public static implicit operator decimal(Money m) => m.Amount; }
public record Address(string Street, string City, string PostalCode, string Country); ```
- 1.Use ValueConverter for complex type mappings:
- 2.```csharp
- 3.// Store a list of tags as comma-separated string
- 4.public class TagListConverter : ValueConverter<List<string>, string>
- 5.{
- 6.public TagListConverter() : base(
- 7.v => string.Join(",", v.Where(t => !string.IsNullOrEmpty(t))),
- 8.v => string.IsNullOrEmpty(v)
- 9.? new List<string>()
- 10.: v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList())
- 11.{ }
- 12.}
// Store Dictionary as JSON public class DictionaryJsonConverter<TKey, TValue> : ValueConverter<Dictionary<TKey, TValue>, string> where TKey : notnull { public DictionaryJsonConverter() : base( v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), v => JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(v, (JsonSerializerOptions?)null) ?? new Dictionary<TKey, TValue>()) { } }
// Entity configuration public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.Property(p => p.Tags) .HasConversion(new TagListConverter()) .HasMaxLength(500);
builder.Property(p => p.Metadata) .HasConversion(new DictionaryJsonConverter<string, string>()) .HasColumnType("nvarchar(max)"); } }
// Register configuration modelBuilder.ApplyConfiguration(new ProductConfiguration());
// Entity public class Product { public int Id { get; set; } public string Name { get; set; } = string.Empty; public List<string> Tags { get; set; } = new(); public Dictionary<string, string> Metadata { get; set; } = new(); } ```
- 1.Use EF Core 8+ primitive collections instead of custom converters:
- 2.```csharp
- 3.// EF Core 8+ has native support for collections
- 4.modelBuilder.Entity<Product>()
- 5..Property(p => p.Tags)
- 6..HasColumnType("nvarchar(max)"); // Stored as JSON array
// For PostgreSQL, use native array type modelBuilder.Entity<Product>() .Property(p => p.Tags) .HasColumnType("text[]"); // PostgreSQL native array
// For JSON columns in EF Core 8+, use JsonColumn instead of converter // (EF Core 8+ supports JSON columns natively for SQL Server and PostgreSQL)
// Query with collection: var productsWithTag = context.Products .Where(p => p.Tags.Contains("electronics")) // Works with EF Core 8+ .ToList(); ```
- 1.Debug value converter registration:
- 2.```csharp
- 3.// Verify converter is applied at runtime
- 4.protected override void OnModelCreating(ModelBuilder modelBuilder)
- 5.{
- 6.modelBuilder.Entity<Order>()
- 7..Property(o => o.Status)
- 8..HasConversion<string>();
// Debug: inspect the model after configuration var model = modelBuilder.Model; foreach (var entityType in model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var converter = property.GetValueConverter(); if (converter != null) { Console.WriteLine( $"{entityType.Name}.{property.Name}: " + $"{converter.ProviderClrType.Name} <-> {converter.ModelClrType.Name}"); } else { Console.WriteLine( $"{entityType.Name}.{property.Name}: No converter"); } } } }
// Common pitfall: HasConversion with enum stored as int (default) // If you want string, you MUST explicitly configure it: public enum OrderStatus { Pending = 0, Processing = 1, Shipped = 2, Delivered = 3 }
// WRONG - stores as int (0, 1, 2, 3) modelBuilder.Entity<Order>() .Property(o => o.Status);
// CORRECT - stores as string ("Pending", "Processing", ...) modelBuilder.Entity<Order>() .Property(o => o.Status) .HasConversion<string>();
// For specific string values, use enum-to-string converter var statusConverter = new EnumToStringConverter<OrderStatus>(); modelBuilder.Entity<Order>() .Property(o => o.Status) .HasConversion(statusConverter); ```
Prevention
- Configure value converters early in
OnModelCreatingbefore model is cached - Use
IEntityTypeConfiguration<T>classes to keep configuration organized - Create and register migrations after adding converters to verify column types
- Write integration tests that save and read entities with converters
- Use EF Core 8+ native JSON and collection support instead of custom converters where possible
- Document which properties use converters and their target database types
- Test converters with both null and non-null values
- For enum conversions, prefer
HasConversion<string>()over integer storage for readability and backward compatibility