Repository navigation
Don't leave the context in a bad state when tracking an entity fails - #39614
Draft
ajcvickers wants to merge 1 commit into
Draft
ajcvickers wants to merge 1 commit into
ajcvickers wants to merge 1 commit into
Conversation
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>
Contributor
Agent harness evaluation deferredThis PR affects: |
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #29060
Problem
When tracking an entity failed because of a key conflict, the context was left in a bad state:
StateManager.StartTrackingadds 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.ChangingStatehad already moved the entry into theAddedreference map, leaving aDetachedentry inChangeTracker.Entries().UpdateIdentityMap: when the new value conflicted, the entry was dropped from the identity map permanently.Changes
StartTrackingremoves the entry from the identity maps it was added to before rethrowing, including whenSnapshotAndSubscribefails.InternalEntityEntry.OnStateChangingcalls the newStateManager.CancelChangingStateto undo the reference map update.IdentityMap.Removeonly uses the shared-identity link for the primary key. Previously, unregistering from an alternate key map could unlink aDeletedentry that shares the primary key.UpdateIdentityMapre-registers the entry under its relationship snapshot values when the new value conflicts (newAddUsingRelationshipSnapshot). When a property belongs to several keys,NavigationFixer.KeyPropertyChangedundoes the keys it had already updated (newCancelIdentityMapUpdate).IStateManager.CancelChangingStateandCancelIdentityMapUpdate,IIdentityMap.AddUsingRelationshipSnapshot. All are.Internal..agents/skills/change-tracking/SKILL.md.Tests
FailedTrackingTestBasein the specification tests, with Sqlite, SQL Server and InMemory subclasses so external providers can inherit it.StateManagerTestcovering 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 inStartTracking.test.cmdpasses 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 cleanmain. Cosmos tests were not run against a live emulator.Known limitations
EntityGraphAttacher.Deletedentry 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.SnapshotAndSubscribefails part-way, collection change handlers can remain attached to the detached entity.StartTrackingFromQueryhas the same gap. Not addressed here.Addleaves any generated key value on the entity.DbUpdateExceptionfromSaveChangesstill leaves the entitiesAdded; that is existing, intended behavior and unchanged.🤖 Generated with Claude Code