Что такое 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 типов на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.