# Value Conversions in EF Core Explained

> Value conversions translate between a domain type and its database column: enums stored as strings, strongly typed IDs stored as Guids, value objects stored as scalars. Here is how to use HasConversion, when you need a ValueComparer, and where converted properties break LINQ translation.

Published: 2026-08-26. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/value-conversions-ef-core

A **value conversion** is a pair of transformations EF Core applies to a property: one converts the .NET value to the database value on write, the other converts it back on read.
That lets a domain type differ from its database representation, which is especially useful for enums and strongly typed IDs.
The conversion must preserve query semantics, and mutable reference types also need a value comparer for correct change tracking.

## What Are Value Conversions?

EF Core value conversions let you transform a property's value when it's stored in the database and when it's read back into your entity. You define a pair of expressions: one for writing and one for reading. EF Core applies them transparently.

This is useful when your domain model uses types that don't map directly to database columns - enums, strongly typed IDs, [value objects](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals), or custom types.

## HasConversion Basics

The simplest way to configure a value conversion is with `HasConversion` in your entity configuration:

```csharp
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.Property(o => o.Status)
            .HasConversion(
                status => status.ToString(),        // Write: enum → string
                value => Enum.Parse<OrderStatus>(value)); // Read: string → enum
    }
}
```

The first lambda converts the .NET value to the database value. The second converts it back. EF Core calls these automatically during `SaveChangesAsync` and queries.

