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

Sources

Related topics