Skip to content

EF Core Issue: Incorrect Save Order with TPC Inheritance #35978

@luigi68

Description

@luigi68

Bug description

IssueOnSave.zip

EF Core Issue: Incorrect Save Order with TPC Inheritance

Issue Description

When using Entity Framework Core with Table-Per-Concrete-type (TPC) inheritance strategy, the framework does not respect parent-child relationships during save operations. Specifically, child entities are being saved before their parent entities, causing foreign key constraint violations.

Environment

  • EF Core version: 9.0.4
  • Database provider: SQL Server
  • Inheritance strategy: TPC (Table-Per-Concrete-type)

Observed Behavior

When saving a parent-child relationship where both entities are tracked by the context, EF Core attempts to save the child entities before their parents, leading to foreign key constraint violations.

SQL Server Profiler confirms that INSERT statements for child entities are being executed before INSERT statements for parent entities they depend on.

Expected Behavior

EF Core should analyze the entity graph and dependency relationships, then generate a correct save order that respects parent-child relationships. Parent entities should be saved before their children to satisfy foreign key constraints.

Code Sample

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EFModel
{
    public abstract class BaseEntity
    {
        public decimal DEPTID { get; set; }

        public DateTime LASTUPDATED { get; set; } = DateTime.Now.Date;
    }

    public abstract class AMOS_ADDRESS : BaseEntity
    {
        [Key]
        public decimal ADDRESSID { get; set; }

        public string? CODE { get; set; }

        public string? ALPHACODE { get; set; }

        public decimal? ADDRESSCATEGORYID { get; set; }

        [ForeignKey("ADDRESSCATEGORYID")]
        public virtual AMOS_ADDRESSCATEGORY? ADDRESSCATEGORY { get; set; }

        public virtual List<AMOS_ADDRESSCONTACT> CONTACTS { get; set; }
    }

    public abstract class AMOS_ADDRESSCONTACT : BaseEntity
    {
        [Key]
        public decimal ADDRESSCONTACTID { get; set; }

        [ForeignKey("ADDRESSID")]
        public virtual AMOS_ADDRESS ADDRESS { get; set; }

        public decimal ADDRESSID { get; set; }

        public string FIRSTNAME { get; set; }

        public string LASTNAME { get; set; }

        public string? EMAIL { get; set; }

        public string? PHONE { get; set; }
    }

    public abstract class AMOS_ADDRESSCATEGORY : BaseEntity
    {
        [Key]
        public decimal ADDRESSCATEGORYID { get; set; }

        public string? DESCRIPTION { get; set; }

        public virtual ICollection<AMOS_ADDRESS> ADDRESSES { get; set; }
    }

    public abstract class AMOS_ACCOUNTCODE : BaseEntity
    {
        [Key]
        public decimal ACCOUNTCODEID
        {
            get;
            set;
        }

        public string CODE
        {
            get;
            set;
        }

        public decimal? ACCOUNTCODECATEGORYID
        {
            get;
            set;
        }

        [ForeignKey("ACCOUNTCODECATEGORYID")]
        public virtual AMOS_ACCOUNTCODECATEGORY ACCOUNTCODECATEGORY
        {
            get;
            set;
        }
    }

    public abstract class AMOS_ACCOUNTCODECATEGORY : BaseEntity
    {
        [Key]
        public decimal ACCOUNTCODECATEGORYID
        {
            get;
            set;
        }

        public decimal SEQUENCENO
        {
            get;
            set;
        }

        public decimal CATEGORYTYPE
        {
            get;
            set;
        }
    }

    public class ADDRESS : AMOS_ADDRESS
    { }

    public class ADDRESSCONTACT : AMOS_ADDRESSCONTACT
    { }

    public class ADDRESSCATEGORY : AMOS_ADDRESSCATEGORY
    { }

    public class ACCOUNTCODE : AMOS_ACCOUNTCODE
    { }

    public class ACCOUNTCODECATEGORY : AMOS_ACCOUNTCODECATEGORY
    { }
}```

## Reproduction Steps

1. Create a new category and address in the same SaveChanges call:

```csharp
using EFModel;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;

