2015-04-04 49 views
7

我有过滤结果的查询:通行证表达参数作为参数传递给另一种表达

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 
} 

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

回答

8

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

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

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

首先,我会改变这种状况得到可重复使用的部分是一个静态方法返回你的表达方法(如MR100建议):

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<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote() 
{ 
    return (q,qpi) => q.User.Id == qpi.ItemOrder; 
} 

包装将与实现展开会发生在:

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的参数替换为原始表达式的参数。为此,我使用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!

+0

好吧,我印象深刻,它完美运作。这绝对是非常有用的,我会尽量让它更通用,以便在更多场合使用它。 – 2015-04-07 07:44:35

2

按照这种方式实现此操作将导致ef linq-to-sql分析器抛出的异常。在你的linq查询中,你调用FilterQuoteProductImagesByQuote函数 - 这被解释为Invoke表达式,并且它不能被解析为sql。为什么?一般来说,因为从SQL中不可能调用MSIL方法。将表达式传递给查询的唯一方法是将其作为查询外的Expression>对象存储,然后将其传递给Where方法。你不能这样做,因为在查询之外你不会有引用对象。这意味着你通常不能达到你想要的。你可能能做到的,是从选择挂在什么地方整个表达式是这样的:

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类定义该查询。

+0

谢谢你明确的答案我已经手动试图建立表达式树,但我碰上,我穿上”问题无法访问** q **参数并且不允许重新定义它。我可以自己构建整个查询(不仅仅是where子句),但是这不值得付出努力,因为我必须构建的实际查询相当大且复杂。我会抛弃可重用性,并多次编写相同的查询。 – 2015-04-05 20:18:19

+0

我也尝试手动构建该查询,并且我几乎完成了它,但它看起来非常复杂,因此很难维护,所以我得出结论,您不会有兴趣看到它,因为它没有真正实现效益。在不幸地工作时,我经常得出结论,在某些情况下,我们必须就代码重复达成一致。 – mr100 2015-04-06 06:41:02

相关问题