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

Snapshot Change Detection, DetectChanges, AutoDetectChangesEnabled, and Cost Control

Make snapshot change detection observable, distinguish direct CLR mutation from EF-aware state changes, and optimize AutoDetectChanges only from measured evidence without losing updates.

Intermediate → Advanced125–160 minutesDetectChanges correctness/perf labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

ServiceHub's domain methods mutate ordinary CLR properties and backing fields. EF cannot intercept every assignment automatically, so its default strategy stores an original snapshot and later compares current values against it. This lesson makes that scan visible, including a correctness failure where AutoDetectChangesEnabled is disabled and a save silently has nothing to write.

01

Explain snapshot change tracking and the difference between direct CLR mutation and EF-aware state APIs.

02

Observe DebugView before and after DetectChanges without assuming DebugView triggers a scan.

03

Identify methods that automatically invoke full or local change detection.

04

Use property IsModified flags to distinguish changed values from entity-level Modified state.

05

Reproduce a missed update caused by disabled automatic detection and repair it safely.

06

Measure change-detection cost before considering optimization, including ValueComparer implications.

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. Snapshot tracking: compare “then” with “now”

When a normal entity is tracked from a query, EF records enough original values to detect later changes. Snapshot change detection compares those originals with current CLR values. For converted or mutable structures, Chapter 06's ValueComparer can participate in snapshot/copy/equality semantics.

csharp · normal domain mutation before a scan
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);Console.WriteLine(db.Entry(order).State); // Unchangedorder.ReviseSummary("Inspect bearing temperature and pump vibration");order.AdvanceRevision();// Do not call Entry/Entries/HasChanges yet: those can trigger detection.Console.WriteLine(db.ChangeTracker.DebugView.LongView);

The debug view can show current property values read directly from the entity while the entry still says Unchanged. That apparent contradiction is useful evidence: the object changed, but EF has not yet completed a change-detection scan.

2. DetectChanges updates state and property flags

csharp · force a full scan
db.ChangeTracker.DetectChanges();Console.WriteLine(db.ChangeTracker.DebugView.LongView);var entry = db.Entry(order);Console.WriteLine(entry.State); // Modifiedforeach (var p in entry.Properties.Where(p => p.IsModified)){    Console.WriteLine($"{p.Metadata.Name}: {p.OriginalValue} -> {p.CurrentValue}");}

After detection, entity state becomes Modified if at least one mapped property is marked modified. EF then has the information required to build a targeted UPDATE. Relationship changes can also be discovered and may cause FK modifications, fixup, or orphan/cascade state changes.

3. Automatic detection occurs at specific EF boundaries

EF invokes detection where stale state could change the answer or write. A full scan normally occurs before SaveChanges/SaveChangesAsync, ChangeTracker.Entries(), HasChanges(), CascadeChanges(), and access to DbSet.Local. DbContext.Entry and member entry APIs can perform local detection for one entity. Local detection is not guaranteed to discover graph-wide cascade effects.

Operation Detection behavior Why it matters
SaveChangesAsync Full detection by default Writes reflect ordinary CLR mutations.
ChangeTracker.Entries() Full detection Returned state list is current.
ChangeTracker.HasChanges() Full detection Boolean is accurate.
DbContext.Entry(entity) Local detection Entry state/property information is refreshed for that entity.
DebugView Does not itself force full detection Useful for showing before/after detection differences.

4. Deliberately wrong: disable AutoDetectChanges and lose an update

csharp · broken optimization
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);db.ChangeTracker.AutoDetectChangesEnabled = false;order.ReviseSummary("This change can remain undetected");order.AdvanceRevision();// With auto detection disabled and no explicit DetectChanges call,// SaveChanges does not discover the direct CLR mutations.var rows = await db.SaveChangesAsync(ct);Console.WriteLine($"Entries written: {rows}");db.ChangeTracker.AutoDetectChangesEnabled = true;
Why this is dangerous

The application can complete without an exception while the intended update is absent. A performance toggle just became a correctness bug.

5. Repair: restore in finally and detect exactly when needed

