Lesson 18 of 45 intermediate

Change Tracking and Loading Data

What EF Core remembers, and when it goes back to the database

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

You load a customer with AsNoTracking, change its Name and call SaveChanges. What happens?

Open interactive version (quiz + challenge)

Real-world analogy

Picture a librarian who photocopies each book page when you borrow it. When you hand the book back, they compare the copy with your marked-up page and fix only the lines that changed. That makes borrowing slower and needs a photocopier, so a visitor who only reads can use a "no copy" desk instead. A book that mentions chapters in another room costs a trip to fetch them, unless you ask for them up front. The photocopy is the tracker's original values, and the no copy desk is AsNoTracking.

What is it?

Change tracking is how EF Core knows what to write back. When you load an entity through a normal query, the context keeps a copy of its original values and watches the entity. On SaveChanges it works out which entities were added, changed or deleted and sends the matching INSERT, UPDATE and DELETE statements. Tracking costs memory and time, so read-only queries should switch it off with AsNoTracking. How you load related data decides how many queries run.

Real-world relevance

The most common production slowdown is a list page that loads thousands of tracked entities it only displays, or a loop that triggers one query per row. The most common confusing bug is the opposite: a developer adds AsNoTracking to speed up a query, then edits the result and finds SaveChanges did nothing. Both come from not knowing whether the context is watching the object.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Which call fetches an order and its items in one query?

A loop reads order.Customer.Name for 500 orders and runs 501 queries. What is this called?

Code example

public async Task RenameAsync(ShopContext db, int id, string name, CancellationToken ct)
{
    // Tracked: the context stores the original values
    var customer = await db.Customers.FirstAsync(c => c.Id == id, ct);

    customer.Name = name;
    Console.WriteLine(db.Entry(customer).State);   // Modified

    // UPDATE Customers SET Name = @p0 WHERE Id = @p1
    await db.SaveChangesAsync(ct);
}

public record CustomerRow(int Id, string Name, int OrderCount);

public Task<List<CustomerRow>> ListAsync(ShopContext db, CancellationToken ct)
{
    // Read-only: no tracking, only three columns come back
    return db.Customers
        .AsNoTracking()
        .Select(c => new CustomerRow(c.Id, c.Name, c.Orders.Count))
        .ToListAsync(ct);
}

Line-by-line walkthrough

  1. 1. FirstAsync loads one customer. Because the query is tracked, the context stores its original values.
  2. 2. Changing Name notifies nobody. Entry() runs change detection for this entity, finds the new value and reports Modified.
  3. 3. SaveChangesAsync compares original and current values and sends an UPDATE for the Name column only.
  4. 4. CustomerRow is a small record that holds just what the list page needs.
  5. 5. AsNoTracking switches the tracker off for this read-only query.
  6. 6. The Select turns the query into one SQL statement that returns three columns, including a count of orders.

Spot the bug

public async Task RenameAsync(ShopContext db, int id, string name)
{
    var customer = await db.Customers
        .AsNoTracking()
        .FirstAsync(c => c.Id == id);

    customer.Name = name;
    await db.SaveChangesAsync();   // runs, but the database row is unchanged
}
Need a hint?
Does the context know this object exists?
Show answer
AsNoTracking tells the context not to watch the entity, so SaveChanges sees no changes and sends no SQL. Fix: remove AsNoTracking for queries you will modify. As an alternative, call db.Customers.Update(customer) to attach it, but that rewrites every column.

Explain like I'm 5

EF Core keeps a copy of what it gave you. When you say "save", it compares your version with its copy and writes down only what changed. If you only wanted to look, you can tell it not to keep a copy.

Fun fact

A projection into plain values, like the CustomerRow above, is never tracked, even without AsNoTracking. Only entity objects returned by the query are tracked.

Hands-on challenge

Say it out loud in 30 seconds: "What does the EF Core change tracker do, and when would you use AsNoTracking?" Then check: did you mention original values compared on SaveChanges, the entity states, and read-only queries being faster without tracking?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook