使用LINQ ExpressionVisitor将基本参数替换为lambda表达式中的属性引用

Rep*_*Man 13 c# linq lambda expression visitor

我正在为我们系统的一部分编写数据层,该数据层记录有关每天运行的自动作业的信息 - 作业名称,运行时间,结果,等等.

我正在使用Entity Framework与数据库交谈,但我试图将这些细节隐藏在更高级别的模块之外,我不希望实体对象本身被暴露.

但是,我想使我的界面在用于查找作业信息的标准中非常灵活.例如,用户界面应允许用户执行复杂的查询,例如"给我所有名为'hello'的作业,该作业在上午10:00到11:00之间运行失败." 显然,这看起来像是动态构建Expression树的工作.

所以我希望我的数据层(存储库)能够接受类型Expression<Func<string, DateTime, ResultCode, long, bool>>(lambda表达式)的LINQ表达式,然后在后台将该lambda转换为我的实体框架ObjectContext可以用作Where()子句中的过滤器的表达式.

简而言之,我正在尝试将类型的lambda表达式转换Expression<Func<string, DateTime, ResultCode, long, bool>>Expression<Func<svc_JobAudit, bool>>,其中svc_JobAuditEntity Framework数据对象对应于存储作业信息的表.(第一个委托中的四个参数分别对应于作业名称,运行时间,结果以及分别在MS中花费的时间)

我在使用该ExpressionVisitor课程时取得了很好的进展,直到我碰到了一堵砖墙并收到了一条InvalidOperationException错误消息:

从"VisitLambda"调用时,重写"System.Linq.Expressions.ParameterExpression"类型的节点必须返回相同类型的非null值.或者,覆盖"VisitLambda"并将其更改为不访问此类型的子项.

我完全不知所措.为什么它不允许我将引用参数的表达式节点转换为引用属性的节点?还有另一种方法可以解决这个问题吗?

以下是一些示例代码:

namespace ExpressionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Expression<Func<string, DateTime, ResultCode, long, bool>> expression = (myString, myDateTime, myResultCode, myTimeSpan) => myResultCode == ResultCode.Failed && myString == "hello";
            var result = ConvertExpression(expression);
        }

        private static Expression<Func<svc_JobAudit, bool>> ConvertExpression(Expression<Func<string, DateTime, ResultCode, long, bool>> expression)
        {
            var newExpression = Expression.Lambda<Func<svc_JobAudit, bool>>(new ReplaceVisitor().Modify(expression), Expression.Parameter(typeof(svc_JobAudit)));
            return newExpression;
        }
    }

    class ReplaceVisitor : ExpressionVisitor
    {
        public Expression Modify(Expression expression)
        {
            return Visit(expression);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (node.Type == typeof(string))
            {
                return Expression.Property(Expression.Parameter(typeof(svc_JobAudit)), "JobName");
            }
            return node;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Rep*_*Man 8

问题是双重的:

  • 我误解了如何访问Lambda表达式类型.我还在返回一个与旧委托匹配的lambda,而不是返回一个新的lambda来匹配新的委托.

  • 我需要持有对新ParameterExpression实例的引用,我没有这样做.

新代码如下所示(请注意访问者现在如何接受对ParameterExpression匹配Entity Framework数据对象的引用):

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();
    }
}
Run Code Online (Sandbox Code Playgroud)


Ian*_*cer 5

可接受的答案被“硬编码”为某些特定类型。这是一个更通用的表达式重写器,不能用参数替代任何其他表达式(lambda,constant,...)。对于lambda表达式,需要更改表达式的签名以合并替换值所需的参数。

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);
    }
}
Run Code Online (Sandbox Code Playgroud)