Skip to content

Don't leave the context in a bad state when tracking an entity fails - #39614

Draft
ajcvickers wants to merge 1 commit into
dotnet:mainfrom
ajcvickers:fix/29060-failed-add-identity-map
Draft

ajcvickers wants to merge 1 commit into
dotnet:mainfrom
ajcvickers:fix/29060-failed-add-identity-map

Conversation

@ajcvickers

Copy link
Copy Markdown
Contributor

Fixes #29060

Problem

When tracking an entity failed because of a key conflict, the context was left in a bad state:

  • StateManager.StartTracking adds the entry to the identity map of each key in turn. If a later key conflicted, the entry stayed registered in the maps of the earlier keys, so those key values were reserved by an entity that was never tracked. Keys are ordered by property count then name, so the primary key is not always processed first.
  • StateManager.ChangingState had already moved the entry into the Added reference map, leaving a Detached entry in ChangeTracker.Entries().
  • Changing a key on an already-tracked entity had the same problem in UpdateIdentityMap: when the new value conflicted, the entry was dropped from the identity map permanently.

Changes

  • StartTracking removes the entry from the identity maps it was added to before rethrowing, including when SnapshotAndSubscribe fails.
  • InternalEntityEntry.OnStateChanging calls the new StateManager.CancelChangingState to undo the reference map update.
  • IdentityMap.Remove only uses the shared-identity link for the primary key. Previously, unregistering from an alternate key map could unlink a Deleted entry that shares the primary key.
  • UpdateIdentityMap re-registers the entry under its relationship snapshot values when the new value conflicts (new AddUsingRelationshipSnapshot). When a property belongs to several keys, NavigationFixer.KeyPropertyChanged undoes the keys it had already updated (new CancelIdentityMapUpdate).
  • Internal API additions: IStateManager.CancelChangingState and CancelIdentityMapUpdate, IIdentityMap.AddUsingRelationshipSnapshot. All are .Internal.
  • Noted the new pitfall in .agents/skills/change-tracking/SKILL.md.

Tests

  • FailedTrackingTestBase in the specification tests, with Sqlite, SQL Server and InMemory subclasses so external providers can inherit it.
  • New unit tests in StateManagerTest covering alternate-key, primary-key and composite-key conflicts in each entity state, retry after resolving the conflict, a deleted entry sharing the primary key, event behaviour, failed key changes (alternate key, primary key, to null, shared identity), a property in two keys, a store-generated key, and a failed graph add. Both orderings are covered (alternate key sorting before and after the primary key), since only the latter exercises the rollback in StartTracking.

test.cmd passes apart from 8 many-to-many query tests (Sqlite and SQL Server) that fail in the full parallel run and pass when their classes are run alone; the custom-key one also fails on clean main. Cosmos tests were not run against a live emulator.

Known limitations

  • Attaching a graph is not rolled back. Entities attached before the failure stay tracked (consistently registered), and the failing entity and the rest of the graph are left untracked. This is documented with a comment in EntityGraphAttacher.
  • A Deleted entry that a replacement entry displaced from a non-primary-key map is not restored if the replacement is rolled back. This matches what already happens when the replacement is detached normally.
  • If SnapshotAndSubscribe fails part-way, collection change handlers can remain attached to the detached entity. StartTrackingFromQuery has the same gap. Not addressed here.
  • A failed Add leaves any generated key value on the entity.
  • A DbUpdateException from SaveChanges still leaves the entities Added; that is existing, intended behavior and unchanged.

🤖 Generated with Claude Code

Fixes dotnet#29060

Starting to track an entity registered it in the identity map of each key in
turn. When a later key conflicted, the entity stayed registered in the maps of
the earlier keys, and StateManager.ChangingState had already moved it into the
Added reference map. The context was left with a Detached entry in
ChangeTracker.Entries() and with keys that were reserved by an entity that was
never tracked.

- StateManager.StartTracking now removes the entry from the identity maps it was
  added to before rethrowing, including when SnapshotAndSubscribe fails.
- InternalEntityEntry.OnStateChanging calls the new
  StateManager.CancelChangingState to undo the reference map update.
- IdentityMap.Remove only uses the shared identity link for the primary key, so
  unregistering from an alternate key map no longer unlinks a deleted entry that
  shares the primary key.
- UpdateIdentityMap re-registers the entry under its relationship snapshot values
  when the new key value conflicts, instead of dropping it from the map. When a
  property belongs to several keys, NavigationFixer.KeyPropertyChanged undoes
  the keys it had already updated.

Attaching a graph is still not rolled back: entities attached before the failure
stay tracked, and the failing entity and the rest of the graph are left
untracked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Agent harness evaluation deferred

This PR affects: change-tracking. Automatic evaluation is deferred because this PR is currently marked as draft. Once it is ready for review, the usual author authorization rules apply. A contributor with write access can comment /eval or /eval component-name, or run the workflow manually for pull request #39614.

@ajcvickers

Copy link
Copy Markdown
Contributor Author

@AndriySvyryd This one is very much a judgement call as to what to protect against without getting into the weeds around graph updates. So look at the known limitations carefully. We can try to go further, but this is an area that typically results in regressions when people are attempting to set their own states explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DbUpdateException should not leave the context in a bad state

1 participant