Как работает AsNoTracking и когда его использовать?
Что экономится
При отслеживании для каждой сущности EF:
- Ищет её в Identity Map по ключу.
- Создаёт
InternalEntityEntryи копию значений всех свойств (snapshot) — фактически удваивает память под данные. - Выполняет fix-up навигаций со всеми уже отслеживаемыми сущностями.
- Впоследствии каждый
DetectChangesбудет сравнивать все эти сущности.
Использование
// Список для API — только чтение
var products = await db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Take(50)
.ToListAsync(ct);
// Для всего контекста по умолчанию
builder.Services.AddDbContext<ReadDbContext>(o => o
.UseNpgsql(conn)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
// ...и явное отслеживание, когда нужно изменить
var order = await db.Orders.AsTracking().SingleAsync(o => o.Id == id, ct);
order.MarkPaid();
await db.SaveChangesAsync(ct);Проблема дубликатов и identity resolution
var orders = await db.Orders.AsNoTracking().Include(o => o.Customer).ToListAsync();
// 100 заказов одного клиента → 100 разных экземпляров Customer с одинаковыми данными
Console.WriteLine(ReferenceEquals(orders[0].Customer, orders[1].Customer)); // False
var orders2 = await db.Orders.AsNoTrackingWithIdentityResolution().Include(o => o.Customer).ToListAsync();
Console.WriteLine(ReferenceEquals(orders2[0].Customer, orders2[1].Customer)); // TrueAsNoTrackingWithIdentityResolution использует временную Identity Map только на время запроса: объекты дедуплицируются, но не остаются в Change Tracker. Чуть медленнее обычного AsNoTracking, но экономит память при повторяющихся связанных сущностях и сохраняет корректный граф (важно для сериализации циклических ссылок).
Сравнение
| Tracking | AsNoTracking | AsNoTrackingWithIdentityResolution | |
|---|---|---|---|
| Change Tracker | Да | Нет | Нет |
| Снимки значений | Да | Нет | Нет |
| Дедупликация объектов | Да (весь контекст) | Нет | Да (в рамках запроса) |
| Изменения сохраняются | Да | Нет | Нет |
| Скорость | Базовая | Максимальная | Высокая |
| Для чего | Изменение и сохранение | Чтение, простые графы | Чтение графов с повторами |
Изменение неотслеживаемой сущности
var product = await db.Products.AsNoTracking().SingleAsync(p => p.Id == id);
product.Price = 999;
await db.SaveChangesAsync(); // НИЧЕГО не сохранится — контекст о product не знает
db.Products.Update(product); // отметить ВСЕ свойства как изменённые
await db.SaveChangesAsync(); // UPDATE всех столбцов
// Или точечно:
db.Attach(product);
db.Entry(product).Property(p => p.Price).IsModified = true;
await db.SaveChangesAsync(); // UPDATE только priceКогда AsNoTracking не нужен или вреден
- Вы собираетесь изменить сущность — отслеживание нужно для
SaveChanges. - Запрос уже проецирует в DTO.
- Небольшая выборка в методе, который потом всё равно сохраняет изменения — выигрыш мизерный, а риск «забытого»
Attachреален. - Нужен
Findиз кэша контекста —Findработает только с отслеживаемыми сущностями.
Архитектурный приём: разделение чтения и записи
В CQRS-стиле запросы (queries) работают через AsNoTracking или отдельный read-only контекст (часто — к реплике БД), а команды (commands) — через отслеживающий контекст с доменной моделью.
public sealed class OrderQueries(ReadDbContext db) // NoTracking по умолчанию, реплика
{
public Task<List<OrderListItem>> GetRecentAsync(CancellationToken ct) =>
db.Orders.OrderByDescending(o => o.CreatedAt).Take(20)
.Select(o => new OrderListItem(o.Id, o.Customer.Name, o.Total)).ToListAsync(ct);
}Вопросы на засыпку
Насколько AsNoTracking ускоряет запрос?
Зависит от количества и «ширины» сущностей. На SQL-время не влияет — выигрыш в материализации и памяти. Для тысяч сущностей — десятки процентов, для одной — несущественно. Проверяйте BenchmarkDotNet-ом.
Работает ли lazy loading с AsNoTracking?
Для прокси — нет: ленивой загрузке нужен контекст, который отслеживает сущность; EF выдаст предупреждение/исключение. Это ещё одна причина не полагаться на lazy loading.
Почему после AsNoTracking-запроса Update может обновить связанные сущности?
Update обходит граф навигаций и помечает все достижимые сущности как Modified (или Added, если ключ не задан). Если к продукту прицеплена категория — её строка тоже будет обновлена. Для точечного обновления используйте Attach + IsModified или ExecuteUpdate.
Что быстрее для массового обновления: загрузить с tracking и изменить или ExecuteUpdate?
ExecuteUpdateAsync (EF Core 7+) — один UPDATE ... WHERE без загрузки данных в память. На порядки быстрее для массовых операций, но минует Change Tracker, интерсепторы SaveChanges и доменные события.
Итог
AsNoTracking убирает накладные расходы Change Tracker там, где данные только читаются. Используйте его (или проекции) для всех read-сценариев, AsNoTrackingWithIdentityResolution — для графов с повторяющимися сущностями, а отслеживание оставляйте для изменения и сохранения.