EF Core
EF Core — объектно-реляционный маппер: вы пишете LINQ по объектам, он выдаёт SQL и материализует строки обратно в сущности. Обманчивая простота в том, что вызов выглядит как работа со списком в памяти, а стоит обращения к базе. Вся практическая работа с EF Core сводится к двум механизмам, и оба спрашивают на собеседовании. Первый — change tracker: отслеживаемый запрос кладёт каждую сущность в ChangeTracker вместе со снимком её исходных значений, а SaveChanges сравнивает снимок с текущим состоянием и по разнице строит UPDATE. Второй — трансляция дерева выражений: IQueryable не выполняется, а накапливается, и уходит в базу только в момент материализации.
Отсюда растут почти все ловушки. Отслеживание стоит памяти и времени — поэтому чтение только для чтения делают через AsNoTracking, и поэтому же изменение сущности, прочитанной без отслеживания, тихо не сохраняется. Ленивая загрузка превращает обход коллекции в цикл запросов — это N+1. DbContext — это единица работы (Unit of Work) с изменяемым состоянием, поэтому его регистрируют Scoped, а не Singleton. Миграция — это diff модели против снимка, а не команда к базе. Разбор по слоям — ниже.
Карта темы
- DbContext и DbSet — сессия с базой, единица работы и
DbSet<T>как queryable-корень и уже готовый репозиторий. - Регистрация и время жизни —
AddDbContextдаётScoped-контекст, один на HTTP-запрос, и почемуSingletonздесь ломается. - Change tracker и AsNoTracking — снимок исходных значений, identity resolution, diff на
SaveChangesи что именно пропускаетAsNoTracking. - Стратегии загрузки — eager через
Include, explicit черезLoad(), lazy через прокси и цена каждой. - Проблема N+1 — как обращение к навигации в цикле превращает один запрос в пятьдесят один и как это чинится проекцией.
- Миграции — diff модели против снимка,
Up/Down,__EFMigrationsHistoryи схема expand/contract для деплоя без простоя.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Делить один DbContext между параллельными операциями | Контекст не потокобезопасен — вторая операция на том же экземпляре падает с InvalidOperationException |
Регистрировать DbContext как Singleton | Один change tracker на всё приложение — сущности копятся и не освобождаются, запросы видят чужое состояние |
Изменить сущность, прочитанную с AsNoTracking, и вызвать SaveChanges | Ни одной команды: у сущности нет EntityEntry, сравнивать не с чем — правка теряется молча |
| Считать отслеживание кэшом строк | Повторный запрос всё равно уходит в базу; identity resolution лишь вернёт уже отслеживаемый экземпляр вместо нового |
| Оставить lazy loading включённым в API | Каждое чтение навигации внутри цикла — отдельный запрос: это и есть N+1 |
Тянуть Include там, где хватило бы Select | В память едет весь граф со всеми колонками вместо трёх нужных полей |
Ставить несколько Include по коллекциям в один запрос | JOIN размножает строки декартовым произведением — нужен AsSplitQuery() |
Ждать, что нетранслируемый Where тихо посчитается на клиенте | С EF Core 3.0 это InvalidOperationException, а не молчаливая деградация |
Править ModelSnapshot руками | Снимок — база для diff: следующая миграция сгенерируется неверно |
| Переименовать колонку одной миграцией при rolling-деплое | Старая сборка ходит в ту же базу и падает — нужна схема expand/contract |
Значение для собеседований
EF Core — любимая тема на C#-собеседованиях именно потому, что она отделяет тех, кто «умеет писать Include», от тех, кто понимает, что происходит между LINQ и строкой в таблице. Проверяют не API, а модель исполнения: сколько обращений к базе породит этот код и что именно кладётся в память.
Что обычно проверяют:
- Что такое
DbContext, почему онUnit of Workи почемуScoped, а неSingleton. - Механику change tracker — снимок, identity resolution, diff на
SaveChanges— и что снимаетAsNoTracking. - Разницу eager / explicit / lazy loading и какая из них порождает N+1.
- Как найти и починить N+1 (
Includeпротив проекцииSelect). - Что делает
dotnet ef migrations add— и что она не касается базы. - Как выкатить изменение схемы на живой сервис за несколько инстансов.
Типичный неверный ответ: «AsNoTracking — просто оптимизация, её можно ставить везде». Дальше разговор идёт про то, что именно снимает AsNoTracking — снимок исходных значений и identity resolution — и что без снимка SaveChanges физически нечего сравнивать: сущность не сохранится, и никакого исключения при этом не будет.