![An OrderStatus enum on the entity is converted to a string when written to the text column, and parsed back to the enum when read from the database](https://milanjovanovic.tech/blogs/articles/value-conversions-ef-core/value-conversion-flow.png)

## Enum-to-String Conversions

Storing enums as strings is one of the most common use cases. It makes your database more readable and avoids breaking changes when you reorder enum members.

EF Core provides a built-in converter for this:

```csharp
builder.Property(o => o.Status)
    .HasConversion(new EnumToStringConverter<OrderStatus>())
    .HasMaxLength(50);
```

Without this, EF Core stores enums as integers by default. That works, but you end up with `0`, `1`, `2` in your database instead of `Draft`, `Confirmed`, `Shipped`.

## Built-in Converters

EF Core ships with several built-in converters in the `Microsoft.EntityFrameworkCore.Storage.ValueConversion` namespace:

- `BoolToStringConverter` - stores a `bool` as configurable strings like "Y"/"N"
- `BoolToZeroOneConverter` - stores a `bool` as `0` or `1`
- `DateTimeToTicksConverter` - stores a `DateTime` as a `long` tick count
- `EnumToStringConverter<TEnum>` - stores an enum as its member name
- `EnumToNumberConverter<TEnum, TNumber>` - stores an enum as a numeric type of your choice
- `GuidToStringConverter` - stores a `Guid` as a string
- `TimeSpanToTicksConverter` - stores a `TimeSpan` as a `long` tick count

You can use any of these directly with `HasConversion`:

```csharp
builder.Property(o => o.IsActive)
    .HasConversion(new BoolToStringConverter("No", "Yes"));
```

## Custom Converters

For more complex scenarios, create a custom `ValueConverter<TModel, TProvider>`:

```csharp
public sealed record Email(string Value);

public class EmailConverter : ValueConverter<Email, string>
{
    public EmailConverter()
        : base(
            email => email.Value,
            value => new Email(value))
    {
    }
}
```

Then apply it:

```csharp
builder.Property(u => u.Email)
    .HasConversion(new EmailConverter())
    .HasMaxLength(255);
```

For value objects with a single property, this is the cleanest approach. You keep your domain model expressive without polluting the database schema.

## Strongly Typed IDs

Strongly typed IDs prevent you from accidentally passing an `OrderId` where a `CustomerId` is expected. Value conversions make them work with EF Core:

```csharp
public readonly record struct OrderId(Guid Value);

public class OrderIdConverter : ValueConverter<OrderId, Guid>
{
    public OrderIdConverter()
        : base(
            id => id.Value,
            value => new OrderId(value))
    {
    }
}
```

Configure in your `DbContext`:

```csharp
builder.Property(o => o.Id)
    .HasConversion(new OrderIdConverter());
```

Or apply it globally using conventions (EF Core 6+):

```csharp
protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<OrderId>()
        .HaveConversion<OrderIdConverter>();
}
```

## Value Comparers

EF Core uses value comparers to determine if a property's value has changed. For reference types, the default comparer uses `ReferenceEquals`, which means the [change tracker](https://milanjovanovic.tech/blog/change-tracker-ef-core) might not detect changes to mutable objects.

You need a custom `ValueComparer` when your converted type is a class:

```csharp
builder.Property(o => o.Tags)
    .HasConversion(
        tags => string.Join(',', tags),
        value => value.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
    .Metadata.SetValueComparer(
        new ValueComparer<List<string>>(
            (a, b) => a.SequenceEqual(b),
            c => c.Aggregate(0, (h, v) => HashCode.Combine(h, v.GetHashCode())),
            c => c.ToList()));
```

The three expressions define equality, hash code, and snapshot. Without a proper comparer, EF Core either misses changes or generates unnecessary updates.

## JSON Columns

EF Core supports mapping object graphs to JSON columns through owned types and, in EF Core 10, complex types.
This is model-level JSON mapping rather than a value converter, so nested members remain available to translated queries:

```csharp
builder.OwnsOne(o => o.ShippingAddress, address =>
{
    address.ToJson();
});
```

For simpler cases, you can use a manual JSON conversion:

```csharp
builder.Property(o => o.Metadata)
    .HasConversion(
        meta => JsonSerializer.Serialize(meta, JsonSerializerOptions.Default),
        json => JsonSerializer.Deserialize<OrderMetadata>(
            json, JsonSerializerOptions.Default)!)
    .HasColumnType("jsonb");
```

The converter approach is fine for blobs of semi-structured data, but the serialized string is opaque to EF Core.
It cannot translate queries against individual properties, which is exactly what the model-level JSON mapping above gives you.

## Limitations

Value conversions have a few important constraints:

- **Null handling**: Null values are never passed through the converter. EF Core handles nulls separately. A nullable property stores `NULL` in the database without invoking the converter.
- **Navigations**: Converters apply to scalar properties, never to navigation properties or entity collections. Use [owned types](https://milanjovanovic.tech/blog/owned-types-ef-core-ddd) or JSON columns for those. A primitive collection like `List<string>` can be converted (as the tags example above shows), but then it needs a value comparer.
- **Querying**: the database only sees the converted value. Filtering on equality works because EF Core converts your parameters too, but operations that need the original type's semantics don't. `Where(o => o.Status > OrderStatus.Confirmed)` on a string-stored enum compares alphabetically, not by enum order.
- **Sorting**: same problem. `OrderBy(o => o.Status)` sorts by the stored string, so `Cancelled` comes before `Draft`.

For value objects with **multiple** properties, a value converter to a single column doesn't fit.
Use [**complex types**](https://milanjovanovic.tech/blog/complex-types-ef-core) (EF Core 8+) or owned entities so each property gets its own column.

## Applying Conversions Globally

Instead of configuring each property individually, use `ConfigureConventions` to apply conversions across all entities:

```csharp
protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<DateTime>()
        .HaveConversion<DateTimeToUtcConverter>();

    configurationBuilder.Properties<OrderStatus>()
        .HaveConversion<EnumToStringConverter<OrderStatus>>()
        .HaveMaxLength(50);
}
```

`DateTimeToUtcConverter` is a small custom converter that normalizes every `DateTime` to UTC:

```csharp
public class DateTimeToUtcConverter : ValueConverter<DateTime, DateTime>
{
    public DateTimeToUtcConverter()
        : base(
            value => value.ToUniversalTime(),
            value => DateTime.SpecifyKind(value, DateTimeKind.Utc))
    {
    }
}
```

This keeps your entity configurations clean and ensures consistency across the entire model.

## Summary

Use `HasConversion` when a domain value has a stable scalar representation in the database.
Add a value comparer for mutable reference types so the [change tracker](https://milanjovanovic.tech/blog/change-tracker-ef-core) can snapshot and compare them correctly.
Apply repeated conversions through `ConfigureConventions`, and use provider JSON mapping rather than a converter when nested members must remain queryable.

## Frequently asked questions

### What is a value conversion in EF Core?

A value conversion is a pair of transformations EF Core applies to a property: one converts the .NET value to the database value on write, the other converts it back on read. It lets domain types like enums, strongly typed IDs, and value objects map to simple database columns.

### How do I store an enum as a string in EF Core?

Configure the property with HasConversion using EnumToStringConverter, or call HasConversion<string>() as a shortcut. Add HasMaxLength so the column is not created as unbounded text.

### Why does EF Core not detect changes to my converted property?

When the converted type is a mutable reference type, the default comparer checks reference equality only. You need to set a custom ValueComparer that defines equality, hash code, and snapshot logic so the change tracker can detect mutations.

### Do value conversions work with LINQ queries?

Mostly. EF Core applies the conversion to constants and parameters so equality filters work, but the database sees only the converted value. Operations that rely on the original type semantics, like numeric comparisons on an enum stored as string, can behave unexpectedly or fail to translate.

### Can EF Core value converters convert null values?

By default, no. Nulls are handled by EF Core outside the converter, so a null property is stored as NULL without invoking your conversion logic.
