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.

Advanced130–165 minutestracker forensics + unexpected-update labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

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.

01

Distinguish CurrentValues, OriginalValues, and GetDatabaseValues without conflating client snapshots with live store state.

02

Inspect PropertyEntry IsModified/IsTemporary and the existing Revision concurrency token.

03

Use DebugView before and after DetectChanges to diagnose missing state transitions.

04

Compare relationship/FK state and explain why relationship changes can produce writes without obvious scalar assignments.

05

Diagnose broad unexpected updates and missing updates from concrete evidence before changing mappings.

06

Use GetDatabaseValues/Reload intentionally when investigating external changes or later concurrency resolution.

Reproducible baseline

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

csharp · tracker diff for one WorkOrder
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

csharp · compare a first context with a second writer
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

csharp · inspect Chapter 04 infrastructure metadata
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

csharp · before/after tracker view
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

csharp · anti-pattern that manufactures broad state
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.

csharp · repair with SetValues on a tracked entity
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

csharp · missing-update checklist
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”

csharp · assignment relationship evidence
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

  1. Reset the SQLite database and load one work order.
  2. Capture Current/Original values and DebugView.LongView.
  3. Mutate Summary/Revision, detect changes, and list modified properties.
  4. Use a second context to change the same row, then call GetDatabaseValuesAsync from the first and compare all three value sets.
  5. Inspect shadow CreatedUtc, Revision, ID temporary state, and assignment FK original/current values.
  6. Reproduce a broad Update(detached) without saving; print every property it would write.
  7. Reproduce a missing update with disabled detection, then diagnose before repairing.
  8. Capture Microsoft.EntityFrameworkCore.Database.Command logs and confirm they match tracker intent.

Check your understanding

  1. What is the difference between OriginalValues and GetDatabaseValues?
  2. Can GetDatabaseValues return null?
  3. Why can DebugView show a changed current value while state is Unchanged?
  4. What does IsModified control?
  5. Why might an FK be modified even if no code assigned the FK directly?
  6. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n