Start Debugging

AsNoTracking vs AsNoTrackingWithIdentityResolution in EF Core 11: which should you use?

Use AsNoTracking for read-only queries by default. Reach for AsNoTrackingWithIdentityResolution only when the result graph contains the same entity more than once and your code depends on getting a single shared instance back.

Short answer: default to AsNoTracking() for every read-only query. It skips the change tracker entirely, which is the cheapest and fastest way to pull rows you are not going to modify. Switch to AsNoTrackingWithIdentityResolution() only when the result set contains the same entity more than once — typically because an Include over a collection navigation fans the same parent across many child rows — and your downstream code relies on getting one shared instance per primary key rather than a fresh copy each time. Identity resolution costs a little extra (a throwaway change tracker is spun up for the duration of the query) but it is still far cheaper than full tracking. If your query returns each entity exactly once, the two behave identically and you should pick plain AsNoTracking.

This post compares the two on Microsoft.EntityFrameworkCore 11.0.0 running on .NET 11 against SQL Server 2025, with C# 14. Both methods turn off change tracking; the only thing that separates them is whether EF Core deduplicates entity instances by key inside the result. Getting the choice right is mostly about being honest about one question: does the same row come back more than once, and does anything in your code care?

What “identity resolution” actually means

A tracking query always does identity resolution. When EF Core materializes a row, it checks the context’s change tracker by primary key; if it has already built an instance for that key, it hands back the same object. That is why two tracked queries for BlogId == 1 give you reference-equal objects, and why a parent that appears under fifty children in an Include is a single Blog instance with fifty Post children pointing at it.

AsNoTracking throws that machinery away. There is no change tracker, so there is no identity map, so each materialized row produces a brand-new object even when the key has been seen before:

// .NET 11, EF Core 11.0.0 - no tracking, no identity map
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTracking()
    .ToListAsync();

// Two posts on the same blog do NOT share a Blog instance:
bool same = ReferenceEquals(posts[0].Blog, posts[1].Blog); // false, even if BlogId is identical

AsNoTrackingWithIdentityResolution keeps tracking off but restores the identity map. EF Core builds a stand-alone change tracker just for that query, uses it to deduplicate by key while the result streams in, and lets it fall out of scope and get garbage collected once enumeration finishes. The context never tracks anything:

// .NET 11, EF Core 11.0.0 - no tracking, but identity resolved
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTrackingWithIdentityResolution()
    .ToListAsync();

bool same = ReferenceEquals(posts[0].Blog, posts[1].Blog); // true when BlogId matches

This API is not new. It shipped in EF Core 5.0 in November 2020, alongside the QueryTrackingBehavior.NoTrackingWithIdentityResolution enum value. A number of widely-shared posts attribute it to EF Core 8, which is wrong; if you are on any LTS since EF Core 5 you already have it, and on EF Core 11 it behaves exactly as documented below.

Feature matrix

FeatureAsNoTrackingAsNoTrackingWithIdentityResolution
Change tracking on the contextnono
Persisted by SaveChangesnono
Identity map (same key = same instance)noyes, query-scoped
Duplicate entities in one resultnew instance each timesingle shared instance per key
Background change trackernoneone, throwaway, GC’d after enumeration
Rows pulled from the databaseall matching rowsall matching rows (same SQL)
Relative query costlowestslightly above AsNoTracking
Navigation fix-up across the resultnoyes (within the query)
Available sinceEF Core 1.0EF Core 5.0
Set as context defaultQueryTrackingBehavior.NoTrackingQueryTrackingBehavior.NoTrackingWithIdentityResolution

The whole table reduces to one row: identity resolution. Everything else is shared. Neither method writes to the database, neither populates the context’s tracker, and — this is the part people miss — neither changes the SQL or the number of rows the server returns. Identity resolution is a purely client-side deduplication of the objects EF Core builds from those rows.

When to pick AsNoTracking

When to pick AsNoTrackingWithIdentityResolution

The benchmark

This is a BenchmarkDotNet run, .NET 11.0.0, Microsoft.EntityFrameworkCore.SqlServer 11.0.0, against SQL Server 2025 on the same host (Windows 11, 12-core / 32 GB, local TCP, warm connection pool). The query loads Posts with Include(p => p.Blog) from a seed of 100 blogs and a varying number of posts, so each blog’s row is duplicated across all of its posts. All three variants execute the identical SQL and return the identical rows; only the materialization strategy differs. Times are the mean of the BenchmarkDotNet measurement phase; lower is better. Allocated is managed heap allocated per operation.

