dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.78k stars 3.19k forks source link

ManyToMany join entity type with separate PK results in incorrect FK configuration #26814

Open JakubFojtik opened 2 years ago

JakubFojtik commented 2 years ago

When configuring a many-to-many relationship, if we define the key of the skipped entity separately, the related collection entities stop appearing in the loaded collection, despite being loaded into the change tracker and having the correct keys in database.

Even though this bug is caused by mis-configuration, it is not reported as an error, and causes a confusing situation.

Full fiddle here: https://dotnetfiddle.net/TI9G5V

For this correct & working code snippet:

modelBuilder.Entity<Blog>().HasMany(x => x.Posts).WithMany(x => x.Blogs)
    .UsingEntity<BlogPost>(
        x => x.HasOne(x => x.Post).WithMany(),
        x => x.HasOne(x => x.Blog).WithMany(),
        x => x.HasKey(x => x.OID)
    );

the bug is caused by changing it to this:

// Setting the key here causes entities to not load into collections
modelBuilder.Entity<BlogPost>().HasKey(x => x.OID);

modelBuilder.Entity<Blog>().HasMany(x => x.Posts).WithMany(x => x.Blogs)
    .UsingEntity<BlogPost>(
        x => x.HasOne(x => x.Post).WithMany(),
        x => x.HasOne(x => x.Blog).WithMany()
    );

now EF won't load Blog.Posts, despite loading the Posts into the Change tracker. It seems EF does not consider the entities related despite loading them in the filtered query.

context.Blogs.Include(x => x.Posts).First().Posts.Count
//returns 0

context.ChangeTracker.Entries<Post>().Count()
//returns 1

//database contains exactly a single Blog,Post and BlogPost that has their IDs.
context.Blogs.Single()
context.Posts.Single()
context.Posts.Single(x=>x.Blogs.Contains(context.Blogs.Single()))==context.Posts.Single()
//returns True

This happened because i set all entity primary keys first in OnModelCreating, then add properties and relations as needed. It would be nice to have an error at model build time, instead of strange behavior at query time.

Include provider and version information

EF Core version: 6.0.0 Database provider: InMemory Target framework: .NET 6.0

ajcvickers commented 2 years ago

@AndriySvyryd Configuring the key first results in the FK properties not being required. This also results in different cascade behavior for FKs.

Key first:

Model: 
  EntityType: Blog
    Properties: 
      BlogId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Name (string)
      Url (string)
    Skip navigations: 
      Posts (ICollection<Post>) CollectionPost Inverse: Blogs
    Keys: 
      BlogId PK
  EntityType: BlogPost
    Properties: 
      OID (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
      BlogId (no field, int?) Shadow FK Index
      PostId (no field, int?) Shadow FK Index
    Navigations: 
      Blog (Blog) ToPrincipal Blog
      Post (Post) ToPrincipal Post
    Keys: 
      OID PK
    Foreign keys: 
      BlogPost {'BlogId'} -> Blog {'BlogId'} ToPrincipal: Blog ClientSetNull
      BlogPost {'PostId'} -> Post {'Id'} ToPrincipal: Post ClientSetNull
    Indexes: 
      BlogId 
      PostId 
  EntityType: Post
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Content (string)
      Title (string)
    Skip navigations: 
      Blogs (ICollection<Blog>) CollectionBlog Inverse: Posts
    Keys: 
      Id PK

Key in UsingEntity:

Model: 
  EntityType: Blog
    Properties: 
      BlogId (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Name (string)
      Url (string)
    Skip navigations: 
      Posts (ICollection<Post>) CollectionPost Inverse: Blogs
    Keys: 
      BlogId PK
  EntityType: BlogPost
    Properties: 
      OID (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
      BlogId (no field, int) Shadow Required FK Index
      PostId (no field, int) Shadow Required FK Index
    Navigations: 
      Blog (Blog) ToPrincipal Blog
      Post (Post) ToPrincipal Post
    Keys: 
      OID PK
    Foreign keys: 
      BlogPost {'BlogId'} -> Blog {'BlogId'} ToPrincipal: Blog Cascade
      BlogPost {'PostId'} -> Post {'Id'} ToPrincipal: Post Cascade
    Indexes: 
      BlogId 
      PostId 
  EntityType: Post
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Content (string)
      Title (string)
    Skip navigations: 
      Blogs (ICollection<Blog>) CollectionBlog Inverse: Posts
    Keys: 
      Id PK
JakubFojtik commented 2 years ago

Today i saw the fiddle stopped working, and recreating it using EF 6 and NET 6 does not produce the bugged behavior. I suspect dotnetfiddle was using NET 5 while showing NET 6.

I think this can be closed, unless @ajcvickers reproduced the bug on NET 6 + EF 6?

ajcvickers commented 2 years ago

@JakubFojtik Yes, I used EF Core 6.0 for my repro.