Entity FrameworkMiddleTechnical

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

Sources

Related topics