Make changing many-to-many collections easier
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
I have a ManyToMany relationship that I want to update. My steps to update are:
1. Load entity from Db, including the related collection
2. Clear the collection
3. Add all the related entities back in from a disconnected entity (the updated state from the frontend)
This works with a OneToMany relationship. Which surprised me, because I expected the familiar "entity with the same key value is already being tracked" exception.
I know how to solve my particular problem, but I'm curios if there is a way to make the "just clear and add the new state" method work on ManyToMany relationships?
Am I even supposed to use this method on OneToMany relationships or does it have any bad effects?
I saw that it fires a full UPDATE statement for every child and every field. Not optimal, but if I know about this behavior I can decide if the simpler code is worth it for me.
I would have expected this drawback to not even exist for the ManyToMany case, since its only updating/creating the joinEntity.
I created a fully reproducible sample.
```
// One To Many works
using (var seedContext = new Context())
{
seedContext.Database.Migrate();
var parentWithChild = new Parent();
parentWithChild.Children.Add(new Child());
seedContext.Add(parentWithChild);
await seedContext.SaveChangesAsync();
}
using (var updateContext = new Context())
{
var disconnectedParent = updateContext.Parents
.AsNoTracking()
.Include(x => x.Children)
.First();
var trackedParent = updateContext.Parents
.Include(x => x.Children)
.First();
trackedParent.Children.Clear();
trackedParent.Children.AddRange(disconnectedParent.Children);
var entries = updateContext.ChangeTracker.Entries().ToList();
// = 3 Entries (1 Parent Unchanged, 1 Child (shared) Deleted, 1 Child (shared) Modified
}
// Many To Many does not work
using (var seedContext = new Context())
{
var post = new Post();
post.Tags.Add(new Tag());
seedContext.Add(post);
await seedContext.SaveChangesAsync();
}
using (var updateContext = new Context())
{
var disconnectedPost = updateContext.Posts
.AsNoTracking()
.Include(x => x.Tags)
.First();
var trackedPost = updateContext.Posts
.Include(x => x.Tags)
.First();
trackedPost.Tags.Clear();
trackedPost.Tags.AddRange(disconnectedPost.Tags);
var entries = updateContext.ChangeTracker.Entries().ToList();
// crashes with "Entity with the same key is already being tracked" Exception
}
public class Context : DbContext
{
public DbSet Parents { get; set; }
public DbSet Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connStr = new SqliteConnectionStringBuilder { DataSource = $@"file:db?mode=memory&cache=shared" }.ConnectionString;
optionsBuilder.UseSqlite(connStr);
base.OnConfiguring(optionsBuilder);
}
}
public class Parent
{
public Guid Id { get; set; }
public List Children { get; set; } = new();
}
public class Child
{
public Guid Id { get; set; }
public Parent Parent { get; set; }
public Guid ParentId { get; set; }
}
public class Post
{
public Guid Id { get; set; }
public List Tags { get; set; } = new();
}
public class Tag
{
public Guid Id { get; set; }
public List Posts { get; set; } = new();
}
```
### Include provider and version information
EF Core version: 6.0.3
Database provider: Sqlite
Contributor guide
Assessment
This issue has not been assessed yet.