8 minlesson

Understanding Change Tracking in EF Core

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:

  1. Records the original values of every entity property when the entity is loaded
  2. Monitors entities for modifications during their lifetime
  3. Computes the differences between original and current values when SaveChanges is called
  4. Generates SQL (INSERT, UPDATE, DELETE) based on those differences
csharp
1using var context = new BlogContext();
2
3// EF Core loads the post AND takes a snapshot of its current values
4var post = await context.Posts.FirstAsync(p => p.Id == 1);
5
6// You modify a property on the C# object
7post.Title = "Updated Title";
8
9// SaveChanges detects the change and generates an UPDATE statement
10await 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 database
2
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:

csharp
1context.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:

  1. DetectChanges - Compares current values to snapshots for all tracked entities
  2. Generate SQL - Creates INSERT, UPDATE, or DELETE statements based on entity states
  3. Begin transaction - Wraps all changes in a database transaction
  4. Execute SQL - Sends the generated statements to the database
  5. Update tracking state - Marks entities as Unchanged after a successful save
  6. Commit transaction - Commits the transaction (or rolls back on failure)
csharp
1using var context = new BlogContext();
2
3var blog = new Blog { Title = "My Blog", Url = "https://myblog.com" };
4context.Blogs.Add(blog); // Tracked as Added
5
6var post = await context.Posts.FirstAsync();
7post.Title = "New Title"; // Tracked as Modified
8
9var old = await context.Posts.FindAsync(99);
10context.Posts.Remove(old); // Tracked as Deleted
11
12// 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 = 99
16await 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:

csharp
1// These entities are tracked
2var posts = await context.Posts.ToListAsync();
3var post = await context.Posts.FindAsync(1);
4
5// Projections to anonymous types or DTOs are NOT tracked
6var 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
  • SaveChanges uses 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.