Sometimes, the data type we want to use in C# memory (like a rich object or an Enum) cannot be natively stored in a SQL database column. Value Conversions act as microscopic translators that run automatically, converting complex C# types into simple SQL types upon saving, and reversing the process upon loading.
By default, EF Core stores C# Enums in SQL Server as Integers (e.g., 0, 1, 2). This is a nightmare for DBAs trying to read the raw database because "Status = 2" is meaningless without reading the C# source code. We can force EF Core to store the Enum as a String!
public enum OrderStatus { Pending, Shipped, Delivered }
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
}
// In OnModelCreating:
modelBuilder.Entity<Order>()
.Property(o => o.Status)
// C# -> SQL, SQL -> C#
.HasConversion(
v => v.ToString(), // How to save to SQL ("Pending")
v => (OrderStatus)Enum.Parse(typeof(OrderStatus), v)); // How to load back to C#
What if you have a C# object representing an "Address" with a Street, City, and Zip code, but you just want to store the entire thing flat inside a single SQL 'NVARCHAR' column?
public class Address { public string City; public string Zip; }
modelBuilder.Entity<User>()
.Property(u => u.HomeAddress)
.HasConversion(
addressObj => JsonSerializer.Serialize(addressObj, (JsonSerializerOptions)null),
jsonString => JsonSerializer.Deserialize<Address>(jsonString, (JsonSerializerOptions)null)
);
A massive bug in C# is passing a ProductId integer into a method that expects a UserId integer. The compiler won't complain because they are both integers. DDD solves this by creating strict ProductId and UserId records. We use Value Conversions to teach EF Core how to handle these custom types.
// The Strongly Typed Record
public readonly record struct ProductId(int Value);
public class Product
{
public ProductId Id { get; set; } // It is absolutely IMPOSSIBLE to assign a UserId here!
}
// Teaching EF Core how to save this custom record as a normal integer
modelBuilder.Entity<Product>()
.Property(p => p.Id)
.HasConversion(
id => id.Value, // Extract the raw int for SQL Server
value => new ProductId(value) // Wrap the int back into the Record on load
);
Q: "If we use a Value Conversion to store a complex object as JSON in an `NVARCHAR(MAX)` column, can we still use EF Core LINQ to filter the data, such as `.Where(u => u.HomeAddress.City == 'London')`?"
Architect Answer: "No, you absolutely cannot. This is the primary pitfall of Value Conversions. Value Conversions operate purely in .NET Memory as a translation layer. SQL Server has no idea what 'HomeAddress.City' means because it just sees a giant string blob. When EF Core tries to translate the LINQ `Where` clause into SQL, it will throw a severe `InvalidOperationException` because it cannot map the JSON property to a SQL Column. If you need to physically query data using LINQ, you must use Owned Entities (which map to physical columns) or EF Core 7+'s native JSON column mapping capabilities, rather than a hacky string-based Value Conversion."