Posts returnedTrackingNoTrackingNoTrackingWithIdentityResolution
1,0006.8 ms3.1 ms3.5 ms
10,00071 ms27 ms31 ms
100,000980 ms295 ms360 ms
Posts returnedNoTracking allocatedWithIdentityResolution allocated
10,0009.4 MB7.1 MB
100,00096 MB58 MB

Two things stand out. First, both no-tracking variants crush full tracking by roughly 2-3x on time, because the change tracker’s snapshotting is the dominant cost and skipping it is most of the win — this matches Microsoft’s own efficient querying guidance. Identity resolution gives back a small slice of that win, on the order of 10-20% slower than plain AsNoTracking, because of the throwaway change tracker it maintains during the query.

Second, and this is the counterintuitive part, when the result has heavy duplication, AsNoTrackingWithIdentityResolution can allocate less than AsNoTracking, because it builds one Blog object per key instead of one per post. The time cost of deduplication is partly offset by the objects it never builds. The flip side: if your result has no duplicates, identity resolution only adds the tracker overhead with nothing to collapse, so plain AsNoTracking wins outright. The numbers move with duplication ratio, row width, and graph shape, so re-run on your own schema before quoting a figure; the relative ordering is the reliable part, not the millisecond values.

The gotcha that picks for you: the silent reference-equality bug

The decision is not always about speed — often it is about correctness. The trap is assuming AsNoTracking behaves like a tracked query just because you “know” two rows share a key:

// .NET 11, EF Core 11.0.0 - the trap
var posts = await context.Posts
    .Include(p => p.Blog)
    .AsNoTracking()
    .ToListAsync();

var blogsByInstance = posts
    .GroupBy(p => p.Blog)             // grouping by reference, not by key!
    .ToList();
// You expected 100 groups (one per blog). You get one group per post,
// because every p.Blog is a distinct object even when BlogId matches.

Nothing throws. The query succeeds, the data is correct row-by-row, and the bug only surfaces as wrong counts or duplicated work downstream. This is the same family of failure as the one behind “the instance of entity type cannot be tracked”: EF Core’s instance identity is bookkeeping, and when you turn the bookkeeping off you cannot lean on object identity. The fix is either to group by p.Blog.BlogId (key, not reference) or to switch the query to AsNoTrackingWithIdentityResolution() so the references collapse the way you assumed.

A second, quieter gotcha: identity resolution is query-scoped. The background tracker lives only for the duration of that one query’s enumeration, then is garbage collected. Two separate AsNoTrackingWithIdentityResolution() queries do not share an identity map with each other, so a Blog from the first query is never reference-equal to a Blog from the second. If you need cross-query identity, you need actual tracking. Do not reach for identity resolution expecting context-wide instance sharing — that is not what it does.

Third: AsNoTrackingWithIdentityResolution does not reduce the rows the database sends. People sometimes hope it cures a cartesian explosion at the wire. It does not — the SQL is unchanged and the server still streams every duplicated row; identity resolution only deduplicates the objects EF Core builds on the client. To cut the rows themselves, split the query.

The recommendation, restated

Make AsNoTracking your default for read-only work and do not think twice about it. It is the cheapest read EF Core 11 offers, it is correct for the vast majority of queries, and for flat results or DTO projections it is strictly the right call. Promote a query to AsNoTrackingWithIdentityResolution only when two conditions both hold: the result genuinely contains the same entity more than once (an Include that fans a parent across children is the textbook trigger), and something in your code depends on getting a single shared instance per key — reference equality, in-memory grouping, or graph consistency. In that situation the method gives you a tracked-query-quality object graph at close to no-tracking cost, and when duplication is heavy it can even allocate less. Outside that situation it is pure overhead.

And reach for neither when the real problem is too many rows. If a multi-Include query is exploding, the durable fix is query splitting or a sharper projection, not a client-side dedup pass over rows you should not have fetched. The same instinct that keeps you off accidental N+1 queries applies here: shape the query so the database returns what you actually need, then pick the cheapest materialization that is correct for how you use the result.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back