EF Core AsNoTracking: when to use it and when not to (tracking, identity resolution)

EF Core Interview Q&A November 13, 2026 csharp

The change tracker keeps a snapshot of every entity it loads, so SaveChanges can find what changed.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// ======================================================================
// Q: When do you use AsNoTracking? When should you NOT use it?
//
// The change tracker keeps a snapshot of every entity it loads, so SaveChanges
// can find what changed. It also performs identity resolution: the same row =
// the same instance. This costs memory and CPU.
// Use AsNoTracking for:
// - Read-only queries: GET endpoints, lists, reports, exports.
// - Large result sets.
// - Data that goes into a cache.
// Do not use it:
// - When you will modify the entity and call SaveChanges.
// - When you need identity resolution across a graph (then use
//   AsNoTrackingWithIdentityResolution).
// - When you load an aggregate to apply domain rules.
// - Projections to a DTO / anonymous type are not tracked anyway, unless they
//   contain entity instances. AsNoTracking does no harm there, but it is not
//   needed.
// - Find / FindAsync always tracks and checks the tracker first (no query if the
//   entity is already loaded).
// - Bulk imports: with tracking, the tracker grows and every SaveChanges gets
//   slower. Call SaveChanges per batch and then db.ChangeTracker.Clear().
// - Measure the gain (BenchmarkDotNet, memory profiler), do not assume it. On
//   small queries the difference is negligible. On thousands of rows it is
//   significant.
// In the demo project (as admin): GET /api/admin/ef-demo/n-plus-
// one?mode=naive|include|projection shows 41 / 2 / 1 queries for the same data,
// GET /api/admin/ef-demo/tracking?mode=tracking|no-tracking shows 205 vs 0
// tracked entities, and POST /api/admin/ef-demo/concurrency-conflict shows the
// DbUpdateConcurrencyException from Q60. EfCoreDemoTests asserts on the number
// of queries.
// ======================================================================

// ✓ Read-only list
var products = await db.Products.AsNoTracking()
    .Where(p => p.Category == category)
    .OrderBy(p => p.Name)
    .ToListAsync(ct);

// ✓ Update: needs tracking
var product = await db.Products.SingleAsync(p => p.Sku == sku, ct);
product.ListPrice = newPrice;
await db.SaveChangesAsync(ct);          // UPDATE of list_price only

// ✗ AsNoTracking then a change: SaveChanges sees nothing
var p2 = await db.Products.AsNoTracking().SingleAsync(p => p.Sku == sku, ct);
p2.ListPrice = newPrice;
await db.SaveChangesAsync(ct);          // 0 rows. (db.Update(p2) would write ALL columns)

// The same customer across many orders = one instance, without tracking
var orders = await db.Orders.Include(o => o.Customer)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync(ct);

// Read-only context (e.g. CQRS read side): no-tracking by default
builder.Services.AddDbContext<ReadDbContext>(o =>
    o.UseNpgsql(cs).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
// and .AsTracking() in the few places where you write