Chapter 11 · Change Tracking, Entity States, Identity Resolution, and Disconnected Graphs
Original / Current / Database Values, DebugView, and Diagnosing Unexpected Updates
Diagnose writes from tracker evidence using CurrentValues, OriginalValues, GetDatabaseValues, IsModified, DebugView, relationship state, temporary values, and generated SQL.
Learning outcomes
A ServiceHub incident appears contradictory: an API issued an
UPDATE for columns the caller did not intend to edit, while
another request changed an object in memory but produced no
UPDATE at all. Guessing at mappings or adding
Update() makes both problems worse. The right
response is tracker forensics: compare
original, current, and
database values; inspect
IsModified; print DebugView; then
correlate that state with generated SQL.
Distinguish CurrentValues, OriginalValues, and GetDatabaseValues without conflating client snapshots with live store state.
Inspect PropertyEntry IsModified/IsTemporary and the existing Revision concurrency token.
Use DebugView before and after DetectChanges to diagnose missing state transitions.
Compare relationship/FK state and explain why relationship changes can produce writes without obvious scalar assignments.
Diagnose broad unexpected updates and missing updates from concrete evidence before changing mappings.
Use GetDatabaseValues/Reload intentionally when investigating external changes or later concurrency resolution.
Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and no paid tooling. EF Core 11 preview APIs are not part of the mandatory path.
1. Three value sets answer three different questions
| Value source | Question answered | Important limitation |
|---|---|---|
CurrentValues |
What values does EF currently see on this tracked entry? | May include unsaved local changes. |
OriginalValues |
What snapshot/originals is EF using for change/concurrency comparison? | For explicitly attached disconnected entities without real originals, values can initially equal current values. |
GetDatabaseValuesAsync |
What scalar values are stored in the database right now? | Executes a database query; can return null if the row no longer exists. |
These are not interchangeable. A concurrency investigation often needs all three: the user's proposed/current values, the original token/value they read, and the latest store values.
2. Print a property-level diff, including concurrency
var order = await db.WorkOrders.SingleAsync(x => x.Id == id, ct);order.ReviseSummary("Inspect mechanical seal leakage before restart");order.AdvanceRevision();db.ChangeTracker.DetectChanges();var entry = db.Entry(order);foreach (var property in entry.Properties){ Console.WriteLine( $"{property.Metadata.Name,-24} " + $"Original={property.OriginalValue} " + $"Current={property.CurrentValue} " + $"Modified={property.IsModified} " + $"Temporary={property.IsTemporary}");}
You should expect Summary/backing-field state and
Revision to be modified. Keys and store-owned
values should not suddenly become ordinary modified properties.
If the list is broader than the command intent, stop before
saving.
3. GetDatabaseValues reveals store drift without overwriting local state
await using var first = await factory.CreateDbContextAsync(ct);await using var second = await factory.CreateDbContextAsync(ct);var local = await first.WorkOrders.SingleAsync(x => x.Id == id, ct);var other = await second.WorkOrders.SingleAsync(x => x.Id == id, ct);other.ReviseSummary("Dispatcher B changed the service scope");other.AdvanceRevision();await second.SaveChangesAsync(ct);var database = await first.Entry(local).GetDatabaseValuesAsync(ct);if (database is null){ Console.WriteLine("Row was deleted from the database.");}else{ Console.WriteLine($"Local current: {local.Summary}"); Console.WriteLine($"Database summary: {database[nameof(WorkOrder.Summary)]}"); Console.WriteLine($"Database revision: {database[nameof(WorkOrder.Revision)]}");}
GetDatabaseValuesAsync does not by itself replace
the entity's current/original values. It gives you an
independent store snapshot for diagnosis or a later merge
policy. Chapter 13 turns this evidence into explicit
concurrency-resolution strategies.
4. Shadow and store-generated values are still inspectable
var entry = db.Entry(order);var created = entry.Property<DateTime>("CreatedUtc");var revision = entry.Property(x => x.Revision);var idEntry = entry.Property(x => x.Id);Console.WriteLine($"CreatedUtc current: {created.CurrentValue}");Console.WriteLine($"Revision original/current: {revision.OriginalValue} / {revision.CurrentValue}");Console.WriteLine($"Id temporary: {idEntry.IsTemporary}");
Shadow properties are absent from the CLR class but fully present in EF metadata/tracking state. Temporary values and store-generated values have ownership semantics that matter during inserts/updates; inspect them instead of treating every property as application-writable.
5. DebugView is a graph-level X-ray, but call DetectChanges deliberately
order.ReviseSummary("Investigate intermittent pressure drop");Console.WriteLine("BEFORE DETECT:");Console.WriteLine(db.ChangeTracker.DebugView.LongView);db.ChangeTracker.DetectChanges();Console.WriteLine("AFTER DETECT:");Console.WriteLine(db.ChangeTracker.DebugView.LongView);
The long view includes keys, state, modified/original values, foreign keys, and navigations. It is diagnostic text—not a stable machine-readable contract—and it does not replace generated command logs or a database query when you need store evidence.
6. Diagnose an unexpected broad UPDATE before “fixing” it
var detached = WorkOrder.RehydrateForAssignment(payload.Id, payload.Revision);detached.ReviseSummary(payload.Summary);db.Update(detached);db.ChangeTracker.DetectChanges();Console.WriteLine(db.ChangeTracker.DebugView.LongView);foreach (var p in db.Entry(detached).Properties.Where(p => p.IsModified)) Console.WriteLine($"Would write: {p.Metadata.Name}");
If every scalar appears modified, the mapping may be perfectly correct—the state declaration was broad. Repair the update boundary, not the mapping.
var tracked = await db.WorkOrders.SingleAsync(x => x.Id == dto.Id, ct);db.Entry(tracked).CurrentValues.SetValues(dto);// SetValues marks only properties whose values actually differ.Console.WriteLine(db.ChangeTracker.DebugView.LongView);
SetValues is useful only when DTO property
names/semantics are intentionally allow-listed. Domain methods
may still be preferable when invariants or side effects such as
revision advancement matter.
7. Diagnose a missing UPDATE from stale tracker state
order.ReviseSummary("Local change that should be saved");Console.WriteLine($"AutoDetect: {db.ChangeTracker.AutoDetectChangesEnabled}");Console.WriteLine(db.ChangeTracker.DebugView.LongView);db.ChangeTracker.DetectChanges();var entry = db.Entry(order);Console.WriteLine($"State: {entry.State}");Console.WriteLine($"Summary modified: {entry.Property(x => x.Summary).IsModified}");
If state remains Unchanged after an explicit full
detection, investigate mapping/property access/value comparer
semantics. If the state becomes Modified only after
detection, the problem was detection timing or an
AutoDetectChangesEnabled scope—not the database.
8. Relationship snapshots explain writes that look “unrelated”
var order = await db.WorkOrders .Include(w => w.AssignedTechnician) .SingleAsync(w => w.Id == id, ct);var oldTechnicianId = order.AssignedTechnicianId;var technician = await db.Technicians.SingleAsync(x => x.Id == newTechnicianId, ct);order.AssignTo(technician);db.ChangeTracker.DetectChanges();var fk = db.Entry(order).Property(x => x.AssignedTechnicianId);Console.WriteLine($"FK original: {fk.OriginalValue}");Console.WriteLine($"FK current: {fk.CurrentValue}");Console.WriteLine($"FK modified: {fk.IsModified}");
Changing a navigation can change its FK through relationship fixup. The resulting UPDATE may therefore be correct even if no code assigned the FK property directly. Inspect both relationship/navigation state and scalar FK evidence.
9. Reload versus GetDatabaseValues
| Tool | Effect | Use deliberately when |
|---|---|---|
GetDatabaseValuesAsync |
Queries store values into a separate PropertyValues object. | You need comparison/merge evidence without immediately replacing local state. |
ReloadAsync |
Reloads the entity from the database and resets tracking values/state to store state. | You intentionally want to discard/replace local tracked values. |
CurrentValues.SetValues(...) |
Copies values into current tracked values; differing properties become modified. | Applying an allow-listed DTO or chosen merge result. |
OriginalValues.SetValues(...) |
Changes the tracker originals used for comparison/concurrency. | Advanced conflict resolution; only when you understand the new baseline. |
10. Hands-on lab: tracker forensics before SQL
- Reset the SQLite database and load one work order.
-
Capture Current/Original values and
DebugView.LongView. - Mutate Summary/Revision, detect changes, and list modified properties.
-
Use a second context to change the same row, then call
GetDatabaseValuesAsyncfrom the first and compare all three value sets. -
Inspect shadow
CreatedUtc,Revision, ID temporary state, and assignment FK original/current values. -
Reproduce a broad
Update(detached)without saving; print every property it would write. - Reproduce a missing update with disabled detection, then diagnose before repairing.
-
Capture
Microsoft.EntityFrameworkCore.Database.Commandlogs and confirm they match tracker intent.
Check your understanding
- What is the difference between OriginalValues and GetDatabaseValues?
- Can GetDatabaseValues return null?
- Why can DebugView show a changed current value while state is Unchanged?
- What does IsModified control?
- Why might an FK be modified even if no code assigned the FK directly?
- What should you inspect before changing mappings to fix an unexpected UPDATE?
Review the answers
OriginalValues are the tracker baseline; GetDatabaseValues executes a query for current store scalar values.
Yes, when the row no longer exists in the database.
DebugView can read the CLR current value without forcing a full DetectChanges scan.
Whether that mapped property is considered modified and normally included in an UPDATE for a tracked existing entity.
Changing a navigation can trigger relationship fixup that changes the FK.
Entity state, property IsModified/original/current values, relationship state, generated SQL/logs, and live database values where relevant.
11. Production judgment and bridge to Chapter 12
When EF writes the wrong shape—or writes nothing—capture tracker
evidence before reaching for broader state declarations.
Current/original/database values answer different questions;
DebugView exposes client graph state; command logs
expose what was sent. Chapter 12 now follows this state all the
way through the SaveChanges pipeline: command
ordering, generated keys, batching, AcceptAllChanges, and
failure semantics.
Authoritative references
- Accessing Tracked Entities - EF Core — Current/Original/Database values, IsModified, IsTemporary, navigations.
- Change Tracker Debugging - EF Core — ShortView/LongView and change-tracking logs.
- Change Detection and Notifications - EF Core — before/after DetectChanges semantics.
- Identity Resolution - EF Core — store refresh and GetDatabaseValues context.
- Handling Concurrency Conflicts - EF Core — using current/original/database values in conflict resolution.