When working with Entity Framework Core, you may have properties that use enums, custom classes, or other types that do not map directly to the database format you want. For example, an application might represent an order status as an enum in C# but store it as a string in SQL Server.
You could convert these values manually every time you save or retrieve a record, but that approach creates extra code and makes data access harder to maintain.
EF Core value converters solve this problem by converting a model property into a database-compatible value when saving data and converting it back when reading data.
In this article, we will look at how value converters work, how to store enums as strings, how to map custom types, and what to consider when using converters in production applications.
What Is a Value Converter in Entity Framework Core?
A value converter tells Entity Framework Core how to convert a property between its .NET type and its database representation.
For example, consider an order status enum:
public enum OrderStatus
{
Pending,
Processing,
Shipped,
Cancelled
}
By default, EF Core typically stores an enum as its underlying numeric value. The Pending value would be stored as 0, Processing as 1, and so on.
This works, but the database values are not very descriptive. If you inspect a row directly, you must know which number represents each status.
A value converter lets you store the enum as readable text instead.
C# value | Default numeric storage | String storage |
|---|
Pending | 0 | Pending |
Processing | 1 | Processing |
Shipped | 2 | Shipped |
Cancelled | 3 | Cancelled |
String storage can make database inspection easier, but it also introduces considerations when enum names change. The right choice depends on your application's data model and compatibility requirements.
Step 1: Create an Entity with an Enum Property
Start with a simple Order entity.
public sealed class Order
{
public int Id { get; set; }
public string OrderNumber { get; set; } = string.Empty;
public OrderStatus Status { get; set; }
}
The Status property uses the OrderStatus enum.
You can use this property normally in your application. For example, business logic can compare the value with OrderStatus.Shipped without knowing how the database stores it.
EF Core handles the conversion during database operations when a converter is configured.
Step 2: Store an Enum as a String
EF Core provides the built-in EnumToStringConverter for converting enum values to strings.
You can configure it in the DbContext using Fluent API.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
public sealed class AppDbContext : DbContext
{
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.Property(order => order.Status)
.HasConversion(
new EnumToStringConverter<OrderStatus>());
}
}
The converter tells EF Core to store the enum's name as text and convert it back to the enum when materializing an entity.
For example, an order with a status of Shipped is stored with the text Shipped rather than the numeric value 2.
You can also configure the conversion more simply:
modelBuilder.Entity<Order>()
.Property(order => order.Status)
.HasConversion<string>();
This is usually the most convenient option when you want the enum's string representation and do not need a custom conversion rule.
Remember that the database column must support the stored values. For SQL Server, check the column's string type and length before applying the migration.
Step 3: Save and Retrieve Converted Values
Once the converter is configured, you do not need to perform manual conversions in application code.
For example, you can save an order as follows:
var order = new Order
{
OrderNumber = "ORD-1001",
Status = OrderStatus.Processing
};
dbContext.Orders.Add(order);
await dbContext.SaveChangesAsync();
EF Core converts Processing to its string representation when writing the database row.
You can retrieve the order using a normal query:
var order = await dbContext.Orders
.SingleAsync(x => x.OrderNumber == "ORD-1001");
Console.WriteLine(order.Status);
The Status property is returned as OrderStatus.Processing.
You can also filter by the enum:
var shippedOrders = await dbContext.Orders
.Where(x => x.Status == OrderStatus.Shipped)
.ToListAsync();
EF Core translates the query according to the configured conversion. The conversion does not mean that every arbitrary C# operation can be translated into SQL, so inspect generated SQL when using more complex expressions.
Step 4: Create a Custom Value Converter
Enums are not the only types you may want to convert.
Suppose your application represents an order reference using a custom C# type instead of a plain string. This can help prevent unrelated strings from being mixed up in business logic.
public sealed record OrderReference(string Value);
Now add a converter that maps OrderReference to a string column.
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
public sealed class OrderReferenceConverter
: ValueConverter<OrderReference, string>
{
public OrderReferenceConverter()
: base(
reference => reference.Value,
value => new OrderReference(value))
{
}
}
The first expression converts an OrderReference into a string when saving. The second creates an OrderReference from the stored string when reading.
Configure it on the entity property:
modelBuilder.Entity<Order>()
.Property(order => order.Reference)
.HasConversion<OrderReferenceConverter>();
For this example, the Order entity also needs a Reference property:
public OrderReference Reference { get; set; }
= new(string.Empty);
This approach works well for simple value objects that can be represented by one database column.
Make sure that the custom type is valid when created from database data. If the value has specific format requirements, validate those requirements when constructing the object or through an appropriate application validation layer.
Step 5: Use a Converter for a Custom Type with Validation
A custom type often represents a business rule, not just a different data format.
For example, a product code might need to be non-empty and follow a specific format.
public sealed record ProductCode
{
public string Value { get; }
public ProductCode(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException(
"Product code cannot be empty.",
nameof(value));
}
Value = value;
}
}
You can create a converter for this type in the same way:
public sealed class ProductCodeConverter
: ValueConverter<ProductCode, string>
{
public ProductCodeConverter()
: base(
code => code.Value,
value => new ProductCode(value))
{
}
}
This ensures that values materialized through the converter pass through the ProductCode constructor.
However, a converter is not a replacement for database constraints or request validation. Invalid data may already exist in the database, and a constructor exception during materialization can cause a query to fail.
Validate incoming values before saving them, and consider database constraints for rules that must remain true regardless of which application writes the data.
Step 6: Understand Value Converters and Database Migrations
Changing a property from numeric enum storage to string storage changes the database representation.
For example, an existing column might contain values such as 0, 1, and 2. After switching to string storage, EF Core expects values such as Pending, Processing, and Shipped.
You should not assume that changing HasConversion automatically transforms existing data safely.
Create and inspect a migration:
dotnet ef migrations add StoreOrderStatusAsString
Review the generated migration and determine how existing numeric values will be converted. Depending on the database provider and schema, you may need a staged migration that adds a new column, converts existing values, and then replaces the old column.
After reviewing the migration, apply it to a test database:
dotnet ef database update
For production systems, test the migration against a representative copy of the database and prepare a rollback or recovery plan.
Also consider compatibility with older application versions. If two application versions run during a rolling deployment, both versions must be able to work with the database representation during the transition.
Comparison: Numeric Enums vs. String Enums
Consideration | Numeric storage | String storage |
|---|
Database readability | Requires mapping numbers to enum values | Values are easier to understand |
Storage size | Usually smaller | Depends on the string length |
Renaming enum members | Numeric values can remain stable | Stored strings may require data migration |
Reordering enum members | Can be dangerous if explicit values are not used | Reordering does not change stored names |
Database constraints | Can use numeric checks | Can use string checks |
Compatibility | Depends on numeric assignments | Depends on stable stored names |
Neither approach is universally better.
If database readability is important, strings can be a good choice. If storage efficiency or compatibility with an existing schema matters more, numeric values may be appropriate.
For numeric storage, explicitly assign enum values when their meanings must remain stable across versions. Avoid relying on implicit numbering when the enum is persisted as part of a long-lived data contract.
Common Mistakes to Avoid
One common mistake is renaming an enum member after storing its name in the database. If existing rows contain Processing but the enum member is renamed to InProgress, EF Core will not automatically know that the old stored value should map to the new name.
Another mistake is assuming that a converter automatically handles every query expression. EF Core translates supported expressions according to the provider and model configuration, but arbitrary methods may not translate to SQL.
Do not use a simple value converter for a complex object that requires several database columns. In that situation, consider owned entity types or complex types, depending on your EF Core version and model requirements.
Finally, do not change the conversion strategy without reviewing existing data, migrations, and application-version compatibility.
Best Practices for Production Applications
Use built-in converters when they already meet your requirements.
Keep custom converters small and easy to test.
Validate values before saving them to the database.
Choose database column types and lengths deliberately.
Avoid changing persisted enum names without a migration plan.
Test conversion behavior for both saving and reading.
Review generated SQL for important queries.
Use database constraints for critical data rules.
Plan migrations carefully when changing existing storage formats.
Consider application compatibility during rolling deployments.
Advantages and Disadvantages
Value converters keep application code focused on domain types while EF Core handles the conversion to and from database values. They can make database records more readable and reduce repeated mapping logic.
However, converters introduce another layer that developers must understand when debugging queries and migrations. They can also make schema changes more involved, especially when existing data uses a different representation.
A converter is most useful when the model type has a clear, stable database representation and the conversion can be applied consistently.
Summary
EF Core value converters let you map enums and simple custom types to database-compatible values without manually converting them in every query or save operation.
Use HasConversion<string>() or EnumToStringConverter when storing enums as text, and create a custom ValueConverter when a domain type needs a specific representation.
Before using converters in production, consider existing data, query translation, database constraints, and migration compatibility. A well-designed conversion keeps the application model clean while ensuring that the database stores values in a format your application can maintain over time.
Jasen FiciPosted Nov 2, 2026, 1:23 PM
We included this in DotNetNews: https://dotnetnews.co/archive/the-net-news-daily-issue-575/