24

We are porting our existing MVC6 EF6 application to Core.

Is there a simple method in EF Core to update a many-to-many relationship?

My old code from EF6 where we clear the list and overwrite it with the new data no longer works.

var model = await _db.Products.FindAsync(vm.Product.ProductId);
model.Colors.Clear();
model.Colors =  _db.Colors.Where(x => 
vm.ColorsSelected.Contains(x.ColorId)).ToList();
4
  • What do you mean by no longer works? Commented May 25, 2018 at 3:21
  • 1
    It no longer produces the expected result. Entity framework core does change tracking differently. Commented May 29, 2018 at 9:08
  • 2
    I had a similar issue, tried the various answers below and in the end fixed it by making sure I was calling Include() and ThenInclude() on the child collection (Colors in your example). Much simpler. This article is also useful: thereformedprogrammer.net/… Commented Apr 23, 2020 at 15:54
  • It's still not clear what you mean by "no longer works". Just replacing the relationships is the easiest way to do this if the number of relationships is limited and it's possible to do this using EF core. Commented Jan 31, 2021 at 9:50

2 Answers 2

44

This will work for you.

Make a class to have the relationship in:

public class ColorProduct
{
    public int ProductId { get; set; }
    public int ColorId { get; set; }

    public Color Color { get; set; }
    public Product Product { get; set; }
}

Add a ColorProduct collection to your Product and Color classes:

 public ICollection<ColorProduct> ColorProducts { get; set; }

Then use this extension I made to remove the unselected and add the newly selected to the list:

public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
{
    db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
    db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
}

public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
{
    return items
        .GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
        .SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
        .Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
        .Select(t => t.t.item);
}

Using it looks like this:

var model = _db.Products
    .Include(x => x.ColorProducts)
    .FirstOrDefault(x => x.ProductId == vm.Product.ProductId);

_db.TryUpdateManyToMany(model.ColorProducts, vm.ColorsSelected
    .Select(x => new ColorProduct
    {
        ColorId = x,
        ProductId = vm.Product.ProductId
    }), x => x.ColorId);
Sign up to request clarification or add additional context in comments.

9 Comments

Wow, this is amazing. I can use this extension in all of my projects. Thank you for your timely answer.
The critical part of this solution (and the problem in the OP code) is the Include call.
AddRange doesn't have Func<T, TKey> getKey parameter
Is it a good and efficient way? Or did you find any possible other way?
Why checking for the equality of null and default? I guess only default is sufficient (as the default for ref types can be null).
|
1

In order to avoid the LINQ hell in the above answer, the templated "Except" method can be rewritten as such:

public static IEnumerable<TEntity> LeftComplementRight<TEntity, TKey>(
        this IEnumerable<TEntity> left,
        IEnumerable<TEntity> right,
        Func<TEntity, TKey> keyRetrievalFunction)
    {
        var leftSet = left.ToList();
        var rightSet = right.ToList();

        var leftSetKeys = leftSet.Select(keyRetrievalFunction);
        var rightSetKeys = rightSet.Select(keyRetrievalFunction);

        var deltaKeys = leftSetKeys.Except(rightSetKeys);
        var leftComplementRightSet = leftSet.Where(i => deltaKeys.Contains(keyRetrievalFunction.Invoke(i)));
        return leftComplementRightSet;
    }

Furthermore the UpdateManyToMany method can be updated to include entities that have been modified as such:

public void UpdateManyToMany<TDependentEntity, TKey>(
        IEnumerable<TDependentEntity> dbEntries,
        IEnumerable<TDependentEntity> updatedEntries,
        Func<TDependentEntity, TKey> keyRetrievalFunction)
        where TDependentEntity : class
    {
        var oldItems = dbEntries.ToList();
        var newItems = updatedEntries.ToList();
        var toBeRemoved = oldItems.LeftComplementRight(newItems, keyRetrievalFunction);
        var toBeAdded = newItems.LeftComplementRight(oldItems, keyRetrievalFunction);
        var toBeUpdated = oldItems.Intersect(newItems, keyRetrievalFunction);

        this.Context.Set<TDependentEntity>().RemoveRange(toBeRemoved);
        this.Context.Set<TDependentEntity>().AddRange(toBeAdded);
        foreach (var entity in toBeUpdated)
        {
            var changed = newItems.Single(i => keyRetrievalFunction.Invoke(i).Equals(keyRetrievalFunction.Invoke(entity)));
            this.Context.Entry(entity).CurrentValues.SetValues(changed);
        }
    }

which uses another custom templated "Intersect" function to find the intersection of the two sets:

public static IEnumerable<TEntity> Intersect<TEntity, TKey>(
        this IEnumerable<TEntity> left,
        IEnumerable<TEntity> right,
        Func<TEntity, TKey> keyRetrievalFunction)
    {
        var leftSet = left.ToList();
        var rightSet = right.ToList();

        var leftSetKeys = leftSet.Select(keyRetrievalFunction);
        var rightSetKeys = rightSet.Select(keyRetrievalFunction);

        var intersectKeys = leftSetKeys.Intersect(rightSetKeys);
        var intersectionEntities = leftSet.Where(i => intersectKeys.Contains(keyRetrievalFunction.Invoke(i)));
        return intersectionEntities;
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.