The Change Tracker — How EF Knows What to Save
To mark a loan returned, ReadingRoom loads the loan, sets ReturnedAt, and calls SaveChangesAsync. No UPDATE statement anywhere, yet the database changes. The piece that makes this work is the change tracker, and understanding it explains most EF surprises.
The rule
Every tracked entity has a state; SaveChanges turns states into SQL.
A DbContext tracks each entity it loads or is handed, in one of five states:
| State | Meaning | SaveChanges sends |
|---|---|---|
Added |
new, not yet in the database | INSERT |
Unchanged |
same as when loaded | nothing |
Modified |
a property changed | UPDATE (changed columns) |
Deleted |
marked for removal | DELETE |
Detached |
not tracked at all | nothing |
var loan = await db.Loans.FindAsync(40); // tracked: Unchanged
loan!.ReturnedAt = DateTime.UtcNow; // becomes Modified at save time
await db.SaveChangesAsync(); // UPDATE Loans SET ReturnedAt = ... WHERE Id = 40
db.Loans.Add(new Loan { BookId = 3, MemberId = 5 }); // Added
db.Members.Remove(oldMember); // Deleted
await db.SaveChangesAsync(); // INSERT and DELETE in one transactionWhen an entity is loaded, EF keeps a snapshot of its original values. During SaveChanges it compares each tracked entity with its snapshot to find what changed. All the statements from one SaveChanges run in a single transaction: all succeed or none do.
The Change Tracker API lets you look inside: db.Entry(loan).State shows one entity's state, db.ChangeTracker.Entries() lists them all, and db.ChangeTracker.DebugView.LongView prints a readable report.
Read-only queries do not need any of this. db.Books.AsNoTracking().ToListAsync() skips the snapshots, which saves memory and time on big lists.
The shortcut
Before each SaveChanges, say the state of every entity you touched. The list of states is the list of SQL statements that will run.
Load member 5
FindAsync(5)— Unchanged. No SQL at save time unless it changes.
The trap: changing an entity loaded with AsNoTracking() and expecting SaveChanges to write it. Nothing is tracking it, so nothing is saved.
Drills
Answer fast — trust the rule.
An entity was just loaded and nothing was changed. What is its state?
What does SaveChangesAsync send for an entity in the Deleted state?
You load books with AsNoTracking(), change a title, and call SaveChanges. What happens?
Why use AsNoTracking() for the /books listing?
Remember
- Every tracked entity has a state; SaveChanges turns states into SQL.
- Name each touched entity's state before saving; that list is the SQL.
AsNoTracking()for read-only queries — and nothing it returns gets saved.