namespace IssueOnSave
{
    public static class Tests
    {
        public static void Run()
        {
            var modelBuilder = new ModelBuilder(SqlServerConventionSetBuilder.Build());

            modelBuilder.HasDefaultSchema("dbo");

            modelBuilder.Entity<AMOS_ADDRESSCATEGORY>().UseTpcMappingStrategy();
            modelBuilder.Entity<ADDRESSCATEGORY>().ToTable("ADDRESSCATEGORY", "dbo");

            modelBuilder.Entity<AMOS_ACCOUNTCODECATEGORY>().UseTpcMappingStrategy();
            modelBuilder.Entity<ACCOUNTCODECATEGORY>().ToTable("ACCOUNTCODECATEGORY", "dbo");

            modelBuilder.Entity<AMOS_ACCOUNTCODE>().UseTpcMappingStrategy();
            modelBuilder.Entity<ACCOUNTCODE>().ToTable("ACCOUNTCODE", "dbo");

            modelBuilder.Entity<AMOS_ADDRESS>().UseTpcMappingStrategy();
            modelBuilder.Entity<ADDRESS>().ToTable("ADDRESS", "dbo");

            modelBuilder.Entity<AMOS_ADDRESSCONTACT>().UseTpcMappingStrategy();
            modelBuilder.Entity<ADDRESSCONTACT>().ToTable("ADDRESSCONTACT", "dbo");

            var optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer("Server=PCHome2;Database=testdb;User Id=sa;Password=sa;TrustServerCertificate=True;");
            optionsBuilder.UseLazyLoadingProxies(true);
            var model = modelBuilder.FinalizeModel();
            optionsBuilder.UseModel(model);

            using (var dbContext = new DbContext(optionsBuilder.Options))
            {
                using (var tr = dbContext.Database.BeginTransaction())
                {
                    var fks = dbContext.GetForeignKeys<ADDRESS>();
                    // The fks contains correctly the foreign keys defined for ADDRESS
                    dbContext.Set<ADDRESS>().Add(new ADDRESS() { ADDRESSCATEGORYID = null, ADDRESSID = 9999997, CODE = "CODE3", DEPTID = 1 });
                    dbContext.Set<ADDRESS>().Add(new ADDRESS() { ADDRESSCATEGORYID = 9999999, ADDRESSID = 9999998, CODE = "CODE", DEPTID = 1 });
                    dbContext.Set<ADDRESS>().Add(new ADDRESS() { ADDRESSCATEGORYID = 9999999, ADDRESSID = 9999999, CODE = "CODE2", DEPTID = 1 });

                    dbContext.Set<ACCOUNTCODECATEGORY>().Add(new ACCOUNTCODECATEGORY() { ACCOUNTCODECATEGORYID = 9999999, CATEGORYTYPE = 1, DEPTID = 1, SEQUENCENO = 1 });
                    dbContext.Set<ACCOUNTCODE>().Add(new ACCOUNTCODE() { ACCOUNTCODECATEGORYID = 9999999, ACCOUNTCODEID = 999999, CODE = "CODEABC", DEPTID = 1 });

                    dbContext.Set<ADDRESSCATEGORY>().Add(new ADDRESSCATEGORY() { ADDRESSCATEGORYID = 9999998, DESCRIPTION = "1ABCD", DEPTID = 1 });
                    dbContext.Set<ADDRESSCATEGORY>().Add(new ADDRESSCATEGORY() { ADDRESSCATEGORYID = 9999999, DESCRIPTION = "2ABCD", DEPTID = 1 });

                    dbContext.SaveChanges();
                    tr.Rollback();
                }
            }
        }

