将表达式参数作为参数传递给另一个表达式 [英] Pass expression parameter as argument to another expression

查看:31
本文介绍了将表达式参数作为参数传递给另一个表达式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个过滤结果的查询:

I have a query which filters results:

public IEnumerable<FilteredViewModel> GetFilteredQuotes()
{
    return _context.Context.Quotes.Select(q => new FilteredViewModel
    {
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => q.User.Id == qpi.ItemOrder))
    });
}

在 where 子句中,我使用参数 q 将属性与来自参数 qpi 的属性进行匹配.因为过滤器将在多个地方使用,所以我试图将 where 子句重写为一个表达式树,它看起来像这样:

In the where clause I'm using the parameter q to match a property against a property from the parameter qpi. Because the filter will be used in several places I'm trying to rewrite the where clause to an expression tree which would look like something like this:

public IEnumerable<FilteredViewModel> GetFilteredQuotes()
{
    return _context.Context.Quotes.Select(q => new FilteredViewModel
    {
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.AsQueryable().Where(ExpressionHelper.FilterQuoteProductImagesByQuote(q)))
    });
}

在此查询中,参数 q 用作函数的参数:

In this query the parameter q is used as a parameter to the function:

public static Expression<Func<QuoteProductImage, bool>> FilterQuoteProductImagesByQuote(Quote quote)
{
    // Match the QuoteProductImage's ItemOrder to the Quote's Id
}

我将如何实现这个功能?或者我应该一起使用不同的方法?

How would I implement this function? Or should I use a different approach alltogether?

推荐答案

如果我理解正确,您想在另一个表达式树中重用表达式树,并且仍然允许编译器为您完成构建表达式树的所有魔法.

If I understand correctly, you want to reuse an expression tree inside another one, and still allow the compiler to do all the magic of building the expression tree for you.

这实际上是可能的,我已经在很多场合这样做了.

This is actually possible, and I have done it in many occasions.

诀窍是将您的可重用部分包装在方法调用中,然后在应用查询之前将其解开.

The trick is to wrap your reusable part in a method call, and then before applying the query, unwrap it.

首先,我会将获取可重用部分的方法更改为返回表达式的静态方法(如 mr100 建议的那样):

First I would change the method that gets the reusable part to be a static method returning your expression (as mr100 suggested):

 public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote()
 {
     return (q,qpi) => q.User.Id == qpi.ItemOrder;
 }

包装将使用:

  public static TFunc AsQuote<TFunc>(this Expression<TFunc> exp)
  {
      throw new InvalidOperationException("This method is not intended to be invoked, just as a marker in Expression trees!");
  }

然后解包会发生在:

  public static Expression<TFunc> ResolveQuotes<TFunc>(this Expression<TFunc> exp)
  {
      var visitor = new ResolveQuoteVisitor();
      return (Expression<TFunc>)visitor.Visit(exp);
  }

显然,最有趣的部分发生在访问者身上.您需要做的是找到调用 AsQuote 方法的节点,然后用 lambda 表达式的主体替换整个节点.lambda 将是该方法的第一个参数.

Obviously the most interesting part happens in the visitor. What you need to do, is find nodes that are method calls to your AsQuote method, and then replace the whole node with the body of your lambdaexpression. The lambda will be the first parameter of the method.

您的 resolveQuote 访问者看起来像:

Your resolveQuote visitor would look like:

    private class ResolveQuoteVisitor : ExpressionVisitor
    {
        public ResolveQuoteVisitor()
        {
            m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition();
        }
        MethodInfo m_asQuoteMethod;
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (IsAsquoteMethodCall(node))
            {
                // we cant handle here parameters, so just ignore them for now
                return Visit(ExtractQuotedExpression(node).Body);
            }
            return base.VisitMethodCall(node);
        }

        private bool IsAsquoteMethodCall(MethodCallExpression node)
        {
            return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == m_asQuoteMethod;
        }

        private LambdaExpression ExtractQuotedExpression(MethodCallExpression node)
        {
            var quoteExpr = node.Arguments[0];
            // you know this is a method call to a static method without parameters
            // you can do the easiest: compile it, and then call:
            // alternatively you could call the method with reflection
            // or even cache the value to the method in a static dictionary, and take the expression from there (the fastest)
            // the choice is up to you. as an example, i show you here the most generic solution (the first)
            return (LambdaExpression)Expression.Lambda(quoteExpr).Compile().DynamicInvoke();
        }
    }

现在我们已经完成了一半.如果你的 lambda 没有任何参数,以上就足够了.在您的情况下,您希望将 lambda 的参数实际替换为原始表达式中的参数.为此,我使用 invoke 表达式,在其中获取我想要在 lambda 中使用的参数.

