Entity FrameworkMiddleCoding

В чём разница между Include() и ThenInclude()?

Include() загружает навигационное свойство первого уровня (прямой потомок), ThenInclude() — продолжает цепочку глубже, загружая свойства у уже загруженного объекта.

Include() и ThenInclude() в EF Core

Include() загружает навигационное свойство первого уровня — прямого «потомка» корневой сущности. ThenInclude() продолжает цепочку и загружает свойство уже у загруженного навигационного объекта, то есть работает на втором и последующих уровнях вложенности.

Структура модели для примеров

public class Order
{
    public int Id { get; set; }
    public Customer Customer { get; set; } = null!;
    public List<OrderItem> Items { get; set; } = new();
}

public class OrderItem
{
    public int Id { get; set; }
    public Product Product { get; set; } = null!;
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public Category Category { get; set; } = null!;
}

public class Category
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
}

Include — первый уровень

// Загружает Customer и Items вместе с Order (2 JOIN или 2 отдельных SELECT)
var orders = await ctx.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .ToListAsync();

// order.Items[0].Product будет null — не загружали глубже

ThenInclude — второй уровень

// Загружает Items, у каждого Item — Product, у Product — Category
var orders = await ctx.Orders
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
            .ThenInclude(p => p.Category)
    .Include(o => o.Customer)  // параллельная ветка — снова Include
    .AsNoTracking()
    .ToListAsync();

// Теперь order.Items[0].Product.Category.Title доступен без доп. запросов

ThenInclude() «привязан» к предыдущему Include() или ThenInclude(). Чтобы начать новую ветку (например, после Items загрузить ещё и Customer), нужно снова вызвать Include() от корневой сущности.

Include через строку (альтернативный синтаксис)

// Работает, но без статической типизации — легко опечататься
var orders = await ctx.Orders
    .Include("Items.Product.Category")
    .ToListAsync();

Когда использовать AsSplitQuery()

Если Include() нескольких коллекций порождает декартово произведение строк, добавьте AsSplitQuery(): EF Core выполнит несколько SELECT вместо одного гигантского JOIN.

var orders = await ctx.Orders
    .Include(o => o.Items).ThenInclude(i => i.Product)
    .Include(o => o.Tags)
    .AsSplitQuery()
    .AsNoTracking()
    .ToListAsync();

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

  • Порядок ThenInclude — вызов ThenInclude() после Include() коллекции (List<T>) работает правильно только если передать лямбду типа элемента, а не коллекции.
  • Дублирование данных — без AsSplitQuery() включение двух коллекций создаёт M×N строк в результирующем наборе и повышает трафик.
  • Фильтрация включений — с EF Core 5+ можно фильтровать: .Include(o => o.Items.Where(i => !i.Deleted)), но AsSplitQuery() в этом случае не поддерживается.
  • Include игнорируется при проекции — если добавить .Select(...) после Include(), EF Core проигнорирует Include() и сам определит нужные JOIN по полям Select.
  • Circular reference при сериализации — двунаправленные навигационные свойства плюс eager loading приводят к зацикливанию JSON.NET; используйте ReferenceHandler.IgnoreCycles или DTO.
  • N+1 при отсутствии Include — если убрать Include(), а lazy loading не включён, обращение к навигационному свойству вернёт null, а не выполнит запрос.

Common mistakes

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

What the interviewer is testing

  • Кандидат объясняет Include и ThenInclude для графа связанных сущностей на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics