Fix RowVersion concurrency issue when replacing entities with TPH inheritance and owned types#37788
Merged
AndriySvyryd merged 8 commits intomainfrom Feb 25, 2026
Merged
Conversation
…n bug Add test reproducing a bug where deleting EntityA (with owned type) and adding EntityB (same PK) in TPH generates an UPDATE command that may interfere with RowVersion concurrency token handling when table sharing is involved due to owned entity types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t for issue #37169 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Fix RowVersion concurrency issue with inheritance and owned types
Fix RowVersion concurrency issue when replacing entities with TPH inheritance and owned types
Feb 24, 2026
…eritance and owned types When deleting an entity with an owned type and inserting a sibling entity with the same PK in TPH, the owned entry's principal can't be found via FindPrincipal because the identity map contains the replacing entry (incompatible sibling type). Fix GetMainEntry to check SharedIdentityEntry on the replacing entry found in the identity map, and use the replacing entry as the main entry so the owned entry is merged into the same modification command. Fixes #37169 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes an EF Core relational update batching edge case where TPH entity replacement (delete + add same PK) combined with owned types and a RowVersion concurrency token can result in multiple modification commands targeting the same row, leading to a DbUpdateConcurrencyException.
Changes:
- Add a fallback in
SharedTableEntryMap.GetMainEntry()to resolve the “main” entry via the identity map +SharedIdentityEntrywhenFindPrincipal()returnsnulldue to incompatible sibling types during TPH replacement. - Add a relational update pipeline regression test covering TPH replacement with an owned type and concurrency token.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/EFCore.Relational/Update/Internal/SharedTableEntryMap.cs | Adds identity-map fallback logic to keep owned entries grouped under the replacement entry for command generation. |
| test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs | Adds regression coverage ensuring batching produces a single valid modification command and includes RowVersion as a condition. |
test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Outdated
Show resolved
Hide resolved
test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
AndriySvyryd
requested changes
Feb 25, 2026
test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs
Outdated
Show resolved
Hide resolved
…ffixes to 37588 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
AndriySvyryd
approved these changes
Feb 25, 2026
Member
|
@copilot Update the PR description with a readable summary |
Contributor
Author
Updated the PR description with a readable summary covering the issue, root cause, and fix. |
roji
approved these changes
Feb 25, 2026
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 #37588
When performing a TPH entity replacement (deleting an entity and inserting a new one of a different derived type with the same primary key), the presence of an owned type combined with a RowVersion concurrency token caused a
DbUpdateConcurrencyException.The root cause was in
SharedTableEntryMap.GetMainEntry(): for a deleted owned entry,FindPrincipal()returnednullbecause the identity map contained the replacing entity (an incompatible sibling type). This caused the owned entry to generate its own separateDELETEcommand targeting the same row, which conflicted with the mainUPDATEcommand.The fix adds a fallback in
GetMainEntry()that, whenFindPrincipal()returnsnull, looks up the entry in the identity map by key values. If that entry has aSharedIdentityEntrywhose entity type is compatible with the foreign key's principal type, it uses the identity map entry as the main entry, correctly grouping the owned entry under the replacement command.Original prompt
This section details on the original issue you should resolve
<issue_title>RowVersion concurrency issue when replacing entities with inheritance and owned types</issue_title>
<issue_description>### Bug description
The issue can best be demonstrated using a simple sample application. Explaining it purely in text is relatively difficult.
ConsoleApp.zip
The structure of the sample application is as follows:
When the following steps are performed:
the following exception is thrown:
'Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyExceptionAsync(RelationalDataReader reader, Int32 commandIndex, Int32 expectedRowsAffected, Int32 rowsAffected, CancellationToken cancellationToken)'
The issue only occurs with this specific combination of:
The problem can be worked around by either:
Your code
Stack traces