Now we are already half way through. The above is enough, if you dont have any parameters on your lambda. In your case you do, so you want to actually replace the parameters of your lambda to the ones from the original expression. For this, I use the invoke expression, where I get the parameters I want to have in the lambda.

首先让我们创建一个访问者,它将用您指定的表达式替换所有参数.

First lets create a visitor, that will replace all parameters with the expressions that you specify.

    private class MultiParamReplaceVisitor : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, Expression> m_replacements;
        private readonly LambdaExpression m_expressionToVisit;
        public MultiParamReplaceVisitor(Expression[] parameterValues, LambdaExpression expressionToVisit)
        {
            // do null check
            if (parameterValues.Length != expressionToVisit.Parameters.Count)
                throw new ArgumentException(string.Format("The paraneter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count));
            m_replacements = expressionToVisit.Parameters
                .Select((p, idx) => new { Idx = idx, Parameter = p })
                .ToDictionary(x => x.Parameter, x => parameterValues[x.Idx]);
            m_expressionToVisit = expressionToVisit;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            Expression replacement;
            if (m_replacements.TryGetValue(node, out replacement))
                return Visit(replacement);
            return base.VisitParameter(node);
        }

        public Expression Replace()
        {
            return Visit(m_expressionToVisit.Body);
        }
    }

现在我们可以返回到我们的 ResolveQuoteVisitor,并正确处理调用:

Now we can advance back to our ResolveQuoteVisitor, and hanlde invocations correctly:

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            if (node.Expression.NodeType == ExpressionType.Call && IsAsquoteMethodCall((MethodCallExpression)node.Expression))
            {
                var targetLambda = ExtractQuotedExpression((MethodCallExpression)node.Expression);
                var replaceParamsVisitor = new MultiParamReplaceVisitor(node.Arguments.ToArray(), targetLambda);
                return Visit(replaceParamsVisitor.Replace());
            }
            return base.VisitInvocation(node);
        }

这应该可以解决所有问题.您可以将其用作:

This should do all the trick. You would use it as:

  public IEnumerable<FilteredViewModel> GetFilteredQuotes()
  {
      Expression<Func<Quote, FilteredViewModel>> selector = q => new FilteredViewModel
      {
          Quote = q,
          QuoteProductImages = q.QuoteProducts.SelectMany(qp => qp.QuoteProductImages.Where(qpi => ExpressionHelper.FilterQuoteProductImagesByQuote().AsQuote()(q, qpi)))
      };
      selector = selector.ResolveQuotes();
      return _context.Context.Quotes.Select(selector);
  }

当然,我认为您可以在这里提高可重用性,甚至在更高级别定义表达式.

Of course I think you can make here much more reusability, with defining expressions even on a higher levels.

您甚至可以更进一步,在 IQueryable 上定义一个 ResolveQuotes,然后访问 IQueryable.Expression 并使用原始提供程序和结果表达式创建一个新的 IQUEryable,例如:

You could even go one step further, and define a ResolveQuotes on the IQueryable, and just visit the IQueryable.Expression and creating a new IQUeryable using the original provider and the result expression, e.g:

    public static IQueryable<T> ResolveQuotes<T>(this IQueryable<T> query)
    {
        var visitor = new ResolveQuoteVisitor();
        return query.Provider.CreateQuery<T>(visitor.Visit(query.Expression));
    }

通过这种方式,您可以内联表达式树的创建.您甚至可以尽可能地覆盖 ef 的默认查询提供程序,并为每个执行的查询解析引号,但这可能太过分了 :P

This way you can inline the expression tree creation. You could even go as far, as override the default query provider for ef, and resolve quotes for every executed query, but that might go too far :P

您还可以看到这将如何转化为任何类似的可重用表达式树.

You can also see how this would translate to actually any similar reusable expression trees.

我希望这会有所帮助:)

I hope this helps :)

免责声明:请记住,在不了解其作用的情况下,切勿将粘贴代码从任何地方复制到生产环境中.我没有在这里包含太多的错误处理,以保持代码最少.我也没有检查使用你的类的部分是否可以编译.我也不对这段代码的正确性承担任何责任,但我认为解释应该足够了,以了解发生了什么,并在有任何问题时修复它.还要记住,这仅适用于产生表达式的方法调用的情况.我很快会根据这个答案写一篇博文,让你也可以使用更多的灵活性:P

Disclaimer: Remember never copy paste code from anywhere to production without understanding what it does. I didn't include much error handling here, to keep the code to minimum. I also didn't check the parts that use your classes if they would compile. I also don't take any responsability for the correctness of this code, but i think the explanation should be enough, to understand what is happening, and fix it if there are any issues with it. Also remember, that this only works for cases, when you have a method call that produces the expression. I will soon write a blog post based on this answer, that allows you to use more flexibility there too :P

这篇关于将表达式参数作为参数传递给另一个表达式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