Using a LINQ ExpressionVisitor to replace primitive parameters with property references in a lambda expression

后端 未结 2 910
春和景丽
春和景丽 2020-12-24 15:20

I\'m in the process of writing a data layer for a part of our system which logs information about automated jobs that run every day - name of the job, how long it ran, what

相关标签:
2条回答
  • 2020-12-24 16:13

    The problem was two-fold:

    • I was misunderstanding how to visit the Lambda expression type. I was still returning a lambda which matched the old delegate instead of returning a new lambda to match the new delegate.

    • I needed to hold a reference to the new ParameterExpression instance, which I wasn't doing.

    The new code looks like this (notice how the visitor now accepts a reference to a ParameterExpression matching the Entity Framework data object):

    class Program
    {
        const string conString = @"myDB";
    
        static void Main(string[] args)
        {
            Expression<Func<string, DateTime, byte, long, bool>> expression = (jobName, ranAt, resultCode, elapsed) => jobName == "Email Notifications" && resultCode == (byte)ResultCode.Failed;
            var criteria = ConvertExpression(expression);
    
            using (MyDataContext dataContext = new MyDataContext(conString))
            {
                List<svc_JobAudit> jobs = dataContext.svc_JobAudit.Where(criteria).ToList();
            }
        }
    
        private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, byte, long, bool>> expression)
        {
            var jobAuditParameter = Expression.Parameter(typeof(svc_JobAudit), "jobAudit");
            var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(
                new ReplaceVisitor()
                   .Modify(expression.Body, jobAuditParameter), jobAuditParameter);
            return newExpression;
        }
    }
    
    class ReplaceVisitor : ExpressionVisitor
    {
        private ParameterExpression parameter;
    
        public Expression Modify(Expression expression, ParameterExpression parameter)
        {
            this.parameter = parameter;
            return Visit(expression);
        }
    
        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            return Expression.Lambda<Func<svc_JobAudit, bool>>(Visit(node.Body), Expression.Parameter(typeof(svc_JobAudit)));
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node.Type == typeof(string))
            {
                return Expression.Property(parameter, "JobName");
            }
            else if (node.Type == typeof(DateTime))
            {
                return Expression.Property(parameter, "RanAt");
            }
            else if (node.Type == typeof(byte))
            {
                return Expression.Property(parameter, "Result");
            }
            else if (node.Type == typeof(long))
            {
                return Expression.Property(parameter, "Elapsed");
            }
            throw new InvalidOperationException();
        }
    }
    
    0 讨论(0)
  • 2020-12-24 16:14

    The accepted answer is 'hardcoded' to some specific types. Here's a more general expression rewriter than can substitute a parameter for any other expression (lambda, constant, ...). In the case of a lambda expression the expression's signature needs to change to incorporate the parameters needed by the substituted value.

    public class ExpressionParameterSubstitute : System.Linq.Expressions.ExpressionVisitor
    {
        private readonly ParameterExpression from;
        private readonly Expression to;
        public ExpressionParameterSubstitute(ParameterExpression from, Expression to)
        {
            this.from = from;
            this.to = to;
        }
    
        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            if (node.Parameters.All(p => p != this.from))
                return node;
    
            // We need to replace the `from` parameter, but in its place we need the `to` parameter(s)
            // e.g. F<DateTime,Bool> subst F<Source,DateTime> => F<Source,bool>
            // e.g. F<DateTime,Bool> subst F<Source1,Source2,DateTime> => F<Source1,Source2,bool>
    
            var toLambda = to as LambdaExpression;
            var substituteParameters = toLambda?.Parameters ?? Enumerable.Empty<ParameterExpression>();
    
            ReadOnlyCollection<ParameterExpression> substitutedParameters
                = new ReadOnlyCollection<ParameterExpression>(node.Parameters
                    .SelectMany(p => p == this.from ? substituteParameters : Enumerable.Repeat(p, 1) )
                    .ToList());
    
            var updatedBody = this.Visit(node.Body);        // which will convert parameters to 'to'
            return Expression.Lambda(updatedBody, substitutedParameters);
        }
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            var toLambda = to as LambdaExpression;
            if (node == from) return toLambda?.Body ?? to;
            return base.VisitParameter(node);
        }
    }
    
    0 讨论(0)
提交回复
热议问题