.NETHard
Explain change tracking and AsNoTracking in EF Core
EF Core's change tracker records every entity it loads. When you call SaveChanges() it compares the current state to the original snapshot and emits the appropriate INSERT/UPDATE/DELETE statements.
var product = await db.Products.FirstAsync(p => p.Id == id); // tracked
product.Price = 99.99m; // change tracked
await db.SaveChangesAsync(); // UPDATE issued
The cost:
- Memory overhead: 2x the entity (current + original snapshot)
- CPU: SaveChanges scans every tracked entity even if untouched
- Hidden writes: a lazy-load proxy can mark an entity "dirty" by side effect
AsNoTracking — for read-only queries:
var products = await db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
Skips the snapshot, halves memory, doubles read throughput on big result sets.
AsNoTrackingWithIdentityResolution — useful when joining and you want the same logical entity to be a single object reference (but still untracked):
var orders = await db.Orders
.Include(o => o.Customer)
.AsNoTrackingWithIdentityResolution()
.ToListAsync();
Default for read-mostly contexts:
optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
Then opt-in to tracking explicitly for writes with .AsTracking().
Rule: anything you'll mutate, track. Anything you'll display, don't.