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