Entity FrameworkMiddleTechnical
Что такое Fluent API в EF Core и чем он отличается от data annotations?
Data annotations — атрибуты прямо на модели ([Required], [MaxLength]). Fluent API — конфигурация в OnModelCreating, приоритетнее аннотаций, поддерживает всё, что аннотации не умеют: составные ключи, индексы, shadow properties, table splitting.
Два способа конфигурировать модель
EF Core позволяет описывать маппинг сущностей двумя способами, которые можно комбинировать:
- Data Annotations — атрибуты из
System.ComponentModel.DataAnnotationsиMicrosoft.EntityFrameworkCore, размещаемые прямо на классе или его свойствах. - Fluent API — конфигурация через переопределение
OnModelCreating(ModelBuilder)вDbContextили через отдельные классыIEntityTypeConfiguration<T>.
Приоритет: при конфликте Fluent API выигрывает над аннотациями.
Data Annotations — пример
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("products")]
public class Product
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[ForeignKey(nameof(Category))]
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
Аннотации удобны для простых случаев: код модели самодокументирован. Но они загрязняют доменный класс инфраструктурными деталями и не покрывают сложные сценарии.
Fluent API — тот же результат + расширенные возможности
// Отдельный класс конфигурации (рекомендуется для крупных моделей)
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("products");
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.IsRequired()
.HasMaxLength(200);
builder.Property(p => p.Price)
.HasColumnType("decimal(18,2)");
// Составной уникальный индекс — недоступно через аннотации
builder.HasIndex(p => new { p.Name, p.CategoryId })
.IsUnique();
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
}
}
// Регистрация в DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
Что умеет только Fluent API
// 1. Shadow properties — колонка в БД без свойства в классе
builder.Property<DateTime>("CreatedAt").HasDefaultValueSql("NOW()");
// 2. Table splitting — две сущности в одной таблице
builder.ToTable("orders");
builder.HasOne(o => o.Details).WithOne().HasForeignKey<OrderDetails>(d => d.OrderId);
// 3. Value conversions
builder.Property(p => p.Status)
.HasConversion(s => s.ToString(), s => Enum.Parse<OrderStatus>(s));
// 4. Owned types (хранение value object в той же таблице)
builder.OwnsOne(o => o.Address);
// 5. Составной первичный ключ
builder.HasKey(x => new { x.OrderId, x.ProductId });
Сравнительная таблица
| Возможность | Data Annotations | Fluent API |
|---|---|---|
| Простая валидация (Required, MaxLength) | Да | Да |
| Имя таблицы / колонки | Да | Да |
| Составной PK | Нет | Да |
| Уникальный индекс | Нет (только через EF-атрибут Index) | Да |
| Shadow properties | Нет | Да |
| Value conversions | Нет | Да |
| OnDelete behavior | Нет | Да |
| Чистота доменного класса | Нет (атрибуты на классе) | Да |
Подводные камни
- Смешивание без понимания приоритетов — аннотация
[MaxLength(100)]иbuilder.Property(p => p.Name).HasMaxLength(200)на одном свойстве дадутvarchar(200)в БД, что может удивить. - Аннотации из System.ComponentModel.DataAnnotations не всегда транслируются в DDL —
[Range],[EmailAddress]— это валидация ASP.NET, а не EF Core. Они не создают CHECK-ограничений в базе данных. - Забытый ApplyConfigurationsFromAssembly — если
IEntityTypeConfigurationнаписан, но не зарегистрирован, конфигурация молча игнорируется; миграция не содержит нужных изменений. - Data annotations на доменном классе нарушают Clean Architecture — если доменный слой не должен зависеть от EF Core, аннотации из
Microsoft.EntityFrameworkCoreсоздают нежелательную зависимость. Используйте Fluent API в отдельном слое инфраструктуры. - Index через атрибут [Index] требует EF Core 5+ — в более ранних версиях индексы задавались только через Fluent API. Перенос кода на старую версию сломает компиляцию.
- Fluent API в OnModelCreating не проверяется при сборке — опечатка в имени колонки или неверный тип в
HasColumnTypeобнаруживаются только при выполнении миграции или первом запросе. - Порядок вызовов в цепочке важен —
builder.HasOne().WithMany().HasForeignKey()должен вызываться в правильном порядке; пропускHasForeignKeyзаставит EF Core генерировать shadow property для FK вместо использования явного поля. - Составной ключ нельзя задать атрибутом [Key] — попытка поставить
[Key]на два свойства вызовет исключение при запуске. Составные ключи — исключительно Fluent API.
Common mistakes
- Путать Fluent API против data annotations с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет Fluent API против data annotations на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.