将expression式parameter passing给另一个expression式的参数

我有一个查询过滤结果:

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中的属性。 因为filter将在几个地方使用,我试图重写where子句的expression式树,看起来像这样:

 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 } 

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

如果我理解正确的话,你想在另一个expression式树中重用一个expression式树,并且仍然允许编译器为你构buildexpression式树的所有魔法。

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

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

首先,我将改变获取可重用部分的方法,使其成为返回expression式的静态方法(如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); } 

显然,最有趣的部分发生在访客。 你需要做的是find节点调用你的AsQuote方法,然后用你的lambdaexpression式的主体replace整个节点。 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的参数replace为原始expression式的参数。 为此,我使用invokeexpression式,在这里我得到了lambda中想要的参数。

首先让我们创build一个访问者,用你指定的expression式replace所有的参数。

  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,并正确地调用invocations:

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

当然,我认为你可以在这里做更多的重用,甚至在更高的层次上定义expression式。

你甚至可以更进一步,在IQueryable上定义一个ResolveQuotes,然后访问IQueryable.Expression并使用原始提供者和结果expression式创build一个新的IQUeryable,例如:

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

这样你可以内联expression式树的创build。 你甚至可以尽可能地去覆盖ef的默认查询提供程序,并为每个已执行的查询parsing引号,但这可能会太过分:P

你也可以看到这将如何转化为实际上任何类似的可重用的expression式树。

我希望这有帮助 :)

免责声明:请记住,不要将粘贴代码从任何地方复制到制作中,而不必理解它的作用 我没有在这里包含太多的error handling,以使代码保持最小。 我也没有检查使用你的类的部分,如果他们将编译。 我也不对这个代码的正确性负任何责任,但是我认为这个解释应该足够了,理解正在发生的事情,并且如果有任何问题,就修正它。 还要记住,这只适用于情况,当你有一个方法调用产生的expression式。 我很快就会根据这个答案写一篇博客文章,让你也可以在这里使用更多的灵活性:P

以这种方式实现这个方法会导致ef linq-to-sql分析器抛出exception。 在你的linq查询中,你调用FilterQuoteProductImagesByQuote函数 – 这被解释为Invokeexpression式,它不能被parsing为sql。 为什么? 一般来说,因为从SQL中不可能调用MSIL方法。 将expression式传递给查询的唯一方法是将其作为查询外的Expression>对象存储,然后将其传递给Where方法。 你不能做这个以外的查询,你不会有Quote对象。 这意味着一般你不能达到你想要的。 你可能可以实现的是从Select中select像这样的整个expression式:

 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 

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

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

将它应用到你的主查询中是可能的,然而这是非常困难和难以阅读的,因为它需要使用Expression类来定义这个查询。