Chapter 11 · Change Tracking, Entity States, Identity Resolution, and Disconnected Graphs

Attach, Update, Entry, TrackGraph, and Selective Changes in Disconnected Applications

Handle disconnected API/message graphs with Attach, Update, Entry, TrackGraph, explicit concurrency originals, property allow-lists, and over-posting defenses.

Advanced135–170 minutesdisconnected graph + security labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

An HTTP request or message consumer receives a work-order payload that was created outside the current DbContext. EF has no original snapshot for that object. This is a disconnected workflow: the server must decide which rows already exist, which values are trustworthy, which properties may change, how concurrency is enforced, and how much of the graph should become tracked.

01

Distinguish Attach, Update, Entry.State, property IsModified, and TrackGraph responsibilities.

02

Use generated-key IsKeySet information without treating it as authorization or existence proof.

03

Expose why Update(graph) can create broad writes and over-posting risk.

04

Apply a command DTO to a queried tracked aggregate as the default safe repair.

05

Use a selective attach pattern only when trusted key/original/concurrency data are available.

06

Use TrackGraph for mixed new/existing graphs while resolving duplicate identities and validating every state decision.

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. Disconnected means the original tracker is gone

In a request/message architecture, the context that originally read an entity has usually been disposed before the client returns changes. The new context receives a CLR object but not the old context's original snapshots, relationship state, shadow values, or authorization decision. Attach and Update can reconstruct tracking state, but they cannot reconstruct business intent automatically.

API Initial declaration Important consequence
Attach(root) Existing graph, generally Unchanged; generated-key unset nodes can be treated as new by graph rules. No broad update until you mark changes.
Update(root) Existing keyed graph is generally Modified; generated-key unset nodes become Added. Can write many columns/rows.
Entry(entity).State State for one entry. Does not traverse the full graph like Add/Attach/Update.
Property(...).IsModified One property should be written. Narrow but requires trusted originals/concurrency.
TrackGraph Your callback decides state while traversing a graph. Powerful; correctness burden is yours.

2. Deliberately wrong: deserialize entities and call Update

csharp · over-posting and broad-write anti-pattern
// Anti-pattern: reconstruct a persistence entity from client-controlled values// and declare the entire detached instance modified.var posted = WorkOrder.RehydrateForAssignment(payload.Id, payload.Revision);posted.ReviseSummary(payload.Summary);db.Update(posted);await db.SaveChangesAsync(ct);

The client may submit values it was never authorized to edit. Missing/default fields can overwrite database values. Related objects in the graph can be marked Added/Modified/Deleted according to graph state. This is both a persistence problem and an API security problem.

sql · representative broad UPDATE risk
UPDATE work_ordersSET customer_name = @p0,    priority = @p1,    summary = @p2,    assigned_technician_id = @p3,    revision = @p4,    ...WHERE work_order_id = @id  AND revision = @originalRevision;

3. Preferred repair: command DTO → query → apply explicit domain intent

