EF Core AsNoTracking: when to use it and when not to (tracking, identity resolution)
The change tracker keeps a snapshot of every entity it loads, so SaveChanges can find what changed.
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 |