Wenn ich richtig verstehe, möchten Sie einen Ausdrucksbaum in einem anderen wiederverwenden, und trotzdem dem Compiler erlauben, die Magie des Aufbaus des Ausdrucksbaums für Sie zu nutzen.
Dies ist tatsächlich möglich, und ich habe es bei vielen Gelegenheiten getan.
Der Trick besteht darin, Ihren wiederverwendbaren Teil in einen Methodenaufruf zu verpacken und ihn dann vor dem Anwenden der Abfrage auszupacken.
Zuerst würde ich die Methode ändern, die den wiederverwendbaren Teil bekommt Ihren Ausdruck Rückkehr eine statische Methode zu sein (wie MR100 vorgeschlagen):
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!");
}
Dann:
public static Expression<Func<Quote,QuoteProductImage, bool>> FilterQuoteProductImagesByQuote()
{
return (q,qpi) => q.User.Id == qpi.ItemOrder;
}
Wrapping würde geschehen Auspacken würde in passieren:
public static Expression<TFunc> ResolveQuotes<TFunc>(this Expression<TFunc> exp)
{
var visitor = new ResolveQuoteVisitor();
return (Expression<TFunc>)visitor.Visit(exp);
}
Offensichtlich passiert der interessanteste Teil in der Besucher. Was Sie tun müssen, ist, Knoten zu finden, die Methodenaufrufe zu Ihrer AsQuote-Methode sind, und dann den gesamten Knoten durch den Körper Ihres Lambda-Ausdrucks zu ersetzen. Das Lambda wird der erste Parameter der Methode sein.
Ihr resolveQuote Besucher würde wie folgt aussehen:
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();
}
}
Jetzt sind wir durch bereits auf halbem Weg sind. Das obige ist genug, wenn Sie keine Parameter auf Ihrem Lambda haben. In Ihrem Fall tun Sie das, also möchten Sie die Parameter Ihres Lambda tatsächlich durch die des ursprünglichen Ausdrucks ersetzen. Dafür benutze ich den Aufruf-Ausdruck, wo ich die Parameter bekomme, die ich im Lambda haben möchte.
Zuerst erstellen wir einen Besucher, der alle Parameter durch die von Ihnen angegebenen Ausdrücke ersetzt.
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);
}
}
Jetzt können wir unseren ResolveQuoteVisitor voran zurück und hanlde Anrufungen richtig:
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);
}
Das ganze Trick tun sollten. Sie würden es als verwenden:
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);
}
Natürlich glaube ich dir hier viel mehr Wiederverwertbarkeit machen können, mit Ausdrücken auch auf einem höheren Ebenen zu definieren.
Sie noch einen Schritt weiter gehen könnte, und ein ResolveQuotes auf der IQueryable definieren, und nur die IQueryable.Expression besuchen und eine neue IQueryable den ursprünglichen Provider das Erstellen und das Ergebnis Ausdruck, zB:
public static IQueryable<T> ResolveQuotes<T>(this IQueryable<T> query)
{
var visitor = new ResolveQuoteVisitor();
return query.Provider.CreateQuery<T>(visitor.Visit(query.Expression));
}
Auf diese Weise können Sie die Erstellung des Ausdrucksbaums inline durchführen. Sie könnten sogar so weit gehen, den Standardabfrageprovider für ef außer Kraft setzen und die Anführungszeichen für jede ausgeführte Abfrage auflösen, aber das könnte zu weit gehen: P
Sie können auch sehen, wie dies zu einem ähnlichen wiederverwendbaren Ausdruck führen würde Bäume.
Ich hoffe, das hilft :)
Haftungsausschluss: Denken Sie daran, Paste Code nie auf die Produktion von überall kopieren, ohne zu verstehen, was es tut. Ich habe hier nicht viel Fehlerbehandlung eingeschlossen, um den Code auf ein Minimum zu beschränken. Ich habe auch nicht die Teile überprüft, die Ihre Klassen verwenden, wenn sie kompilieren würden. Ich übernehme auch keine Verantwortung für die Korrektheit dieses Codes, aber ich denke, die Erklärung sollte ausreichen, um zu verstehen, was passiert, und es beheben, wenn es irgendwelche Probleme damit gibt. Denken Sie auch daran, dass dies nur für Fälle funktioniert, wenn Sie einen Methodenaufruf haben, der den Ausdruck erzeugt.Ich werde bald einen Blogbeitrag schreiben, der auf dieser Antwort basiert und Ihnen erlaubt, dort auch mehr Flexibilität zu nutzen: P
Ok ich bin beeindruckt, es hat perfekt funktioniert. Dies ist definitiv sehr nützlich und ich werde versuchen, es generischer zu machen, damit ich es öfter nutzen kann. –