efcore
efcore copied to clipboard
Self-referencing many-to-many between TPH subtypes creates cascade cycles
These types create a valid model for SQL Server:
public abstract class Animal
{
public int Id { get; set; }
public ICollection<Animal> Humans { get; } = new List<Animal>();
public ICollection<Animal> Pets { get; } = new List<Animal>();
}
public class Pet : Animal
{
}
public class Human : Animal
{
}
Notice that only one direction is configured as Cascade.
Model:
EntityType: Animal Abstract
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
Discriminator (no field, string) Shadow Required AfterSave:Throw
Skip navigations:
Humans (ICollection<Animal>) CollectionAnimal Inverse: Pets
Pets (ICollection<Animal>) CollectionAnimal Inverse: Humans
Keys:
Id PK
EntityType: AnimalAnimal (Dictionary<string, object>) CLR Type: Dictionary<string, object>
Properties:
HumansId (no field, int) Indexer Required PK FK AfterSave:Throw
PetsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
Keys:
HumansId, PetsId PK
Foreign keys:
AnimalAnimal (Dictionary<string, object>) {'HumansId'} -> Animal {'Id'} Cascade
AnimalAnimal (Dictionary<string, object>) {'PetsId'} -> Animal {'Id'} ClientCascade
Indexes:
PetsId
EntityType: Human Base: Animal
EntityType: Pet Base: Animal
But moving the relationships to subtypes:
public abstract class Animal
{
public int Id { get; set; }
}
public class Pet : Animal
{
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class Human : Animal
{
public ICollection<Pet> Pets { get; } = new List<Pet>();
}
Results in both types being Cascade.
Model:
EntityType: Animal Abstract
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
Discriminator (no field, string) Shadow Required AfterSave:Throw
Keys:
Id PK
EntityType: Human Base: Animal
Skip navigations:
Pets (ICollection<Pet>) CollectionPet Inverse: Humans
EntityType: HumanPet (Dictionary<string, object>) CLR Type: Dictionary<string, object>
Properties:
HumansId (no field, int) Indexer Required PK FK AfterSave:Throw
PetsId (no field, int) Indexer Required PK FK Index AfterSave:Throw
Keys:
HumansId, PetsId PK
Foreign keys:
HumanPet (Dictionary<string, object>) {'HumansId'} -> Human {'Id'} Cascade
HumanPet (Dictionary<string, object>) {'PetsId'} -> Pet {'Id'} Cascade
Indexes:
PetsId
EntityType: Pet Base: Animal
Skip navigations:
Humans (ICollection<Human>) CollectionHuman Inverse: Pets
Which throws on SQL Server:
Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): Introducing FOREIGN KEY constraint 'FK_HumanPet_Animals_PetsId' on table 'HumanPet' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at Microsoft.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean isAsync, Int32 timeout, Boolean asyncWrite)
at Microsoft.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String methodName)
at Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery()
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
at Microsoft.EntityFrameworkCore.Migrations.MigrationCommand.ExecuteNonQuery(IRelationalConnection connection, IReadOnlyDictionary`2 parameterValues)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.ExecuteNonQuery(IEnumerable`1 migrationCommands, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreator.CreateTables()
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseCreator.EnsureCreated()
at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated()
at Program.Main() in C:\local\code\AllTogetherNow\Daily\Daily.cs:line 80
ClientConnectionId:a095e670-5eed-42ba-bdef-57d7e30a170d
Error Number:1785,State:0,Class:16
Full code:
public static class Your
{
public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Database=Test";
}
public abstract class Animal
{
public int Id { get; set; }
}
public class Pet : Animal
{
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class Human : Animal
{
public ICollection<Pet> Pets { get; } = new List<Pet>();
}
public class SomeDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
//.UseNpgsql("Server=127.0.0.1;Port=5432;Database=myDataBase;User Id=myUsername;Password=myPassword;")
//.UseSqlite("Data Source=test.db")
//.UseInMemoryDatabase("Test")
// .UseLazyLoadingProxies()
.UseSqlServer(Your.ConnectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Human> Humans => Set<Human>();
public DbSet<Pet> Pets => Set<Pet>();
}
public class Program
{
public static void Main()
{
using (var context = new SomeDbContext())
{
Console.WriteLine(context.Model.ToDebugString());
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
}
}
}