Skip to content

FIX Insert order bug when using TPC - Issue #35978 #38070

Open
andrewraper-Sage wants to merge 5 commits intodotnet:mainfrom
andrewraper-Sage:tpc-insert-fix
Open

FIX Insert order bug when using TPC - Issue #35978 #38070
andrewraper-Sage wants to merge 5 commits intodotnet:mainfrom
andrewraper-Sage:tpc-insert-fix

Conversation

@andrewraper-Sage
Copy link
Copy Markdown

When using TPC inheritance with abstract base types, SaveChanges produces an incorrect INSERT order - child entities are inserted before their parents, causing FK constraint violations.

This happens because CommandBatchPreparer.AddForeignKeyEdges() fails to create dependency edges for FKs whose principal is an abstract TPC type mapped to no table. Since no IForeignKeyConstraint is created for such FKs, the table-level constraint path finds nothing.

The model-level FK fallback path should handle this, but for INSERT ordering it was skipped by a guard that assumed table-level constraints would cover it, and for DELETE ordering it was gated behind command.Table == null (unreachable for concrete TPC entities which always have a table).

The fix ensures the model-level FK path participates in dependency edge creation when no mapped table-level constraint exists: an additional foreignKey.GetMappedConstraints().Any() check on the INSERT principal registration guard, and removing the else gates on both DELETE paths so they always run alongside the table-level path (CanCreateDependency already prevents double-counting for FKs that do have mapped constraints).

Fixes #35978

@andrewraper-Sage
Copy link
Copy Markdown
Author

@andrewraper-Sage please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"

Contributor License Agreement

@dotnet-policy-service agree company="Sage"

@andrewraper-Sage andrewraper-Sage marked this pull request as ready for review April 9, 2026 12:25
@andrewraper-Sage andrewraper-Sage requested a review from a team as a code owner April 9, 2026 12:25
Copilot AI review requested due to automatic review settings April 9, 2026 12:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect SaveChanges command ordering for TPC inheritance when a foreign key’s principal is an abstract TPC type mapped to no table (preventing FK constraint violations by ensuring principals are ordered before dependents on INSERT, and dependents before principals on DELETE).

Changes:

  • Update CommandBatchPreparer.AddForeignKeyEdges() to include the model-level FK dependency path when no mapped table-level FK constraints exist.
  • Remove else gating so model-level FK dependency detection runs alongside the table-level constraint path (with CanCreateDependency preventing duplicates).
  • Add regression tests covering INSERT and DELETE ordering for the abstract-TPC-principal scenario.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs Ensures dependency edges are created via model-level FK logic when table-level constraints are missing, fixing ordering for abstract TPC principals.
test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs Adds regression tests validating correct batching/sort order for Added and Deleted entities under abstract-principal TPC FKs.

Copilot AI review requested due to automatic review settings April 13, 2026 08:52
@andrewraper-Sage andrewraper-Sage marked this pull request as draft April 13, 2026 08:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

@andrewraper-Sage andrewraper-Sage marked this pull request as ready for review April 13, 2026 11:12
Copilot AI review requested due to automatic review settings April 13, 2026 11:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +731 to +736

foreach (var entry in command.Entries)
{
foreach (var entry in command.Entries)
foreach (var foreignKey in entry.EntityType.GetForeignKeys())
{
foreach (var foreignKey in entry.EntityType.GetForeignKeys())
if (!CanCreateDependency(foreignKey, command, principal: false)
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the else means the model-level FK scanning loops now run even when command.Table != null. Although CanCreateDependency() filters out most FKs with mapped constraints, this still adds per-command work on common SaveChanges paths. If possible, consider an early skip/guard so the model-level path only runs when there are unmapped constraints for the given command (to avoid unnecessary iteration on large graphs).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes a valid point, however, the added complexity likely doesn't trade off with the optimization that would be gained.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not only about perf, removing the else check also muddles this, already complicated logic. Currently there are two modes in the method:

  1. command.Table != null is the common case
  2. command.Table == null is used when there is not relational model, which should only be the case sometimes when applying seed data changes in migrations.

So, to keep the logic clear only change the first case. There, you can add another loop that goes over the FKs that are not mapped and would be between two different tables if mapped. Something like:

foreach (var foreignKey in principal
                ? entry.EntityType.GetReferencingForeignKeys()
                : (IEnumerable<IForeignKey>)entry.EntityType.GetForeignKeys())
{
    if (!foreignKey.GetMappedConstraints().Any()
        || (principal ? foreignKey.DeclaringEntityType : foreignKey.PrincipalEntityType).GetTableMappings().Any(m => m.Table == command.Table))
    {
        return true;
    }

    ...
}

The logic inside would indeed look a lot like the logic for the case without a relational model, but instead of trying to force the execution path to use it you can extract it to a local method that's used from both places.

Same applies for the changes below

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.

EF Core Issue: Incorrect Save Order with TPC Inheritance

3 participants