Understanding Change Tracking in EF Core
Change tracking is one of the most important features in EF Core. It is the mechanism that allows EF Core to detect what has changed in your entities and generate the correct SQL when you call SaveChanges. In this lesson, you will learn how change tracking works under the hood and why it matters.
What is Change Tracking?
When you query entities from the database using EF Core, the DbContext does not just hand you plain objects. It also remembers the state of every entity it loaded. This process is called change tracking.
The Change Tracker is a component inside the DbContext that:
- Records the original values of every entity property when the entity is loaded
- Monitors entities for modifications during their lifetime
- Computes the differences between original and current values when
SaveChangesis called - Generates SQL (INSERT, UPDATE, DELETE) based on those differences
csharp1using var context = new BlogContext();23// EF Core loads the post AND takes a snapshot of its current values4var post = await context.Posts.FirstAsync(p => p.Id == 1);56// You modify a property on the C# object7post.Title = "Updated Title";89// SaveChanges detects the change and generates an UPDATE statement10await context.SaveChangesAsync();11// SQL: UPDATE Posts SET Title = 'Updated Title' WHERE Id = 1
Without change tracking, EF Core would have no way of knowing which properties changed, and it would either have to update every column or require you to tell it explicitly.
Snapshot Change Tracking
EF Core uses snapshot change tracking by default. When an entity is loaded from the database, EF Core takes a snapshot (a copy) of all its property values. When SaveChanges is called, EF Core compares the current values of each property against the snapshot to determine what changed.
1Load entity from database2 ↓3Take snapshot: { Title = "Original", Content = "..." }4 ↓5Application modifies: post.Title = "Updated"6 ↓7SaveChanges compares:8 Title: "Original" → "Updated" (CHANGED)9 Content: "..." → "..." (unchanged)10 ↓11Generate SQL: UPDATE Posts SET Title = 'Updated' WHERE Id = 1
This approach is simple and works with any POCO class. You do not need to implement special interfaces or inherit from a base class. EF Core handles everything automatically.
DetectChanges
The comparison between snapshots and current values happens during a process called DetectChanges. EF Core calls DetectChanges automatically before SaveChanges and when you access ChangeTracker.Entries(). You can also call it manually:
csharp1context.ChangeTracker.DetectChanges();
In most cases, you never need to call this manually. EF Core calls it at the right times.
How SaveChanges Uses Tracked Changes
When you call SaveChangesAsync(), EF Core goes through several steps:
- DetectChanges - Compares current values to snapshots for all tracked entities
- Generate SQL - Creates INSERT, UPDATE, or DELETE statements based on entity states
- Begin transaction - Wraps all changes in a database transaction
- Execute SQL - Sends the generated statements to the database
- Update tracking state - Marks entities as
Unchangedafter a successful save - Commit transaction - Commits the transaction (or rolls back on failure)
csharp1using var context = new BlogContext();23var blog = new Blog { Title = "My Blog", Url = "https://myblog.com" };4context.Blogs.Add(blog); // Tracked as Added56var post = await context.Posts.FirstAsync();7post.Title = "New Title"; // Tracked as Modified89var old = await context.Posts.FindAsync(99);10context.Posts.Remove(old); // Tracked as Deleted1112// SaveChanges generates and executes three SQL statements in one transaction:13// INSERT INTO Blogs (Title, Url) VALUES ('My Blog', 'https://myblog.com')14// UPDATE Posts SET Title = 'New Title' WHERE Id = ...15// DELETE FROM Posts WHERE Id = 9916await context.SaveChangesAsync();
All three operations are sent to the database within a single transaction. If any statement fails, the entire batch is rolled back.
When Change Tracking is Active
Change tracking is active by default for all queries that return entities. This means every entity returned by a LINQ query is tracked by the context:
csharp1// These entities are tracked2var posts = await context.Posts.ToListAsync();3var post = await context.Posts.FindAsync(1);45// Projections to anonymous types or DTOs are NOT tracked6var titles = await context.Posts.Select(p => new { p.Title }).ToListAsync();
Projections (using Select to return non-entity types) are not tracked because they are not full entities. This is an important distinction that we will explore further in the next lessons.
Summary
In this lesson, you learned:
- Change tracking is how EF Core detects modifications to your entities
- EF Core uses snapshot change tracking, comparing current values against stored snapshots
SaveChangesuses tracked changes to generate the correct INSERT, UPDATE, and DELETE SQL- All changes are executed in a single transaction for consistency
- Entities returned from queries are tracked by default; projections are not
Next, we will explore the specific entity states that the Change Tracker uses to categorize each entity.