        public static IEnumerable<IForeignKey> GetForeignKeys<T>(this DbContext context)
        {
            var table = context.Model.FindEntityType(typeof(T));
            return table.GetForeignKeys();
        }
    }
}```

## Additional Information

This issue could be related to usage of TPC inheritance.

## SQL Script for Test Database

```sql
CREATE TABLE [dbo].[AccountCodeCategory](
	[AccountCodeCategoryID] [numeric](12, 0) NOT NULL,
	[SequenceNo] [numeric](2, 0) NOT NULL,
	[CategoryType] [numeric](1, 0) NOT NULL,
	[DeptID] [numeric](12, 0) NOT NULL,
	[ExportMarker] [numeric](1, 0) NOT NULL,
	[LastUpdated] [datetime] NOT NULL
 CONSTRAINT [PK_ACCOUNTCODECATEGORY] PRIMARY KEY CLUSTERED 
(
	[AccountCodeCategoryID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[AccountCodeCategory] ADD  DEFAULT ((1)) FOR [CategoryType]
GO

ALTER TABLE [dbo].[AccountCodeCategory] ADD  DEFAULT ((1)) FOR [ExportMarker]
GO

ALTER TABLE [dbo].[AccountCodeCategory] ADD  DEFAULT (getdate()) FOR [LastUpdated]
GO

CREATE TABLE [dbo].[AccountCode](
	[AccountCodeID] [numeric](12, 0) NOT NULL,
	[AccountCodeCategoryID] [numeric](12, 0) NOT NULL,
	[Code] [nvarchar](20) NOT NULL,
	[DeptID] [numeric](12, 0) NOT NULL,
	[ExportMarker] [numeric](1, 0) NOT NULL,
	[LastUpdated] [datetime] NOT NULL
 CONSTRAINT [PK_ACCOUNTCODE] PRIMARY KEY CLUSTERED 
(
	[AccountCodeID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[AccountCode] ADD  DEFAULT ((1)) FOR [ExportMarker]
GO

ALTER TABLE [dbo].[AccountCode] ADD  DEFAULT (getdate()) FOR [LastUpdated]
GO

ALTER TABLE [dbo].[AccountCode]  WITH CHECK ADD  CONSTRAINT [AccountCode_Category] FOREIGN KEY([AccountCodeCategoryID])
REFERENCES [dbo].[AccountCodeCategory] ([AccountCodeCategoryID])
GO

ALTER TABLE [dbo].[AccountCode] CHECK CONSTRAINT [AccountCode_Category]
GO

CREATE TABLE [dbo].[AddressCategory](
	[AddressCategoryID] [numeric](12, 0) NOT NULL,
	[Description] [nvarchar](30) NOT NULL,
	[DeptID] [numeric](12, 0) NOT NULL,
	[ExportMarker] [numeric](1, 0) NOT NULL,
	[LastUpdated] [datetime] NOT NULL
 CONSTRAINT [PK_ADDRESSCATEGORY] PRIMARY KEY CLUSTERED 
(
	[AddressCategoryID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[AddressCategory] ADD  DEFAULT ((1)) FOR [ExportMarker]
GO

ALTER TABLE [dbo].[AddressCategory] ADD  DEFAULT (getdate()) FOR [LastUpdated]
GO

CREATE TABLE [dbo].[Address](
	[AddressID] [numeric](12, 0) NOT NULL,
	[Code] [nvarchar](15) NOT NULL,
	[AlphaCode] [nvarchar](100) NULL,
	[AddressCategoryID] [numeric](12, 0) NULL,
	[DeptID] [numeric](12, 0) NOT NULL,
	[ExportMarker] [numeric](1, 0) NOT NULL,
	[LastUpdated] [datetime] NOT NULL
 CONSTRAINT [PK_ADDRESS] PRIMARY KEY CLUSTERED 
(
	[AddressID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Address] ADD  DEFAULT ((1)) FOR [ExportMarker]
GO

ALTER TABLE [dbo].[Address] ADD  DEFAULT (getdate()) FOR [LastUpdated]
GO

ALTER TABLE [dbo].[Address]  WITH CHECK ADD  CONSTRAINT [Address_AddressCategory] FOREIGN KEY([AddressCategoryID])
REFERENCES [dbo].[AddressCategory] ([AddressCategoryID])
GO

ALTER TABLE [dbo].[Address] CHECK CONSTRAINT [Address_AddressCategory]
GO

CREATE TABLE [dbo].[AddressContact](
	[AddressContactID] [numeric](12, 0) NOT NULL,
	[AddressID] [numeric](12, 0) NOT NULL,
	[LastName] [nvarchar](100) NOT NULL,
	[FirstName] [nvarchar](40) NOT NULL,
	[Email] [nvarchar](100) NULL,
	[Phone] [nvarchar](100) NULL,
	[DeptID] [numeric](12, 0) NOT NULL,
	[ExportMarker] [numeric](1, 0) NOT NULL,
	[LastUpdated] [datetime] NOT NULL
 CONSTRAINT [PK_ADDRESSCONTACT] PRIMARY KEY CLUSTERED 
(
	[AddressContactID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[AddressContact] ADD  DEFAULT ((1)) FOR [ExportMarker]
GO

ALTER TABLE [dbo].[AddressContact] ADD  DEFAULT (getdate()) FOR [LastUpdated]
GO

ALTER TABLE [dbo].[AddressContact]  WITH CHECK ADD  CONSTRAINT [AddressContact_Address] FOREIGN KEY([AddressID])
REFERENCES [dbo].[Address] ([AddressID])
GO

ALTER TABLE [dbo].[AddressContact] CHECK CONSTRAINT [AddressContact_Address]
GO

Your code

Reproducible code is attached

Stack traces


Verbose output


EF Core version

9.0.4

Database provider

No response

Target framework

.NET 8

Operating system

No response

IDE

Visual Studio 2022 17.13.6

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions