Generic method for updating EFCore joins

后端 未结 1 1432
失恋的感觉
失恋的感觉 2021-02-06 11:20

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

1条回答
  •  深忆病人
    2021-02-06 11:39

    "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(this DbSet dbSet, 
        Expression> fromIdProperty, int fromId, 
        Expression> toIdProperty, int[] toIds)
        where TLink : class, new()
    {
        // link => link.FromId == fromId
        var filter = Expression.Lambda>(
            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>(
            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();
        }
    }
    

    0 讨论(0)
提交回复
热议问题