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.
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.
Explain snapshot change tracking and the difference between direct CLR mutation and EF-aware state APIs.
Observe DebugView before and after DetectChanges without assuming DebugView triggers a scan.
Identify methods that automatically invoke full or local change detection.
Use property IsModified flags to distinguish changed values from entity-level Modified state.
Reproduce a missed update caused by disabled automatic detection and repair it safely.
Measure change-detection cost before considering optimization, including ValueComparer implications.
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.
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
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
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;
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
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
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.
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
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
- Query one deterministic work order in a fresh context.
-
Mutate Summary/Revision through domain methods and print
DebugView.LongViewbefore detection. -
Call
DetectChanges(), print again, and list modified properties. -
Reset; disable
AutoDetectChangesEnabled, mutate directly, call SaveChanges, and verify the database row did not change. -
Repair with one explicit
DetectChanges()before save while the flag is disabled. - Repeat with 10, 100, and 1,000 seeded tracked rows only if your machine can do so comfortably; record rather than invent timings.
-
Restore the flag in
finallyand assert it is true afterward.
Check your understanding
- What does snapshot tracking store?
- Does ChangeTracker.DebugView force a full DetectChanges?
- Name two operations that normally trigger full detection.
- What can happen if AutoDetectChangesEnabled is false and you forget DetectChanges?
- When should detection be disabled?
- 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
- Change Detection and Notifications - EF Core — snapshot detection, automatic triggers, disabling guidance, notification strategies.
- Change Tracker Debugging - EF Core — ShortView/LongView and tracker diagnostics.
- AutoDetectChangesEnabled API — default true and correctness responsibility when disabled.
- Value Comparers - EF Core — snapshot/equality semantics for converted/mutable values.
- EF Core Performance — measure before applying performance features.