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

8

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

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
}

我该如何实现这个函数?或者我应该采用不同的方法?
2个回答

12

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

实际上,这是可行的,我在许多场合都做过这样的事情。

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

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

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

Wrapping 会使用以下方式进行:

  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中具有的参数。

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

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

当然,我认为你可以更多地提高代码的可重用性,甚至在更高的层级上定义表达式。

你可以迈出一步,定义一个 ResolveQuotes 在 IQueryable 上,并仅访问 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


好的,我印象深刻,它完美地运行了。这绝对非常有用,我会尝试使其更通用,这样我就可以在更多场合使用它。 - Jeroen van Tongeren
我在ResolveQuoteVisitor上遇到了空引用异常,代码行为m_asQuoteMethod = typeof(Extensions).GetMethod("AsQuote").GetGenericMethodDefinition(); 你有什么想法吗? - Danny Lager
3
这篇文章很老了,你应该尝试使用 LinqKit。它可以直接实现上述所有功能。 - MBoros

3
按照您的方式实现会导致 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)))
    };

然后你可以将它作为参数传递给select函数:
_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;
}

应用it技术到你的主查询中是可能的,但是这很困难而且很难读,因为它需要使用Expression类来定义该查询。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接