Search code examples
c#linqexpressionentity-framework-plus

How to combine multiple c# Lambda Expressions (Expression<Func<T, T>>)


I have the following function which is actually a wrapper around Z.EntityFramework.Plus bulk update:

    public static int UpdateBulk<T>(this IQueryable<T> query, Expression<Func<T, T>> updateFactory) where T : IBaseEntity, new()
    {
        Expression<Func<T, T>> modifiedExpression = x => new T() { ModifiedBy = "Test", ModifiedDate = DateTime.Now };
        var combine = Expression.Lambda<Func<T, T>>(
            Expression.AndAlso(
                Expression.Invoke(updateFactory, updateFactory.Parameters),
                Expression.Invoke(modifiedExpression, modifiedExpression.Parameters)
            ),
            updateFactory.Parameters.Concat(modifiedExpression.Parameters)
        );  //This returns an error

        return query.Update(combine);
    }

Called like this:

        decimal probId = ProbId.ParseDecimal();

        db.Problems
            .Where(e => e.ProbId == probId)
            .UpdateBulk(e => new Problem() {
                CatId = Category.ParseNullableInt(),
                SubCatId = SubCategory.ParseNullableInt(),
                ListId = Problem.ParseNullableInt()
            });

Where IBaseEntity is defined as follows:

public abstract class IBaseEntity
{
    public System.DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }

    public System.DateTime ModifiedDate { get; set; }
    public string ModifiedBy { get; set; }

    public string DeletedBy { get; set; }
}

The 'Problem' class by the way implements IBaseEntity.

What I want to do is automatically append ModifiedBy and ModifiedDate to updateFactory in the UpdateBulk function so that this doesn't have to be done in every call to UpdateBulk.

I tried in the above UpdateBulk function to combine the parsed 'updateFactory' expression with the 'modifiedExpression' but it returns the error:

the binary operator AndAlso is not defined for the types 'Problem'

Is it possible to merge Expression like this and if so, what am I doing wrong? If not, how can I merge ModifiedBy = "Test", ModifiedDate = DateTime.Now into the updateFactory expression?

Thanks, Rod


Solution

  • You cannot use AndAlso, since that's meant for the BinaryExpression - Expression<Func<T,bool>>, In this case you need Expression Visitor, as defined here by Marc Gravell (so he deserves all the credit)

    I am using the same to provide a solution in your case, with an assumption of Problem class schema, pasting the Linqpad code:

    void Main()
    {
      var final = UpdateBulk((Problem p) => new Problem{CatId = 1,SubCatId = 2, ListId=3});
    
     // final is of type Expression<Func<T,T>>, which can be used for further processing
    
      final.Dump();
    }
    
    public static Expression<Func<T, T>> UpdateBulk<T>(Expression<Func<T, T>> updateFactory) where T : IBaseEntity, new()
    {
        Expression<Func<T, T>> modifiedExpression = x => new T() { ModifiedBy = "Test", ModifiedDate = DateTime.Now };
    
        var result = Combine(updateFactory, modifiedExpression);
    
        return result;
    }
    
    
    static Expression<Func<TSource, TDestination>> Combine<TSource, TDestination>(
        params Expression<Func<TSource, TDestination>>[] selectors)
    {
        var param = Expression.Parameter(typeof(TSource), "x");
        return Expression.Lambda<Func<TSource, TDestination>>(
            Expression.MemberInit(
                Expression.New(typeof(TDestination).GetConstructor(Type.EmptyTypes)),
                from selector in selectors
                let replace = new ParameterReplaceVisitor(
                      selector.Parameters[0], param)
                from binding in ((MemberInitExpression)selector.Body).Bindings
                      .OfType<MemberAssignment>()
                select Expression.Bind(binding.Member,
                      replace.VisitAndConvert(binding.Expression, "Combine")))
            , param);
    }
    
    class ParameterReplaceVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression from, to;
        public ParameterReplaceVisitor(ParameterExpression from, ParameterExpression to)
        {
            this.from = from;
            this.to = to;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == from ? to : base.VisitParameter(node);
        }
    }
    
    public abstract class IBaseEntity
    {
        public System.DateTime CreatedDate { get; set; }
        public string CreatedBy { get; set; }
    
        public System.DateTime ModifiedDate { get; set; }
        public string ModifiedBy { get; set; }
    
        public string DeletedBy { get; set; }
    }
    
    public class Problem : IBaseEntity
    {
        public int CatId { get; set; }
    
        public int SubCatId { get; set; }
    
        public int ListId { get; set; }
    }