csharp · allow-listed update command
public sealed record ReviseWorkOrderCommand(    int WorkOrderId,    string Summary,    Guid Revision);public async Task ReviseAsync(ReviseWorkOrderCommand command, CancellationToken ct){    var order = await db.WorkOrders        .SingleAsync(x => x.Id == command.WorkOrderId, ct);    AuthorizeRevision(order); // application authorization, not EF    db.Entry(order).Property(x => x.Revision).OriginalValue = command.Revision;    order.ReviseSummary(command.Summary);    order.AdvanceRevision();    await db.SaveChangesAsync(ct);}

This path loads one tracked instance, keeps server-owned values out of the payload, reuses domain validation, and preserves Chapter 04's optimistic-concurrency contract. The extra SELECT is often an excellent correctness trade.

4. Selective Attach can avoid a read, but it raises the evidence burden

csharp · trusted stub + one modified FK
var stub = WorkOrder.RehydrateForAssignment(    command.WorkOrderId,    command.Revision);db.Attach(stub); // existing row: Unchangedvar entry = db.Entry(stub);entry.Property(x => x.Revision).OriginalValue = command.Revision;entry.Property(x => x.AssignedTechnicianId).CurrentValue = command.TechnicianId;entry.Property(x => x.AssignedTechnicianId).IsModified = true;stub.AdvanceRevision();await db.SaveChangesAsync(ct);

This pattern assumes the key is trusted, the target row should exist, authorization has already been established without loading it, the FK target is valid, and the submitted concurrency token is the correct original. If those assumptions are awkward to prove, query first.

5. IsKeySet helps classify generated-key nodes, not authorize them

csharp · inspect generated-key state before attaching a graph
var entry = db.Entry(candidate);Console.WriteLine($"Key set: {entry.IsKeySet}");

For generated keys, an unset key is a useful signal that a disconnected node is probably new. A set key does not prove the row exists, belongs to the current tenant/user, or is safe to modify. Those are database/application checks.

6. TrackGraph makes the state policy explicit

csharp · mixed new/existing graph policy sketch
db.ChangeTracker.TrackGraph(root, node =>{    var entry = node.Entry;    if (entry.Entity is WorkOrder)    {        entry.State = EntityState.Unchanged; // root policy handled explicitly later        return;    }    entry.State = entry.IsKeySet        ? EntityState.Unchanged        : EntityState.Added;});// After traversal, apply only the properties/relationships authorized by the command.var rootEntry = db.Entry(root);rootEntry.Property(x => x.AssignedTechnicianId).IsModified = true;

TrackGraph walks a graph and invokes your callback before the graph is fully tracked. That is useful when mixed new/existing nodes need custom decisions. It is not a magic “merge graph” API: your callback must resolve duplicates, validate ownership, handle deletions intentionally, and set concurrency originals where needed.

7. Duplicate identities should be consolidated before tracking

Serialized graphs often repeat the same entity in multiple navigation paths. If two distinct Tag or WorkOrder objects carry the same key and disagree about values, EF cannot safely choose one. Prefer DTO contracts that avoid cyclic entity graphs, or consolidate duplicates by key before calling tracking APIs. The one-instance-per-key invariant from Lesson 3 still applies.

Never “fix” by turning off identity resolution

The conflict is evidence that the incoming graph is ambiguous. Resolve the graph/data contract; do not detach whichever instance happens to throw first.

8. Observe exactly what a disconnected attach would write

csharp · state/property audit before SaveChanges
db.ChangeTracker.DetectChanges();Console.WriteLine(db.ChangeTracker.DebugView.LongView);foreach (var e in db.ChangeTracker.Entries()){    Console.WriteLine($"{e.Metadata.ClrType.Name}: {e.State}");    foreach (var p in e.Properties.Where(p => p.IsModified))        Console.WriteLine($"  WRITE {p.Metadata.Name}: {p.CurrentValue}");}

Make this audit part of tests for risky disconnected flows. The generated SQL command log is the next layer of evidence: verify which columns are actually sent, which original concurrency tokens appear in predicates, and whether unexpected inserts/deletes were generated.

9. Hands-on lab: broad Update versus selective intent

  1. Reset the disposable database and choose one deterministic work order.
  2. Construct a detached teaching graph with an existing root and one new child note.
  3. Call Update(graph) without saving; inspect states/modified properties and record how broad the write would be.
  4. Clear/reset the context; implement query → command application and compare states/SQL.
  5. Implement the trusted selective-attach assignment example and verify only the FK + revision are written.
  6. Build a mixed graph and apply a TrackGraph callback; assert each node state before save.
  7. Inject a duplicate same-key node and verify your pre-tracking validation rejects/consolidates it.

Check your understanding

  1. What information is missing when an entity arrives disconnected?
  2. Why can Update(graph) over-write too much?
  3. What is the safest default update boundary for an API?
  4. What does IsKeySet prove?
  5. When is selective Attach reasonable?
  6. What responsibility does TrackGraph leave to you?
Review the answers

The original context tracker, including snapshots, relationship state, shadow values, and server-side authorization intent.

It broadly declares existing keyed entities/properties modified according to graph rules rather than inferring an allow-listed client intent.

A command/DTO, a server query for the tracked aggregate, authorization/validation, explicit domain changes, then SaveChanges.

Only that EF considers the generated key set; it does not prove database existence or authorization.

When key, original/concurrency values, permitted property set, and related-row validity are trusted/validated without requiring a read.

The state policy for every node, duplicate resolution, ownership/security checks, deletion semantics, and concurrency originals.

10. Production judgment and bridge

Disconnected writes are security and consistency boundaries, not merely EF syntax. Prefer narrow commands and tracked server-side intent. Use selective attach or TrackGraph only when their assumptions are explicit and tested. Lesson 5 provides the forensic tools for when the state you expected and the SQL EF generated still disagree.

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