What the Change Tracker Actually Does
Every time you pull entities out of a DbContext using a tracking query, EF Core doesn't just hand you plain objects — it registers each one with an internal component called the ChangeTracker. That tracker keeps a snapshot of the values each entity had when it was loaded, so that when you call SaveChanges, EF can compare current values against the snapshot and generate only the UPDATE statements that are actually needed.
Cricket analogy: Like a third umpire reviewing a run-out, the ChangeTracker keeps a frozen frame of exactly where every entity stood at load time so it can compare that frame against the current position before deciding what happened.
Entity States
Every tracked entity is assigned one of five EntityState values: Detached, Unchanged, Added, Modified, or Deleted. A freshly queried entity starts as Unchanged; calling context.Add() marks it Added; mutating a scalar property flips it to Modified; and context.Remove() marks it Deleted. SaveChanges walks the tracker, translates each non-Unchanged entity into the corresponding INSERT, UPDATE, or DELETE, and then resets everything back to Unchanged (or Detached for removed rows) once the transaction commits.
Cricket analogy: It's like the five statuses a batter can have on the scorecard — not out, retired, run out, stumped, or caught — each one dictating exactly what entry the scorer writes next to their name.
Detecting Changes: Snapshot Tracking vs Change-Tracking Proxies
By default EF Core uses snapshot change tracking: it clones the original property values into a separate ChangeTracker.Entries() shadow store and performs a value-by-value comparison (DetectChanges) whenever SaveChanges, Entry(), or ChangeTracker.DetectChanges() is invoked. This is convenient because your POCOs stay plain, but DetectChanges has to walk every tracked entity's every property, which can get expensive with thousands of tracked rows. As an alternative, EF Core supports change-tracking proxies (via UseChangeTrackingProxies) that generate dynamic subclasses implementing INotifyPropertyChanged, so changes are flagged immediately on assignment instead of being discovered by a later full scan.
Cricket analogy: It's the difference between Hawk-Eye reconstructing a ball's full trajectory after the fact versus a stump-mic sensor that pings the instant the bail is dislodged — one recomputes everything, the other reacts immediately.
// Inspecting and manipulating entity state directly
using var context = new BloggingContext();
var blog = context.Blogs.First(b => b.BlogId == 1);
Console.WriteLine(context.Entry(blog).State); // Unchanged
blog.Url = "https://updated-example.com";
Console.WriteLine(context.Entry(blog).State); // Modified
// Force EF to recompute change state without calling SaveChanges
context.ChangeTracker.DetectChanges();
foreach (var entry in context.ChangeTracker.Entries())
{
Console.WriteLine($"{entry.Entity.GetType().Name} is {entry.State}");
}
context.SaveChanges(); // Entries reset to Unchanged after commit
ChangeTracker.Entries() lets you inspect every tracked entity and its state before calling SaveChanges — useful for auditing or building generic logging middleware around SaveChanges.
- The ChangeTracker stores an original-value snapshot for every tracked entity so SaveChanges can compute a minimal diff.
- Every tracked entity has one of five states: Detached, Unchanged, Added, Modified, Deleted.
- SaveChanges converts non-Unchanged entities into INSERT, UPDATE, or DELETE statements, then resets state to Unchanged.
- Default snapshot tracking calls DetectChanges to compare current vs. original values, which scales with the number of tracked entities and properties.
- Change-tracking proxies (UseChangeTrackingProxies) avoid full DetectChanges scans by reacting to property assignment immediately via INotifyPropertyChanged.
- ChangeTracker.Entries() exposes every tracked entity's EntityState for debugging, auditing, or logging.
Practice what you learned
1. What does EF Core's ChangeTracker store when a tracking query loads an entity?
2. Which EntityState does a freshly queried, unmodified entity have?
3. What happens to an entity's EntityState immediately after a successful SaveChanges commits an UPDATE for it?
4. What is the main benefit of enabling change-tracking proxies via UseChangeTrackingProxies?
5. Which method call lets you inspect an entity's current EntityState without calling SaveChanges?
Was this page helpful?
You May Also Like
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.
Optimistic Concurrency Control
How EF Core detects and resolves concurrent edit conflicts using concurrency tokens and DbUpdateConcurrencyException, and how this compares to pessimistic locking.
Avoiding the N+1 Query Problem
How lazy loading silently causes one query per entity in a loop, and how Include, projection, and split queries fix it in EF Core.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics