Chapter 11 · Change Tracking, Entity States, Identity Resolution, and Disconnected Graphs

Identity Resolution, Tracking vs No-Tracking, and AsNoTrackingWithIdentityResolution

Compare tracking, no-tracking, and no-tracking-with-identity-resolution by CLR reference identity, ChangeTracker state, fixup behavior, memory tradeoffs, and update boundaries.

Intermediate115–145 minutesidentity-resolution comparison labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

A reporting query joins one work order to several tags. The relational result repeats the work-order columns once per tag. Should the client receive one CLR WorkOrder object reused three times, or three separate objects with the same primary key? The answer depends on tracking behavior and identity resolution, which are related but not identical.

01

Define identity resolution as one CLR instance per entity key within a tracking scope.

02

Compare tracking, AsNoTracking, and AsNoTrackingWithIdentityResolution using duplicate relational rows.

03

Inspect ReferenceEquals results and context ChangeTracker counts rather than guessing.

04

Explain navigation fixup, update semantics, memory costs, and serialization implications.

05

Reproduce the duplicate-instance InvalidOperationException when two same-key instances are attached.

06

Choose a query tracking mode based on whether the result is an editable unit of work or a read model.

Reproducible baseline

Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and no paid tooling. EF Core 11 preview APIs are not part of the mandatory path.

1. Identity resolution solves an ambiguity, not a SQL problem

A DbContext can track only one entity instance for a given primary-key value. If two different objects both claim to be WorkOrder Id=42, EF would not know which property values or relationships represent the unit of work. Identity resolution therefore maps repeated rows/keys to one CLR instance within the relevant tracker.

Still executes the query

A tracking query is sent to the database. If the returned key is already tracked, EF returns the existing instance rather than overwriting its current/original values with the newly returned database values. Use Reload/GetDatabaseValues when you intentionally need store refresh evidence.

2. Build a duplicate-row query from the existing tag association

csharp · same WorkOrder repeated once per tag link
IQueryable<WorkOrder> DuplicatedOrders(ServiceHubContext db, int id) =>    db.Set<WorkOrderTag>()      .Where(link => link.WorkOrderId == id)      .OrderBy(link => link.DisplayOrder)      .Select(link => link.WorkOrder);

If the work order has three tag links, the SQL can return three rows representing the same work-order key. This is ideal for observing materialization identity without adding a fake schema just for the lesson.

3. Tracking: one context instance per key

csharp · tracking identity resolution
await using var trackingDb = await factory.CreateDbContextAsync(ct);var rows = await DuplicatedOrders(trackingDb, id).ToListAsync(ct);Console.WriteLine($"Rows: {rows.Count}");Console.WriteLine($"Same CLR instance: {ReferenceEquals(rows[0], rows[1])}");Console.WriteLine($"Tracked WorkOrders: {trackingDb.ChangeTracker.Entries<WorkOrder>().Count()}");

Repeated database rows can appear multiple times in the result list, but each occurrence refers to the same tracked WorkOrder object. Navigation fixup can then connect this object to other tracked related entities. Any later detected modifications can participate in SaveChanges.

4. AsNoTracking: database-shaped instances, no context state

csharp · no tracking, no identity resolution
await using var noTrackDb = await factory.CreateDbContextAsync(ct);var rows = await DuplicatedOrders(noTrackDb, id)    .AsNoTracking()    .ToListAsync(ct);Console.WriteLine($"Same CLR instance: {ReferenceEquals(rows[0], rows[1])}");Console.WriteLine($"Context tracked entries: {noTrackDb.ChangeTracker.Entries().Count()}");

For ordinary no-tracking entity queries, repeated entity rows can materialize as distinct CLR objects. This often suits read-only streaming/serialization because the context does not retain snapshots and state, but duplicate instances can increase allocation and can surprise reference-equality logic.

5. AsNoTrackingWithIdentityResolution: temporary identity map, no unit-of-work tracking

csharp · deduplicate materialization without tracking for SaveChanges
await using var identityDb = await factory.CreateDbContextAsync(ct);var rows = await DuplicatedOrders(identityDb, id)    .AsNoTrackingWithIdentityResolution()    .ToListAsync(ct);Console.WriteLine($"Same CLR instance: {ReferenceEquals(rows[0], rows[1])}");Console.WriteLine($"Context tracked entries: {identityDb.ChangeTracker.Entries().Count()}");

