Entity FrameworkMiddleCoding

Что такое value converters в EF Core и как их использовать?

Value converters преобразуют значения свойств между CLR-типом модели и типом, хранимым в БД. Настраиваются через HasConversion() в OnModelCreating или через атрибут [ValueConverter]. Встроенные конвертеры: EnumToStringConverter, DateTimeToTicksConverter и другие.

Value Converters в EF Core

Value converter — это пара функций: toProvider (из CLR → БД) и fromProvider (из БД → CLR). EF Core применяет их автоматически при чтении и записи данных. Это позволяет хранить сложные типы в простых столбцах БД без изменения схемы или модели домена.

Базовое использование: HasConversion

// Хранение enum как строки вместо int
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Order>()
        .Property(o => o.Status)
        .HasConversion(
            v => v.ToString(),          // CLR → DB: "Pending", "Shipped"
            v => Enum.Parse<OrderStatus>(v) // DB → CLR
        );
}

Встроенные конвертеры

EF Core предоставляет готовые конвертеры в пространстве имён Microsoft.EntityFrameworkCore.Storage.ValueConversion:

using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

modelBuilder
    .Entity<Order>()
    .Property(o => o.Status)
    .HasConversion(new EnumToStringConverter<OrderStatus>());

// Другие встроенные:
// BoolToZeroOneConverter<int>   — bool → 0/1
// DateTimeToTicksConverter       — DateTime → long
// DateTimeOffsetToBinaryConverter
// TimeSpanToTicksConverter
// GuidToBytesConverter

Пользовательский конвертер (класс)

Для переиспользования конвертер выносят в отдельный класс, наследующий ValueConverter<TModel, TProvider>.

public class MoneyConverter : ValueConverter<decimal, long>
{
    public MoneyConverter() : base(
        v => (long)(v * 100),       // рубли → копейки в БД
        v => v / 100m               // копейки → рубли в CLR
    ) { }
}

// Применение:
modelBuilder
    .Entity<Product>()
    .Property(p => p.Price)
    .HasConversion(new MoneyConverter());

Хранение объекта как JSON-строки

Полезно для Value Object или сложной структуры без отдельной таблицы.

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string City   { get; set; } = string.Empty;
    public string Zip    { get; set; } = string.Empty;
}

var addressConverter = new ValueConverter<Address, string>(
    v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
    v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions?)null)!
);

modelBuilder
    .Entity<Customer>()
    .Property(c => c.ShippingAddress)
    .HasConversion(addressConverter)
    .HasColumnType("text"); // явно укажите тип столбца

Применение конвертера ко всем свойствам типа

EF Core 6+ позволяет зарегистрировать конвертер глобально для всех свойств определённого типа.

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<OrderStatus>()
        .HaveConversion<EnumToStringConverter<OrderStatus>>();

    // Или для кастомного типа:
    configurationBuilder
        .Properties<decimal>()
        .HaveConversion<MoneyConverter>();
}

ValueComparer — обязателен для изменяемых типов

EF Core отслеживает изменения через сравнение значений. Для изменяемых типов (списков, объектов) необходимо предоставить ValueComparer, иначе EF Core не заметит изменений внутри объекта.

var listConverter = new ValueConverter<List<string>, string>(
    v => string.Join(',', v),
    v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
);

var listComparer = new ValueComparer<List<string>>(
    (c1, c2) => c1!.SequenceEqual(c2!),
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
    c => c.ToList() // снимок для change tracking
);

modelBuilder
    .Entity<Tag>()
    .Property(t => t.Aliases)
    .HasConversion(listConverter, listComparer);

Подводные камни

  • LINQ-фильтрация в БД может не работать — EF Core не всегда транслирует предикаты на конвертированных свойствах в SQL; проверяйте, что запрос не уходит в memory-evaluation (EnableSensitiveDataLogging + логи).
  • Без ValueComparer изменяемые объекты не отслеживаются — EF Core не обнаружит изменение поля внутри сложного объекта без явного компаратора.
  • Nullable-типы требуют явной обработки null — если свойство nullable, добавьте null-проверку в лямбды конвертера, иначе получите NullReferenceException при чтении NULL из БД.
  • Тип столбца не определяется автоматически — при хранении объекта как JSON укажите .HasColumnType("text") или "nvarchar(max)", иначе провайдер выберет неоптимальный тип.
  • HasConversion переопределяет ColumnType — указывайте HasColumnType после HasConversion, а не до.
  • Конвертер не работает с raw SQL — при использовании ExecuteSqlRaw или Dapper значения передаются напрямую без конвертации EF Core.
  • Производительность JSON-сериализации — конвертеры вызываются при каждом чтении/записи строки; для горячих путей используйте кэширование JsonSerializerOptions.

Common mistakes

  • Путать value converters и преобразование CLR/DB типов с похожим механизмом из другой версии или платформы.
  • Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
  • Не обсуждать null/empty/error cases и поведение под нагрузкой.

What the interviewer is testing

  • Кандидат объясняет value converters и преобразование CLR/DB типов на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics