Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deletion can cause error "The association between entity types A and B has been severed..." #30122

Closed
edwiles opened this issue Jan 23, 2023 · 2 comments · Fixed by #30204
Closed
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported Servicing-approved type-bug
Milestone

Comments

@edwiles
Copy link

edwiles commented Jan 23, 2023

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

  1. 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>
  1. 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; }
    }
}
  1. 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();
        }
    }
}
  1. Create the database and run the program:
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet run

The program returns successfully, without output.

  1. 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>
  1. 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

@ajcvickers
Copy link
Member

Note for triage: looks like a regression when entities deleted due to changes in one relationship are then also severed by changes in another relationship.

@edwiles Workaround is to call db.ChangeTracker.DetectChanges(); between Inputs.Things.Clear(); and Outputs.ThingOutputs.Clear();.

@edwiles
Copy link
Author

edwiles commented Jan 25, 2023

Thanks @ajcvickers , that workaround is working for our real code. Would appreciate a fix when you can.

@ajcvickers ajcvickers self-assigned this Jan 28, 2023
@ajcvickers ajcvickers added this to the 7.0.x milestone Jan 28, 2023
ajcvickers added a commit that referenced this issue Jan 31, 2023
…inished

Fixes #30122

Because the entities for which the relationship is being severed may end up being deleted later on in SaveChanges.
@ajcvickers ajcvickers modified the milestones: 7.0.x, 8.0.0 Jan 31, 2023
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Jan 31, 2023
ajcvickers added a commit that referenced this issue Feb 3, 2023
Fixes #30122
Fixes #30135

EF7 contained some new calls to local `DetectChanges`. This resulted in re-entrance into `DetectChanges` for some types of graphs. This change prevents that.
ajcvickers added a commit that referenced this issue Feb 9, 2023
Fixes #30122
Fixes #30135

EF7 contained some new calls to local `DetectChanges`. This resulted in re-entrance into `DetectChanges` for some types of graphs. This change prevents that.
@ajcvickers ajcvickers modified the milestones: 8.0.0, 7.0.x Feb 9, 2023
@ajcvickers ajcvickers reopened this Feb 11, 2023
ajcvickers added a commit that referenced this issue Feb 14, 2023
Fixes #30122
Fixes #30135

EF7 contained some new calls to local `DetectChanges`. This resulted in re-entrance into `DetectChanges` for some types of graphs. This change prevents that.
@ajcvickers ajcvickers modified the milestones: 7.0.x, 7.0.4 Feb 14, 2023
@ajcvickers ajcvickers changed the title .NET 7 regression: Deletion can cause error "The association between entity types A and B has been severed etc." Deletion can cause error "The association between entity types A and B has been severed..." Mar 14, 2023
@ajcvickers ajcvickers removed their assignment Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported Servicing-approved type-bug
Projects
None yet
2 participants