EF uses a stand-alone tracker while enumerating the result so each key is materialized once, but those entities are not attached to the context's normal change tracker. After enumeration, the temporary identity-resolution tracker can be discarded.

Mode Identity resolution Tracked by DbContext Typical fit
Tracking Yes Yes Editable unit of work; relationship fixup.
AsNoTracking No No Simple read model/stream where duplicate instances are acceptable.
AsNoTrackingWithIdentityResolution Yes, per query enumeration No Read graph with repeated entity keys where reference reuse matters.

6. Deliberately wrong: attach a second same-key object

csharp · identity conflict
var tracked = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);var duplicate = WorkOrder.RehydrateForAssignment(id, tracked.Revision);try{    db.Attach(duplicate);}catch (InvalidOperationException ex){    Console.WriteLine(ex.Message);}

The expected mechanism is not a database unique-key error. It is a client-side identity-map conflict: this context already tracks another WorkOrder with the same key. Repair by using the already tracked instance, or start a fresh unit of work—not by detaching arbitrary entities until the exception disappears.

7. Tracking queries can return existing stale client values

csharp · store changes do not overwrite a tracked instance automatically
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReviseSummary("Unsaved local draft");// The query still goes to the database, but identity resolution returns// the already tracked WorkOrder instance for the same key.var queriedAgain = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);Console.WriteLine(ReferenceEquals(order, queriedAgain)); // trueConsole.WriteLine(queriedAgain.Summary);                 // local tracked valuevar databaseValues = await db.Entry(order).GetDatabaseValuesAsync(ct);

This is a feature of a unit of work: current client state is not silently overwritten by another query. It is also why long-lived contexts become stale and why a fresh context is usually the right read boundary.

8. Projections can still track entities

csharp · entity inside a projection remains trackable by default
var rows = await db.WorkOrders    .Select(w => new    {        Entity = w,        NoteCount = w.Notes.Count    })    .ToListAsync(ct);Console.WriteLine(db.ChangeTracker.Entries<WorkOrder>().Count());

Returning an anonymous/DTO-like outer object does not automatically mean “no tracking” if the projection contains entity instances. A scalar-only projection with no entity object has nothing to track. Be explicit at read boundaries.

9. Hands-on lab: prove reference identity three ways

  1. Seed one work order with at least three WorkOrderTag links.
  2. Run the duplicate-row query in three fresh contexts: tracking, no-tracking, and no-tracking-with-identity-resolution.
  3. Record list count, ReferenceEquals(rows[0], rows[1]), and context tracker count.
  4. In a tracking context, modify the returned instance and confirm SaveChanges sees it.
  5. In a no-tracking context, modify the returned object and confirm no write occurs until you deliberately attach/update it.
  6. Trigger the duplicate same-key attach exception and capture the exact message.

Check your understanding

  1. What invariant does identity resolution enforce in a DbContext?
  2. Does AsNoTracking perform ordinary identity resolution?
  3. What does AsNoTrackingWithIdentityResolution add?
  4. Does a tracking query overwrite local tracked current values with database values?
  5. Can a projection still cause tracking?
  6. Why is attaching two same-key instances rejected?
Review the answers

At most one tracked CLR entity instance for a given entity type/key.

No; repeated rows can materialize as distinct CLR objects.

A stand-alone tracker for per-query identity reuse without attaching results to the DbContext tracker.

No; when the key is already tracked, the existing instance is reused. Reload/GetDatabaseValues are explicit refresh tools.

Yes, if the projection contains entity instances and the query is tracking.

EF would have ambiguous property and relationship state for one database identity.

10. Production judgment and bridge

Choose tracking because the unit of work will edit entities—not because it is the default. Choose ordinary no-tracking for read models where duplicate object identity is irrelevant. Add identity resolution only when repeated entity keys materially affect result consistency/allocation. Lesson 4 moves across a process/request boundary where the original tracker is gone and the application must reconstruct write intent safely.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n