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

Posted

技术标签:

【中文标题】将表达式参数作为参数传递给另一个表达式【英文标题】:Pass expression parameter as argument to another expression 【发布时间】:2015-06-09 12:03:31 【问题描述】:

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

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

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

【问题讨论】:

【参考方案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 方法的方法调用的节点,然后将整个节点替换为 lambda 表达式的主体。 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 的参数实际替换为原始表达式中的参数。为此,我使用了调用表达式,在这里我得到了我想要在 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,并正确处理调用:

        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 并使用原始提供程序和结果表达式创建一个新的 IQUEeryable,例如:

    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

【讨论】:

好吧,我印象深刻,它运行良好。这绝对非常有用,我会尝试使其更通用,以便在更多场合使用它。 我在 ResolveQuoteVisitor 上得到一个空引用异常,行 m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition();有什么想法吗? 这篇文章太老了,你应该试试 LinqKit,它可以开箱即用地完成上述所有操作【参考方案2】:

以您的方式实现此功能将导致 ef linq-to-sql 解析器抛出异常。在您的 linq 查询中,您调用 FilterQuoteProductImagesByQuote 函数 - 这被解释为 Invoke 表达式,它根本无法解析为 sql。为什么?通常是因为从 SQL 中调用 MSIL 方法是不可能的。将表达式传递给查询的唯一方法是将其存储为查询之外的 Expression> 对象,然后将其传递给 Where 方法。您不能这样做,因为在查询之外您将没有 Quote 对象。这意味着通常你无法实现你想要的。您可能可以实现的是将 Select 中的整个表达式保存在某个地方,如下所示:

Expression<Func<Quote,FilteredViewModel>> selectExp =
    q => new FilteredViewModel
    
        Quote = q,
        QuoteProductImages = q.QuoteProducts.SelectMany(qp =>  qp.QuoteProductImages.AsQueryable().Where(qpi => q.User.Id == qpi.ItemOrder)))
    ;

然后你可以将它作为参数传递给选择:

_context.Context.Quotes.Select(selectExp);

从而使其可重复使用。如果您想要可重用的查询:

qpi => q.User.Id == qpi.ItemOrder

那么首先你必须创建不同的方法来持有它:

public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote()

    return (q,qpi) => q.User.Id == qpi.ItemOrder;

将它应用到您的主查询是可能的,但是非常困难且难以阅读,因为它需要使用 Expression 类来定义该查询。

【讨论】:

感谢您的明确回答!我已尝试手动构建表达式树,但遇到了无法访问 q 参数并重新定义它的问题不允许。我可以自己构建整个查询(不仅仅是 where 子句),但这不值得付出努力,因为我必须构建的实际查询相当大且复杂。我将放弃可重用性并多次编写相同的查询。 我也尝试手动构建该查询,我几乎完成了它,但它看起来非常复杂,因此很难维护,所以我得出的结论是你不会感兴趣看到它,因为它没有真正的好处。不幸的是,在与 ef 合作期间,我经常得出一个结论,即在某些情况下,我们必须就代码重复达成一致。

以上是关于将表达式参数作为参数传递给另一个表达式的主要内容,如果未能解决你的问题,请参考以下文章

将方法作为参数传递给另一个方法[重复]

如何将带有 args 的成员函数作为参数传递给另一个成员函数?

对象作为参数传递给另一个类,通过值还是引用?

C#将方法作为参数传递给另一个方法[重复]

将一个函数的所有参数传递给另一个函数

如何将向量作为参数传递给另一个向量?