通过前pression参数作为参数传递给另一位前pression [英] Pass expression parameter as argument to another expression

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

问题描述

我有过滤结果的查询:

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子句中我使用的参数问:,以对抗的两个属性从参数 QPI 匹配。
由于过滤器将在多个地方使用我想where子句改写为一名前pression树,看起来就像是这样的:

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?

推荐答案

如果我理解正确的话,你想重用里面一个又一个的前pression树,仍然允许编译器进行构建的所有魔法前pression树适合你。

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.

首先,我会改变,获取可再用部分是一个静态方法返回你的前pression(如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;
 }

包装将与来完成:

Wrapping would be done with:

  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!");
  }

然后在解包会发生:

Then unwrapping would happen in:

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

显然最有趣的部分发生在访问者。
你需要做的,就是找到那些方法调用你的AsQuote方法节点,然后用你的lambdaex pression的身体更换整个节点。拉姆达将本方法的第一个参数。

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任何参数。在你的情况,你做什么,所以你要真正替代你的拉姆达的参数,从原来的前pression的人。对于这一点,我使用invoke前pression,在那里我得到我想要的拉姆达的参数。

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.

首先让我们创建一个游客,将取代所有参数与前pressions您指定。

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和hanlde调用正确的:

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);
  }

当然,我觉得你可以在这里做更多的可重用性,具有即使在更高级别的定义前pressions。

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

您甚至可以走一步,并在IQueryable的定义ResolveQuotes,只是参观IQueryable.Ex pression和使用原始供应商和结果前pression,例如,创建一个新的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));
    }

这种方式可以内联前pression树创建。你甚至可以去远,以覆盖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

您还可以看到如何将其转化为实际的可重复使用相似的前pression树。

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

我希望这有助于:)

免责声明:记得从未从任何地方生产的复制粘贴code不理解它做什么。我不包括太多的错误处理,以保持code到最低限度。我也没有检查使用类,如果他们将编制的部分。我也没有对本code的正确性承担任何responsability,但我想解释应该是不够的,要明白发生了什么,并修复它是否有与它的任何问题。
还记得,这仅适用于情况下,当你有一个产生除权pression一个方法调用。我会尽快写基于这个答案一篇博客文章,这可以让你使用起来更加灵活有太多: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

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

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