我有一個篩選結果的查詢:
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
}
我該如何實現這個功能?或者我應該使用不同的方法嗎?
如果我理解正確,你想在另一個中重用一個表達式樹,並且仍然允許編譯器為你構建表達式樹。
這實際上是可能的,我已經在很多場合做過了。
訣竅是將可重用部分包裝在方法調用中,然後在應用查詢之前將其解包。
首先,我將更改獲取可重用部分的方法作為返回表達式的靜態方法(如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,並正確地進行調用:
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
以您的方式實現這將導致ef linq-to-sql解析器拋出異常。在你的linq查詢中,你調用FilterQuoteProductImagesByQuote函數 - 這被解釋為Invoke表達式,它根本無法解析為sql。為什麼?通常因為從SQL中調用MSIL方法是不可能的。將表達式傳遞給查詢的唯一方法是將其作為Expression> object存儲在查詢之外,然後將其傳遞給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類來定義該查詢。