Skip to content

[EF Core 8] [Breaking Change] - error on working with explicit many-to-many relations with OnDelete.Restrict #32383

Description

@Gretam11

Description

I'm currently migrating our project to the latest .NET and EF Core version (8), and I stumbled across the narrow, undocumented breaking change.
When you have many-to-many relation (Authors<->Books) with explicit intermediate table (BookAuthors), and you defined relations with OnDelete.Restrict action, then SaveChanges() throws an error if you try to modify list of linked entities (BookAuthors) through the root entity (Book).

Example

Entities:

public class Author
{
    public long Id { get; set; }
    public string Name { get; set; }
    public List<BookAuthor> BookAuthors { get; set; }
}

public class Book
{
    public long Id { get; set; }
    public string Name { get; set; }
    public List<BookAuthor> BookAuthors { get; set; }
}

public class BookAuthor
{
    public long BookId { get; set; }
    public long AuthorId { get; set; }
    public Book Book { get; set; }
    public Author Author { get; set; }
}

Model config:

modelBuilder.Entity<BookAuthor>().HasKey(x => new { x.AuthorId, x.BookId });

modelBuilder.Entity<BookAuthor>()
    .HasOne<Author>(x => x.Author)
    .WithMany(x => x.BookAuthors)
    .OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<BookAuthor>()
    .HasOne<Book>(x => x.Book)
    .WithMany(x => x.BookAuthors)
    .OnDelete(DeleteBehavior.Restrict);

Actual operation:

var book = await dbContext.Books
                .Include(x => x.BookAuthors)
                .FirstAsync();

book.BookAuthors.RemoveAt(0);

await dbContext.SaveChangesAsync(); // error is thrown here

Exception:

Unhandled exception. System.InvalidOperationException: The property 'BookAuthor.BookId' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key,
 first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.
   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.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Program.Main() in D:\projects\EF8M2MUpdateError\EF8M2MUpdateError\Program.cs:line 109
   at Program.<Main>()

Versions information

EF Core version: 8.0.0
Database provider: ANY
Target framework: .NET 8.0

Additional information

Considering that normal one-to-many relations update with OnDelete.Restrict would also fail in such scenario (with clearer message though: The association between entity types 'Book' and 'Author' has been severed, but the relationship is either marked as required or is implicitly required), probably, the behavior above is correct, and it was wrongly working in EF Core v7. But I decided to create a ticket anyway, maybe it'll help someone who also stumbles across the same issue (since it's kind of a breaking change).

Reproduction repository

Here you can check the minimal project where this bug is reproduced (link).
You can also change package versions back to EF.* v7 to check that it wasn't the case in v7.

Activity

  1. ajcvickers commented on Nov 27, 2023

    @ajcvickers
    Contributor

    Note for triage: everything seems to be by-design here.

  2. ADNewsom09 commented on Nov 27, 2023

    @ADNewsom09
    Contributor

    We also ran into this same breaking issue on our 7 --> 8 upgrade

  3. jirikanda commented on Nov 29, 2023

    @jirikanda

    We found this problem after upgrading our project from EF Core 7 to EF Core 8.
    Hope, the code mentioned by @Gretam11 is corrent in EF Core 7 and also corrent in EF Core 8. Even if it is not purely clean it became usual.

    This breaking change (if by-design for triage) is not mentioned at Breaking changes in EF Core 8.0.

  4. jirikanda commented on Nov 30, 2023

    @jirikanda

    Why the code is correct? The same code is mentioned in the Severing a relationship chapter of the documentation.

    Severing a relationship now works only for DeleteBehaviors Cascade and ClientCascade. For all other DeleteBehavios the change tracker throws an exception. The exceptions say we cannot change or modify the value of the (composed) primary key, but we are not trying to do it, we want to delete the entity representing the many-to-many relationship.

    The only workaround I found is to registrer entities to be removed prior to removing them from the collection.

    dbContext.RemoveRange(book.BookAuthors);
    book.BookAuthors.Clear();
    
  5. ostracoder commented on Nov 30, 2023

    @ostracoder

    I also have this regression but only on join tables with payload.

    It seems that just declaring the relationships as Cascade in the EF model is another possible workaround. You don't have to actually apply any change to the database tables. Ugly but it works.

  6. self-assigned this
    on Dec 1, 2023
  7. added this to the 8.0.x milestone on Dec 1, 2023
  8. 10 remaining items

  9. daemons88 commented on Jan 21, 2024

    @daemons88

    Why is this closed if the fix doesn't come until version 8.0.2?

  10. ajcvickers commented on Jan 21, 2024

    @ajcvickers
    Contributor

    @daemons88 Because all the work is done and the fix is now ready to ship with 8.0.2.

  11. ADNewsom09 commented on Jan 21, 2024

    @ADNewsom09
    Contributor

    If you need it now you can use the nightly builds.

  12. daemons88 commented on Jan 22, 2024

    @daemons88

    Okay, thanks for your answers!

  13. t00 commented on Jan 31, 2024

    @t00

    Will the fix also work if the entity has a composite key with references to the same table?
    I am having the same issue with the following table:

        public class RoleInRole
        {
            [Key, Column(Order = 0)]
            public int RoleId { get; set; }
    
            public Role Role { get; set; }
    
            [Key, Column(Order = 1)]
            public int InRoleId { get; set; }
    
            public Role InRole { get; set; }
    
            public RoleAccess Allow { get; set; }
        }
    
  14. stephanmo commented on Feb 7, 2024

    @stephanmo

    @ajcvickers When do you plan to release version 8.0.2

  15. LennardF1989 commented on Feb 13, 2024

    @LennardF1989

    Would like to know the same! This "issue" is affecting a solution of mine, but there is no daily build with this fix available, so I'm also eagerly awaiting the official release of 8.0.2 :)

  16. ajcvickers commented on Feb 13, 2024

    @ajcvickers
    Contributor

    @LennardF1989 The latest daily build contains this fix. If you are seeing otherwise, then please open a new issue and attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

  17. ErikEJ commented on Feb 13, 2024

    @ErikEJ
    Contributor
  18. LennardF1989 commented on Feb 13, 2024

    @LennardF1989

    That's an EFCore 9 version, no? I would like to stick to the 8.x range :) The dotnet8 feed doesn't go further than 8.0.0.

    However, I just tried to workaround mentioned by @jirikanda and can confirm that first running a Remove/RemoveRange on the DBContext prior to removing it from a navigation property does the trick as well!

  19. removed their assignment
    on Aug 31, 2024
  20. added theissue type on Jun 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions