100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Tracking vs No-Tracking Queries

When to let EF Core track query results for later updates versus opting out with AsNoTracking for faster, read-only workloads.

Performance & TransactionsIntermediate8 min readJul 10, 2026
Analogies

Tracking Queries Are the Default

When you write context.Blogs.Where(b => b.Rating > 3).ToList(), EF Core runs the query, materializes each row into an entity, and registers every one of those entities with the ChangeTracker as Unchanged. This is what makes it possible to mutate a property afterward and have SaveChanges pick it up automatically. The convenience comes at a cost: EF has to keep an identity map (so the same primary key always resolves to the same in-memory instance) and an original-value snapshot for every row, which consumes memory and adds bookkeeping overhead proportional to the number of rows returned.

🏏

Cricket analogy: It's like a scorer logging every ball bowled in a Test match into the official book by default — useful if you need to review the innings later, but overkill if you only wanted today's final score.

AsNoTracking: Opting Out for Read-Only Scenarios

Calling .AsNoTracking() on a query tells EF Core to skip both the identity map and the original-value snapshot for the returned entities — it just materializes objects and hands them back untracked. This is the right default for read-only workloads: API GET endpoints, report generation, or any query whose results you're never going to pass back into SaveChanges. Because there's no snapshot bookkeeping, no-tracking queries are measurably faster and use less memory, especially when materializing large result sets or deeply nested Include graphs.

🏏

Cricket analogy: It's like a fan watching the match purely for entertainment rather than as an official scorer — you absorb the action without maintaining a ball-by-ball ledger you'll never need to reference again.

AsNoTrackingWithIdentityResolution

Plain AsNoTracking() has one gotcha: if the same row appears multiple times across a query result (common with Include on a one-to-many relationship), EF Core materializes a separate object instance for each occurrence instead of resolving them to one shared instance, since there's no identity map to deduplicate against. AsNoTrackingWithIdentityResolution() fixes this by keeping a temporary, weak-referenced identity map scoped only to that single query, so duplicate rows resolve to the same in-memory instance without the overhead of full change tracking or long-lived snapshots.

🏏

Cricket analogy: It's like a stadium selling separate tickets for the same seat number across different gate entrances unless someone cross-checks a master seating chart to make sure Seat 14B always refers to the same physical seat.

Choosing the Right Query Mode

The decision is straightforward once you frame it correctly: use a tracking query (the default) whenever you intend to modify the results and call SaveChanges — for example, loading an order to update its status. Use AsNoTracking() for anything read-only, which in most web applications is the majority of queries, especially GET endpoints and DTO projections. Reach for AsNoTrackingWithIdentityResolution() specifically when you need duplicate-free object graphs from a no-tracking Include query, such as returning a Category with its Products where each Product also references back to the same Category instance.

🏏

Cricket analogy: It's like a captain deciding whether to review a decision with DRS — you only spend the review (tracking overhead) when the outcome actually needs to change, not on every single delivery.

csharp
// Read-only endpoint: skip change tracking entirely
public async Task<List<BlogDto>> GetBlogsAsync()
{
    return await _context.Blogs
        .AsNoTracking()
        .Select(b => new BlogDto { Id = b.BlogId, Url = b.Url })
        .ToListAsync();
}

// Duplicate-free graph from a no-tracking Include query
var categories = await _context.Categories
    .Include(c => c.Products)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

// Update scenario: keep the default tracking behavior
var order = await _context.Orders.FirstAsync(o => o.OrderId == id);
order.Status = OrderStatus.Shipped;
await _context.SaveChangesAsync();

Calling SaveChanges after mutating an entity returned from an AsNoTracking() query will silently do nothing — the entity was never registered with the ChangeTracker, so EF has no snapshot to diff against and generates no SQL.

  • Tracking is EF Core's default: every materialized entity is registered with the ChangeTracker and identity map.
  • AsNoTracking() skips the identity map and snapshot, giving faster, lower-memory results for read-only queries.
  • Mutating an AsNoTracking() entity and calling SaveChanges has no effect — EF isn't watching that instance.
  • AsNoTrackingWithIdentityResolution() adds a query-scoped identity map to deduplicate repeated rows in Include graphs without full tracking overhead.
  • Use tracking queries for anything you plan to modify and persist via SaveChanges.
  • Use AsNoTracking() for GET endpoints, reports, and DTO projections that never round-trip back to SaveChanges.
  • Choose AsNoTrackingWithIdentityResolution() when a no-tracking Include query would otherwise produce duplicate object instances for the same row.

Practice what you learned

Was this page helpful?

Topics covered

#EntityFrameworkCoreStudyNotes#MicrosoftTechnologies#TrackingVsNoTrackingQueries#Tracking#Queries#Default#AsNoTracking#SQL#StudyNotes#SkillVeris