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

Make changing many-to-many collections easier #27677

Open
Maskoe opened this issue Mar 21, 2022 · 1 comment
Open

Make changing many-to-many collections easier #27677

Maskoe opened this issue Mar 21, 2022 · 1 comment
Labels
area-change-tracking consider-for-current-release customer-reported punted-for-7.0 Originally planned for the EF Core 7.0 (EF7) release, but moved out due to resource constraints. type-enhancement
Milestone

Comments

@Maskoe
Copy link

Maskoe commented Mar 21, 2022

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<Parent> Parents { get; set; }
    public DbSet<Post> 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<Child> 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<Tag> Tags { get; set; } = new();
}

public class Tag
{
    public Guid Id { get; set; }
    public List<Post> Posts { get; set; } = new();
}

Include provider and version information

EF Core version: 6.0.3
Database provider: Sqlite

@ajcvickers
Copy link
Member

The first case works because DetectChanges sees the new entity has a generated key and hence marks the new entity has Modified instead of Added. This doesn't work in the second case because clearing the collection only marks the join entities as Deleted, leaving the actual Tag entities as Unchanged:

Post {Id: 09529e59-ed3d-4eba-9e1c-ecb334f25df8} Unchanged
  Id: '09529e59-ed3d-4eba-9e1c-ecb334f25df8' PK
  Tags: []
PostTag (Dictionary<string, object>) {PostsId: 09529e59-ed3d-4eba-9e1c-ecb334f25df8, TagsId: a37debfb-abaf-492d-972e-af7ca3ea9dea} Deleted
  PostsId: '09529e59-ed3d-4eba-9e1c-ecb334f25df8' PK FK
  TagsId: 'a37debfb-abaf-492d-972e-af7ca3ea9dea' PK FK
Tag {Id: a37debfb-abaf-492d-972e-af7ca3ea9dea} Unchanged
  Id: 'a37debfb-abaf-492d-972e-af7ca3ea9dea' PK
  Posts: []

This is technically correct, since clearing the collection does not mean that the entities cannot exist, like it does for dependents of a required one-to-many. However, we should look into making the deletion of all entities in a skip navigation. See also #27436, which is different, but also about managing many-to-many relationships.

@ajcvickers ajcvickers added this to the 7.0.0 milestone Apr 4, 2022
@ajcvickers ajcvickers self-assigned this Apr 4, 2022
@ajcvickers ajcvickers changed the title Updating Related Collections via Clearing and Readding Make changing many-to-many collections easier Apr 4, 2022
@ajcvickers ajcvickers added propose-punt consider-for-next-release punted-for-7.0 Originally planned for the EF Core 7.0 (EF7) release, but moved out due to resource constraints. and removed propose-punt labels Jul 6, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0, Backlog Jul 7, 2022
@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 consider-for-current-release customer-reported punted-for-7.0 Originally planned for the EF Core 7.0 (EF7) release, but moved out due to resource constraints. type-enhancement
Projects
None yet
Development

No branches or pull requests

2 participants