В чём разница между AsNoTracking() и отслеживаемыми запросами, и когда использовать каждый вариант?
AsNoTracking() отключает слежение за сущностями в ChangeTracker: EF Core не делает снимок и не регистрирует изменения. Используйте для read-only запросов — экономит память и CPU. Для операций записи нужен отслеживаемый запрос.
Что такое change tracking и зачем он нужен
По умолчанию EF Core регистрирует каждую загруженную сущность в ChangeTracker. При вызове SaveChangesAsync() EF сравнивает текущее состояние объектов со снимком, сделанным в момент загрузки, и генерирует UPDATE/INSERT/DELETE только там, где есть расхождения. Это удобно для сценариев редактирования, но дорого обходится при чтении — создаётся снимок каждой строки, объекты помещаются в словарь identity map, и при большом результирующем наборе давление на GC заметно растёт.
Как работает AsNoTracking()
AsNoTracking() — расширяющий метод из пространства имён Microsoft.EntityFrameworkCore, который устанавливает QueryTrackingBehavior.NoTracking для конкретного запроса. EF Core материализует сущности в памяти, но не кладёт их в ChangeTracker, не делает snapshot и не строит identity map для этого запроса. Результат: меньше аллокаций, быстрее выполнение, никакого влияния на SaveChangesAsync().
// Пример: read-only список для страницы каталога
var products = await context.Products
.AsNoTracking()
.Where(p => p.CategoryId == categoryId && p.IsActive)
.OrderBy(p => p.Name)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.ToListAsync(cancellationToken);
Можно также задать поведение глобально для контекста через DbContextOptionsBuilder:
services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
При глобальном NoTracking отслеживание для конкретного запроса можно вернуть через AsTracking().
Когда использовать AsNoTracking()
- Страницы списков, каталоги, отчёты — любое чтение без последующей записи через тот же
DbContext. - API-эндпоинты, возвращающие DTO: объекты всё равно отображаются в другой тип, tracking бесполезен.
- Фоновые задачи, генерирующие большие выборки (тысячи строк).
- Сценарии с коротким lifetime контекста (scoped per request) — экономия заметна при сотнях запросов в секунду.
Когда нужен отслеживаемый запрос
- Загрузка сущности с намерением изменить её и сохранить через
SaveChangesAsync(). - Использование identity map: два запроса возвращают одну и ту же строку — EF возвращает один и тот же экземпляр объекта.
- Каскадные операции: EF автоматически отслеживает связанные сущности и применяет правила cascade delete.
// Пример отслеживаемого запроса для обновления
var order = await context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order is null)
return Results.NotFound();
order.Status = OrderStatus.Confirmed;
order.ConfirmedAt = DateTime.UtcNow;
await context.SaveChangesAsync(cancellationToken);
AsNoTrackingWithIdentityResolution()
EF Core 5+ добавил промежуточный режим: AsNoTrackingWithIdentityResolution(). Он не регистрирует сущности в ChangeTracker, но всё же строит локальный identity map в рамках одного запроса. Это нужно, когда результат содержит повторяющиеся строки (например, JOIN с коллекцией), и вы хотите получить единственный экземпляр объекта вместо дублей. Стоимость — дополнительная аллокация под временный словарь.
Подводные камни
- Неожиданное поведение Attach/Update. Если загрузить сущность через
AsNoTracking(), а потом передать её вcontext.Update(entity), EF пометит все свойства как Modified и обновит каждое поле, даже неизменённое. Это генерирует избыточный UPDATE и может перезаписать чужие изменения. - Lazy loading не работает. Lazy-навигации (
virtualсвойства с прокси) требуют отслеживания. ПриNoTrackingпрокси не создаются или не работают — связанные коллекции останутсяnull. - Глобальный NoTracking и случайные записи. Если установить
UseQueryTrackingBehavior(NoTracking)глобально, а потом забыть добавитьAsTracking()там, где нужна запись,SaveChangesAsync()молча ничего не сохранит — сущности не в трекере. - Дубли при Include без IdentityResolution. Запрос с
Includeна коллекцию иAsNoTracking()может вернуть дублированные родительские объекты, если не использоватьAsNoTrackingWithIdentityResolution()илиAsSplitQuery(). - Мнимая экономия при Select в DTO. Если вы проецируете результат в DTO через
Select(), EF Core и так не создаёт отслеживаемые сущности — tracking тут не применяется.AsNoTracking()в этом случае не даёт прироста. - Snapshot при автоматическом detect changes. По умолчанию EF Core вызывает
DetectChanges()перед сохранением. При большом числе отслеживаемых сущностей это O(n) операция.AsNoTracking()— правильное решение, но альтернатива — отключить автоматическийDetectChangesи вызывать вручную. - Thread safety.
DbContextне потокобезопасен.AsNoTracking()не решает эту проблему — один контекст нельзя использовать из нескольких потоков одновременно. - Версионная разница EF6 vs EF Core. В EF6 метод называется
AsNoTracking()на уровнеDbQuery, поведение схоже, но API конфигурации и возможности (например,AsNoTrackingWithIdentityResolution) существуют только в EF Core.
Common mistakes
- Путать AsNoTracking и стоимость change tracking с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет AsNoTracking и стоимость change tracking на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.