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.

Understanding EF Core Change Tracking: How It Works Under the Hood

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 the Enrollments list.
  • Student and Enrollment 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-generated StudentId is assigned.
  • ️Then, the enrollment is inserted into the Enrollments table, and its StudentId 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 using context.Students.First().
  • Then, a new Enrollment object with CourseName = "Physics" is created and linked to the existing Student.

Stage 2: Add Enrollment to EF Core's ChangeTracker (context.Add(newEnrollment))

  • EF Core sets the state of the newEnrollme as Added. The Student 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 on student.Id.

Stage 3: Save to Database (context.SaveChanges())

  • EF Core only inserts the new Enrollment record in the Enrollments table.
  • The Student record remains unchanged because we did not modify it.
  • The StudentId in the new Enrollment 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 the Name property as changed and only updates the modified field of Name.

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 query UPDATE 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