csharp · safe explicit-detection pattern
var previous = db.ChangeTracker.AutoDetectChangesEnabled;try{    db.ChangeTracker.AutoDetectChangesEnabled = false;    foreach (var order in batch)    {        order.AdvanceRevision();        // apply measured high-volume changes    }    db.ChangeTracker.DetectChanges(); // one intentional full scan    await db.SaveChangesAsync(ct);    // no second automatic scan while disabled}finally{    db.ChangeTracker.AutoDetectChangesEnabled = previous;}

This pattern only makes sense after profiling shows repeated detection is significant. If the loop calls APIs such as Entries(), Entry(...), or other operations that themselves detect changes, the supposed optimization may vanish or become difficult to reason about.

6. EF-aware state APIs can mark changes immediately

csharp · PropertyEntry CurrentValue is EF-aware
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);var summary = db.Entry(order).Property(x => x.Summary);summary.CurrentValue = "Inspect mechanical seal before restart";Console.WriteLine(summary.IsModified); // EF knows about this assignment

Calling EF APIs is not a recommended replacement for normal domain methods. It simply demonstrates the mechanism: an EF API can update tracker state immediately because the state manager is involved directly, while ordinary property/backing-field mutation needs detection under snapshot tracking.

7. Notification strategies exist, but snapshot tracking is the baseline

Entities implementing INotifyPropertyChanging/INotifyPropertyChanged, or change-tracking proxies, can notify EF about changes as they occur. That can avoid snapshot scans for configured entity types, but it adds domain plumbing/proxy constraints. The ServiceHub course keeps snapshot tracking as the default because it is simpler and is the recommended fit for most applications.

Do not mix mechanisms casually

A notification strategy must be configured consistently and the entity must raise the required events for every tracked property/navigation. Partial implementation creates harder-to-debug state than ordinary snapshots.

8. Performance: measure scans separately from database work

csharp · repeatable measurement skeleton
var sw = Stopwatch.StartNew();for (var i = 0; i < iterations; i++){    db.ChangeTracker.DetectChanges();}sw.Stop();Console.WriteLine($"Tracked entries: {db.ChangeTracker.Entries().Count()}");Console.WriteLine($"Iterations: {iterations}");Console.WriteLine($"Elapsed: {sw.Elapsed}");

Record Release/Debug build, machine/runtime, tracked entry count and types, property counts, relationship density, converter/comparer usage, and whether logging/debugger instrumentation is active. Do not extrapolate a universal “disable above N entities” threshold. Microsoft explicitly notes that detection is not a bottleneck for most applications and becomes relevant only for some large tracked graphs.

9. Hands-on lab: make the stale-state bug undeniable

  1. Query one deterministic work order in a fresh context.
  2. Mutate Summary/Revision through domain methods and print DebugView.LongView before detection.
  3. Call DetectChanges(), print again, and list modified properties.
  4. Reset; disable AutoDetectChangesEnabled, mutate directly, call SaveChanges, and verify the database row did not change.
  5. Repair with one explicit DetectChanges() before save while the flag is disabled.
  6. Repeat with 10, 100, and 1,000 seeded tracked rows only if your machine can do so comfortably; record rather than invent timings.
  7. Restore the flag in finally and assert it is true afterward.

Check your understanding

  1. What does snapshot tracking store?
  2. Does ChangeTracker.DebugView force a full DetectChanges?
  3. Name two operations that normally trigger full detection.
  4. What can happen if AutoDetectChangesEnabled is false and you forget DetectChanges?
  5. When should detection be disabled?
  6. Why can ValueComparer matter here?
Review the answers

Original state/value information that EF can compare with current CLR values later.

No; this is why it can show current values while state still appears Unchanged.

SaveChanges/SaveChangesAsync, Entries(), HasChanges(), CascadeChanges(), and DbSet.Local are documented examples.

Direct CLR mutations may not be marked Modified and therefore may not be persisted.

Only after profiling a realistic workload shows detection is materially expensive and the code can preserve correctness.

Snapshot tracking needs equality/snapshot/hash semantics for converted or mutable values; a comparer can define those semantics.

10. Production judgment and bridge

Keep automatic change detection enabled by default. Optimize it only inside a small, measured scope with explicit detection and guaranteed restoration. The next lesson separates another pair of ideas often conflated with tracking cost: change tracking and identity resolution. You can request one without the other using AsNoTrackingWithIdentityResolution.

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