One thing that I am finding really tedious with the way EFCore handles many-to-many relationships is updating an entities joined collections. It is a frequent requirement that
"All very simple stuff", but not so simple to factorize, especially taking into account different key types, explicit or shadow FK properties etc., at the same time keeping the minimum method arguments.
Here is the best factorized method I can think of, which works for link (join) entities having 2 explicit int
FKs:
public static void UpdateLinks<TLink>(this DbSet<TLink> dbSet,
Expression<Func<TLink, int>> fromIdProperty, int fromId,
Expression<Func<TLink, int>> toIdProperty, int[] toIds)
where TLink : class, new()
{
// link => link.FromId == fromId
var filter = Expression.Lambda<Func<TLink, bool>>(
Expression.Equal(fromIdProperty.Body, Expression.Constant(fromId)),
fromIdProperty.Parameters);
var existingLinks = dbSet.Where(filter).ToList();
var toIdFunc = toIdProperty.Compile();
var deleteLinks = existingLinks
.Where(link => !toIds.Contains(toIdFunc(link)));
// toId => new TLink { FromId = fromId, ToId = toId }
var toIdParam = Expression.Parameter(typeof(int), "toId");
var createLink = Expression.Lambda<Func<int, TLink>>(
Expression.MemberInit(
Expression.New(typeof(TLink)),
Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, Expression.Constant(fromId)),
Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)),
toIdParam);
var addLinks = toIds
.Where(toId => !existingLinks.Any(link => toIdFunc(link) == toId))
.Select(createLink.Compile());
dbSet.RemoveRange(deleteLinks);
dbSet.AddRange(addLinks);
}
All it needs is the join entity DbSet
, two expressions representing the FK properties, and the desired values. The property selector expressions are used to dynamically build query filters as well as composing and compiling a functor to create and initialize new link entity.
The code is not that hard, but requires System.Linq.Expressions.Expression
methods knowledge.
The only difference with handwritten code is that
Expression.Constant(fromId)
inside filter
expression will cause EF generating a SQL query with constant value rather than parameter, which will prevent query plan caching. It can be fixed by replacing the above with
Expression.Property(Expression.Constant(new { fromId }), "fromId")
With that being said, the usage with your sample would be like this:
public static void UpdateCars(int personId, int[] carIds)
{
using (var db = new PersonCarDbContext())
{
db.PersonCars.UpdateLinks(pc => pc.PersonId, personId, pc => pc.CarId, carIds);
db.SaveChanges();
}
}
and also other way around:
public static void UpdatePersons(int carId, int[] personIds)
{
using (var db = new PersonCarDbContext())
{
db.PersonCars.UpdateLinks(pc => pc.CarId, carId, pc => pc.PersonId, personIds);
db.SaveChanges();
}
}