-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
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.