Understanding EF Core Change Tracking: How It Works Under the Hood
Entity Framework Core (EF Core) makes data handling easy. We all are leveraging its conciseness, flexibility, and rich features in our projects. However, have you ever wondered what goes under the hood? How has EF Core detached us from SQL queries? Today, I will disclose the curtain behind EF Core operations and how it tracks changes for our Create, Update, and Delete operations.

How does EF Core write in the SQL database?
First, let us look at what EF Core does under the hood when you write data in the database.
Step 1: Create models
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// Navigation Property
public virtual ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
}
public class Enrollment
{
public int Id { get; set; } // Primary Key
public string CourseName { get; set; } = string.Empty;
// Foreign Key
public int StudentId { get; set; }
// Navigation Property
public virtual Student Student { get; set; } = null!;
}
Step 2: Write data into the database
// Stage 1
var student = new Student
{
Name = "Ubaid Akram",
Enrollments = new List<Enrollment>()
};
student.Enrollments.Add(new Enrollment { CourseName = "Mathematics" });
// Stage 2
context.Add(student); // EF Core starts tracking
// Stage 3
context.SaveChanges();
Let's break down all the above.
Stage 1: Object Creation
- A new
Student
object is created with the Name "Ubaid Akram." - A
List
is initialized but starts empty for enrollments. - A new
Enrollment
with a course name of "Mathematics" is added to theEnrollments
list. Student
andEnrollment
exists only in memory at this point and EF Core is not yet involved.
Stage 2: Tracking Begins (context.Add(student)
)
- EF Core marks the student as Added.
- It detects the related enrollment inside the
Enrollments
list and mark it as Added. - EF Core assigns temporary IDs for students and enrollments before the database assigns real ones.
Stage 3: Database Operations (context.SaveChanges()
)
EF Core sends two INSERT
queries to the database in the correct order.
- First, the student is inserted into the
Students
table, and a database-generatedStudentId
is assigned. - ️Then, the enrollment is inserted into the
Enrollments
table, and itsStudentId
is updated with the new value. - EF Core updates the
StudentId
in both objects in memory to match the database-generated ID.
My example was simple. However, the navigation can be more complex. EF Core calls SaveChanges
once, in the correct order, to reflect the updates in the database. So, EF Core makes writing complex data in a database easy for the developer.
Let's move to another example, where we are not creating all the navigating entities but referring to one.
// Stage 1: Fetch an existing student from the database
var student = context.Students.First(s => s.Name == "Ubaid Akram");
// Create a new Enrollment and link it to the existing student
var newEnrollment = new Enrollment { CourseName = "Physics", Student = student };
// Stage 2: EF Core starts tracking the new entity
context.Add(newEnrollment);
// Stage 3: Save to the database
context.SaveChanges();
Stage 1: Fetch an existing student
- Instead of creating a new
Student
, we retrieve an existing student ("Ubaid Akram") from the database usingcontext.Students.First()
. - Then, a new
Enrollment
object withCourseName = "Physics"
is created and linked to the existingStudent
.
Stage 2: Add Enrollment
to EF Core's ChangeTracker (context.Add(newEnrollment)
)
- EF Core sets the state of the
newEnrollme
as Added. TheStudent
entity is not marked as Added or Modified because it was retrieved from the database and remains Unchanged. - EF Core automatically detects the relationship and fills in the
StudentId
foreign key based onstudent.Id
.
Stage 3: Save to Database (context.SaveChanges()
)
- EF Core only inserts the new
Enrollment
record in theEnrollments
table. - The
Student
record remains unchanged because we did not modify it. - The
StudentId
in the newEnrollment
row correctly references the existing student. - Again, EF Core will identify the correct order of operations. It will track that the student with the name "Ubaid Akram" already exists and does not need to be inserted. So, it only inserts
newEnrollment
with the student's reference.
How does EF Core update the SQL database?
We have gone through an exercise of writing the data using EF Core. Now, dive into its update operations.
In the same record we are updating the name:
// Stage 1: Fetch an existing student from the database
var student = context.Students.First(s => s.Name == "Ubaid Akram");
// Modify the student's name
student.Name = "Ubaid Akram Khan";
// Stage 2: EF Core starts tracking this change automatically
// Stage 3: Save the changes to the database
context.SaveChanges();
Stage 1: Fetch an Existing Student
- We retrieved a student named "Ubaid Akram" from the database.
- EF Core now tracks which also holds a student object, is now tracked by EF Core. Also, it holds a tracking snapshot of the database data.
Stage 2: Modify the Entity
- We changed the
Name
property to "Ubaid Akram Khan." - EF Core automatically marks this entity as Modified because it's being tracked. EF Core compares tracking snapshots with the updated data and generates an update command accordingly. The ChangeTracker of EF Core detects every change in the tracked entities, both for non-relational properties like
Title
,PubishedOn
, and navigational links, which will be converted to changes to foreign keys that link tables together. The ChangeTracker marks only theName
property as changed and only updates the modified field ofName
.
Stage 3: Save Changes (context.SaveChanges()
)
- EF Core generates an
UPDATE
SQL statement for only the modified properties. - The database updates the record where
Id = student.Id
. Underlying SQL queryUPDATE Students SET Name = 'Ubaid Akram Khan' WHERE Id = 1;
.
How does EF Core delete in the SQL database?
Continuing with the same example, delete the student with the Name "Ubaid Akram Khan".
// Stage 1: Fetch an existing student from the database
var student = context.Students.First(s => s.Name == "Ubaid Akram Khan");
// Stage 2: Mark the student for deletion
context.Remove(student);
// Stage 3: Save changes to apply the deletion in the database
context.SaveChanges();
Stage 1: Fetch an Existing Student
- We retrieve an existing student from the database.
- EF Core now tracks the student object.
Stage 2: Mark the entity for deletion (context.Remove(student)
)
- EF Core marks the entity as Deleted.
The ChangeTracker sets the entity's state as Deleted using the entity's primary key.
Stage 3: Save Changes (context.SaveChanges()
)
- EF Core generates a
DELETE
SQL statement to remove the record. - The student record is permanently deleted from the database.
Conclusion
EF Core is a primal choice for database operations for most projects. It provides a flexible, handy, and feature-rich alternative to SQL queries along with LINQ. In the article, we looked below the surface to how EF Core performs Create, Update, and Delete operations from start to end. The article discussed the role of the Change tracker, how it compares the updated data with the tracking snapshot, and how it creates SQL queries.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website