EF Core это стандартный ORM в .NET. На собесе проверяют, понимаете ли вы, какой SQL получится: N+1, загрузка связей, отслеживание изменений, миграции и конкурентность.
DbContext
DbContext это единица работы: трекер изменений плюс доступ к DbSet<T>. Он не потокобезопасен и рассчитан на короткую жизнь: в ASP.NET Core один на запрос (AddDbContext регистрирует его как Scoped). Соединение берётся из пула ADO.NET на время операции, поэтому создавать контексты дёшево. Для фоновых задач IDbContextFactory<T>, для горячих путей AddDbContextPool.
IQueryable и момент выполнения
Запрос переводится в SQL при материализации: ToListAsync, FirstOrDefaultAsync, CountAsync, foreach.
// плохо: вся таблица в памяти
var adults = db.Users.ToList().Where(u => u.Age >= 18);
// хорошо: WHERE в SQL
var adults = await db.Users.Where(u => u.Age >= 18).ToListAsync(ct);
Метод, возвращающий IEnumerable<T> вместо IQueryable<T>, тоже переносит дальнейшие фильтры в память.
Загрузка связей и N+1
var blogs = await db.Blogs.ToListAsync();
foreach (var b in blogs)
Console.WriteLine(b.Posts.Count); // с ленивой загрузкой: запрос на каждый блог
Варианты:
Include(b => b.Posts).ThenInclude(p => p.Tags): связи в том же запросе;AsSplitQuery(): по запросу на коллекцию, против декартова взрыва при несколькихInclude;- проекция:
Select(b => new BlogDto(b.Name, b.Posts.Count())), лучший вариант для чтения, считает в SQL; - явная загрузка
Entry(b).Collection(x => x.Posts).LoadAsync().
Ленивая загрузка выключена по умолчанию, и это хорошо.
Отслеживание изменений
Каждая сущность в трекере имеет состояние: Added, Unchanged, Modified, Deleted, Detached. При загрузке EF сохраняет снимок значений, а SaveChanges через DetectChanges находит изменения и генерирует INSERT, UPDATE (только изменённых колонок), DELETE в одной транзакции.
AsNoTracking()для запросов только на чтение: без снимков, быстрее.RemoveуAdded-сущности просто перестаёт её отслеживать.- Большой долгоживущий контекст тормозит:
DetectChangesпроходит все сущности.
Массовые операции без загрузки (EF Core 7+):
await db.Orders.Where(o => o.CreatedAt < cutoff).ExecuteDeleteAsync(ct);
await db.Products.Where(p => p.CategoryId == id)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.Price, p => p.Price * 1.1m), ct);
Транзакции и конкурентность
SaveChanges сам транзакционный. Для нескольких сохранений await using var tx = await db.Database.BeginTransactionAsync();. При политиках повтора (EnableRetryOnFailure) ручные транзакции оборачивают в CreateExecutionStrategy().ExecuteAsync.
Оптимистичная конкурентность: свойство-токен ([Timestamp], rowversion в SQL Server, xmin в PostgreSQL). EF добавляет условие на старое значение в UPDATE, и при конфликте бросает DbUpdateConcurrencyException.
Миграции
dotnet ef migrations add AddOrderStatus
dotnet ef migrations script --idempotent -o migrate.sql
dotnet ef migrations bundle
В продакшене миграции применяют отдельным шагом деплоя (скрипт или bundle), а не Database.Migrate() при старте каждой реплики. EnsureCreated с миграциями несовместим. Опасные изменения (переименование колонки, удаление) делают в несколько релизов, чтобы старая и новая версии приложения работали с одной схемой.
Сырой SQL
FromSql($"... {param}") и ExecuteSql параметризуют интерполяцию. FromSqlRaw с конкатенацией строк это SQL-инъекция.
Частые ошибки
ToList()слишком рано.- N+1 и десятки одинаковых запросов в логе.
- Отдавать сущности наружу вместо DTO.
- Один
DbContextна несколько параллельных задач. - InMemory-провайдер для тестов запросов.
Что спросят на собесе
- Что такое N+1 и как его найти и исправить?
- Зачем
AsNoTracking? - Как EF Core понимает, какие поля изменились?
- Как применять миграции в продакшене?
- Как обработать одновременное редактирование записи?