Understanding AsTracking vs AsNoTracking in EF Core
Understanding AsTracking vs AsNoTracking in EF Core
Why Tracking Behavior Matters
Every time Entity Framework Core materializes entities from a query, it decides whether to track those entities in the DbContext's change tracker. Tracking is what lets EF Core detect changes and generate UPDATE/DELETE statements when you call SaveChangesAsync(). That capability isn't free — it costs memory (snapshotting original values, maintaining an identity map) and CPU cycles. In many read-only code paths, that cost buys you nothing.
The Default Behavior
By default, any LINQ query against a DbSet<T> is tracked:
public async Task<List<Order>> GetPendingOrdersAsync(string customerId){
This is implicitly tracked. EF Core snapshots every Order entity returned, in case one needs updating later via SaveChangesAsync(). If this method is part of a "fetch, mutate, save" workflow, tracking is doing useful work.
When AsNoTracking() Is the Better Choice
Not every query needs tracking. Read-only lookups — checking a permission, rendering a list, or validating a condition — don't need change tracking at all:
// Read-only check — no need to track
AsNoTracking() skips snapshotting entirely, which:
- Reduces memory usage (no identity map entries, no original-value snapshots).
- Speeds up query materialization, especially for larger result sets.
- Avoids "phantom updates" — a subtle bug where an entity is loaded, mutated somewhere down the call stack, and unexpectedly persisted on the next
SaveChangesAsync().
When to Use AsTracking() Explicitly
If your DbContext is configured with a default of QueryTrackingBehavior.NoTracking (e.g., via UseQueryTrackingBehavior), you can flip individual queries back to tracked mode with .AsTracking():
var order = await _context.Orders
Without AsTracking() here, the change to Status would silently be lost — EF Core wouldn't know the entity changed because it never took a baseline snapshot.
A Practical Rule of Thumb
For services that mix read-heavy queries with write-heavy updates:
- Default the context to
NoTrackingat theDbContextOptionsBuilderlevel for maximum read performance. - Explicitly call
.AsTracking()on any query whose results will be mutated and saved later in the same unit of work. - Keep "fetch → mutate → save" workflows tracked.
- Keep authorization/validation queries as
AsNoTracking(), since they only inform a decision and are never saved.
Takeaway
Tracking isn't free, and it isn't always necessary. Auditing your EF Core queries — separating "I need to save this later" from "I just need to read this" — is a low-risk, high-value optimization, especially in systems where a shared DbContext handles both read-heavy validation logic and write-heavy processing logic.
Comments
Be the first to post a comment