一尘不染

将表达式参数作为参数传递给另一个表达式

c#

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

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子句重写为表达式树,如下所示:

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用作函数的参数:

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

我将如何实现此功能?还是我应该一起使用其他方法?


阅读 589

收藏
2020-05-19

共1个答案

一尘不染

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

实际上这是可能的,而且我已经在很多场合做到了。

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

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

 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方法进行方法调用的节点,然后将整个节点替换为lambdaexpression的主体。lambda将是方法的第一个参数。

您的resolveQuote访问者将如下所示:

    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中想要的参数。

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

    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:

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

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

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

当然,我认为您可以通过在更高层次上定义表达式来提高可重用性。

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

    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

您还可以看到这将如何转换为实际上任何类似的可重用表达式树。

我希望这有帮助 :)

免责声明:切记,切勿在不了解粘贴功能的情况下将粘贴代码从任何地方复制到生产环境。为了使代码最少,我在这里没有太多错误处理。我也没有检查使用您的类的部分是否可以编译。我也不对代码的正确性承担任何责任,但是我认为解释应该足够,可以了解正在发生的事情,并在出现任何问题时进行修复。还请记住,这仅在您具有生成表达式的方法调用时适用。我很快将根据此答案写一篇博客文章,让您也可以在其中使用更多的灵活性:P

2020-05-19