2015-04-04 8 views
7

Ich habe eine Abfrage, die Ergebnisse Filter:Pass Ausdruck Parameter als Argument an einem anderen Ausdruck

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

In der where-Klausel Ich bin mit dem Parameter q aus dem Parameter eine Eigenschaft gegen eine Eigenschaft Übereinstimmen qpi. Da die Filter werden an mehreren Stellen verwendet werden, ich versuche, die where-Klausel auf einen Ausdruck Baum neu zu schreiben, die wie etwas würden wie folgt aussehen:

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

die Parameter q als Parameter verwendet wird In dieser Abfrage die Funktion:

public static Expression<Func<QuoteProductImage, bool>> FilterQuoteProductImagesByQuote(Quote quote) 
{ 
    // Match the QuoteProductImage's ItemOrder to the Quote's Id 
} 

Wie würde ich diese Funktion implementieren? Oder sollte ich insgesamt einen anderen Ansatz wählen?

Antwort

8

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

+0

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. –

2

Die Implementierung dieser Methode führt zu einer Ausnahme, die vom ef linq-to-sql-Parser ausgelöst wird. Innerhalb Ihrer Linq-Abfrage rufen Sie die FilterQuoteProductImagesByQuote-Funktion auf - dies wird als Invoke-Ausdruck interpretiert und kann einfach nicht nach sql geparst werden. Warum? Allgemein, weil es aus SQL keine Möglichkeit gibt, die MSIL-Methode aufzurufen. Die einzige Möglichkeit, den Ausdruck an die Abfrage zu übergeben, besteht darin, ihn als Ausdruck> -Objekt außerhalb der Abfrage zu speichern und ihn dann an die Where-Methode zu übergeben. Sie können dies nicht tun, da Sie außerhalb der Abfrage kein Zitatobjekt haben. Dies bedeutet, dass Sie im Allgemeinen nicht erreichen können, was Sie wollten. Was man vielleicht erreichen, ist irgendwo ganzen Ausdruck von Select wie folgt zu halten:

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

Und dann können Sie es als Argument wählen passieren:

_context.Context.Quotes.Select(selectExp); 

somit wiederverwendbar zu machen. Wenn Sie wiederverwendbare Abfrage haben mögen:

qpi => q.User.Id == qpi.ItemOrder 

Dann erst würden Sie andere Methode zum Halten sie schaffen müssen:

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

Anwendung der es zu einem Haupt-Abfrage möglich wäre, jedoch ziemlich schwierig und schwer zu lesen, da diese Abfrage mit der Expression-Klasse definiert werden muss.

+0

Danke für Ihre klare Antwort! Ich habe versucht, den Ausdrucksbaum manuell aufzubauen, aber ich stoße auf das Problem, t Zugriff auf den Parameter ** q ** und Neudefinition ist nicht erlaubt. Ich könnte die gesamte Abfrage (nicht nur die where-Klausel) selbst erstellen, aber das ist den Aufwand nicht wert, da die tatsächliche Abfrage, die ich erstellen müsste, ziemlich groß und komplex ist. Ich werde stattdessen die Wiederverwendbarkeit einfach ablegen und die gleiche Abfrage mehrmals schreiben. –

+0

Ich habe auch versucht, diese Abfrage manuell zu erstellen, und ich fast fertig, aber es sah sehr komplex aus und wäre daher schwer zu pflegen, so kam ich zu dem Schluss, dass Sie nicht interessiert sein werden, es zu sehen, da es keine echte gibt Vorteil. Leider bin ich bei der Arbeit mit ef sehr oft zu dem Schluss gekommen, dass wir uns in bestimmten Situationen auf die Code-Duplizierung einigen müssen. – mr100