-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Milestone
Description
When multiple cascade paths have been avoided by setting one path to DeleteBehavior.Restrict
, deletion can fail with the error:
Unhandled exception. System.InvalidOperationException: The association between entity types 'A' and 'B' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.
The code below runs successfully with EF Core 6.0.13, but fails with 7.0.0-7.0.2.
Steps to reproduce
- Create a C# console application, with the following
.csproj
file that uses EF Core 6.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.13" />
</ItemGroup>
</Project>
- Create the following code in a file
Model.cs
:
using Microsoft.EntityFrameworkCore;
namespace ConsoleApp.SQLite
{
public class MyContext : DbContext
{
public DbSet<Root> Roots { get; set; }
public DbSet<Inputs> Inputs { get; set; }
public DbSet<Outputs> Outputs { get; set; }
public DbSet<ThingOutputs> ThingOutputs { get; set; }
public DbSet<Thing> Thing { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=mydb");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Root>().HasData(new Root { Id = 1 });
modelBuilder.Entity<Inputs>().HasData(new Inputs { Id = 1, RootId = 1 });
modelBuilder.Entity<Thing>().HasData(new Thing { Id = 1, InputsId = 1 });
modelBuilder.Entity<Outputs>().HasData(new Outputs { Id = 1, RootId = 1 });
modelBuilder.Entity<ThingOutputs>().HasData(new ThingOutputs { Id = 1, OutputsId = 1, ThingId = 1 });
// Avoid multiple cascade paths (our "real" code uses SQL Server)
// ON DELETE CASCADE path 1: Root -> Inputs -> Thing
// ON DELETE CASCADE path 2: Root -> Outputs -> ThingOutputs -> Thing
// Broken by removing Outputs -> ThingOutputs
SetForeignKeyDeleteBehaviour<Outputs, ThingOutputs>(modelBuilder, DeleteBehavior.Restrict);
}
private static void SetForeignKeyDeleteBehaviour<TParent, TChild>(ModelBuilder modelBuilder, DeleteBehavior deleteBehavior)
{
modelBuilder.Model.GetEntityTypes().First(t => t.ClrType == typeof(TChild))
.GetForeignKeys().First(k => k.PrincipalEntityType.ClrType == typeof(TParent))
.DeleteBehavior = deleteBehavior;
}
}
public class Root
{
public int Id { get; set; }
public Inputs Inputs { get; set; }
public Outputs Outputs { get; set; }
}
public class Inputs
{
public int Id { get; set; }
public int RootId { get; set; }
public Root Root { get; set; }
public IList<Thing> Things { get; set; }
}
public class Thing
{
public int Id { get; set; }
public int InputsId { get; set; }
public Inputs Inputs { get; set; }
}
public class Outputs
{
public int Id { get; set; }
public int RootId { get; set; }
public Root Root { get; set; }
public IList<ThingOutputs> ThingOutputs { get; set; }
}
public class ThingOutputs
{
public int Id { get; set; }
public int OutputsId { get; set; }
public Outputs Outputs { get; set; }
public int ThingId { get; set; }
public Thing Thing { get; set; }
}
}
- Create the following code in a file
Program.cs
:
using Microsoft.EntityFrameworkCore;
namespace ConsoleApp.SQLite
{
public class Program
{
public static void Main()
{
using var db = new MyContext();
// Get data
var outputs = db.Roots
.Include(x => x.Inputs)
.ThenInclude(x => x.Things)
.Include(x => x.Outputs)
.ThenInclude(x => x.ThingOutputs)
.SingleOrDefault(x => x.Id == 1);
// Delete data
DeleteData(outputs);
// Save
db.SaveChanges();
}
private static void DeleteData(Root root)
{
// We want to delete all the Things. So we clear the Things and their outputs.
root.Inputs.Things.Clear();
root.Outputs.ThingOutputs.Clear();
}
}
}
- Create the database and run the program:
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet run
The program returns successfully, without output.
- Modify the
.csproj
file to upgrade to EF Core 7:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
</ItemGroup>
</Project>
- Recreate the database and run the program:
dotnet ef database drop
dotnet ef database update
dotnet run
The program execution fails with the error:
Unhandled exception. System.InvalidOperationException: The association between entity types 'Outputs' and 'ThingOutputs' has been severed, but the relationship is either marked as required or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, configure the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleConceptualNulls(Boolean sensitiveLoggingEnabled, Boolean force, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleNullForeignKey(IProperty property, Boolean setModified, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean isMaterialization, Boolean setModified, Boolean isCascadeDelete, CurrentValueType valueType)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean isMaterialization, Boolean setModified, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.set_Item(IPropertyBase propertyBase, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.ConditionallyNullForeignKeyProperties(InternalEntityEntry dependentEntry, InternalEntityEntry principalEntry, IForeignKey foreignKey)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.LocalDetectChanges(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry, HashSet`1 visited)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry, HashSet`1 visited)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.CascadeDelete(InternalEntityEntry entry, Boolean force, IEnumerable`1 foreignKeys)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleConceptualNulls(Boolean sensitiveLoggingEnabled, Boolean force, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.HandleNullForeignKey(IProperty property, Boolean setModified, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean isMaterialization, Boolean setModified, Boolean isCascadeDelete, CurrentValueType valueType)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean isMaterialization, Boolean setModified, Boolean isCascadeDelete)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.set_Item(IPropertyBase propertyBase, Object value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.ConditionallyNullForeignKeyProperties(InternalEntityEntry dependentEntry, InternalEntityEntry principalEntry, IForeignKey foreignKey)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigationBase navigationBase, IEnumerable`1 added, IEnumerable`1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.LocalDetectChanges(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager)
at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.DetectChanges()
at Microsoft.EntityFrameworkCore.DbContext.TryDetectChanges()
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
at ConsoleApp.SQLite.Program.Main() in C:...\Program.cs:line 23
Further technical details
EF Core version: 7.0.2
.NET SDK: 7.0.100
Database Provider: Microsoft.EntityFrameworkCore.Sqlite (though in our "real" code we use SqlServer)
Operating system: Windows 10 Enterprise
IDE: Visual Studio 17.4.2