Besonderheiten von QueryProvider
QueryProvider kann damit nicht umgehen:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ToList();
Es kann keinen Satz mit einer interpolierten Zeichenfolge behandeln, aber es wird leicht damit umgehen:
var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList();
Am schmerzhaftesten ist es, Fehler nach dem Aktivieren von ClientEvaluation zu beheben (Ausnahme für die clientseitige Berechnung), da alle Automapper- Profile streng auf Interpolation analysiert werden sollten. Lassen Sie uns herausfinden, was was ist und unsere Lösung für das Problem vorschlagen.
Dinge reparieren
Die Interpolation im Ausdrucksbaum wird wie folgt konvertiert (dies ist ein Ergebnis der ExpressionStringBuilder.ExpressionToString- Methode, bei der einige Knoten übersprungen wurden, dies ist jedoch in Ordnung):
Oder so, wenn es mehr als 3 Argumente gibt:
Format("Name:{0} Age:{1}", new [] {x.Name, Convert(x.Age, Object)))
Wir können daraus schließen, dass dem Anbieter einfach nicht beigebracht wurde, diese Fälle zu verarbeiten, aber es könnte gelehrt werden, diese Fälle mit dem bekannten ToString () zu bringen , der wie folgt verarbeitet wird:
((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object)))
Ich möchte einen Besucher schreiben, der dem Ausdrucksbaum folgt (insbesondere den MethodCallExpression- Knoten) und die Format- Methode durch Verkettung ersetzen. Wenn Sie mit Ausdrucksbäumen vertraut sind, wissen Sie, dass C # einen eigenen Besucher zur Umgehung des Baums bereitstellt - ExpressionVisitor . Weitere Infos für Interessierte .
Wir müssen lediglich die VisitMethodCall- Methode überschreiben und den zurückgegebenen Wert geringfügig ändern. Der Methodenparameter ist vom Typ MethodCallExpression und enthält Informationen zur Methode selbst und den ihr zugeführten Argumenten.
Teilen wir die Aufgabe in mehrere Teile auf:
- Stellen Sie fest, dass es sich um die Format- Methode handelt, die in VisitMethodCall enthalten ist.
- Ersetzen Sie die Methode durch die Verkettung von Zeichenfolgen.
- Behandeln Sie alle Überladungen der Format- Methode, die wir haben können.
- Schreiben Sie die Erweiterungsmethode, um unseren Besucher anzurufen.
Der erste Teil ist einfach: Die Format- Methode verfügt über 4 Überladungen, die in einem Ausdrucksbaum erstellt wurden:
public static string Format(string format, object arg0) public static string Format(string format, object arg0,object arg1) public static string Format(string format, object arg0,object arg1,object arg2) public static string Format(string format, params object[] args)
Lassen Sie uns sie mithilfe ihrer MethodInfo- Reflexion extrahieren:
private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format"))
Hervorragend. Jetzt können wir feststellen, ob die Format- Methode in MethodCallExpression " eingegangen" ist .
Beim Umgehen des Baums in VisitMethodCall können die folgenden Methoden verwendet werden:
- Formatieren mit Objektargumenten
- Formatieren Sie mit dem Argument object []
- Etwas ganz anderes.
Ein bisschen benutzerdefiniertes Pattern Matching
Da wir nur drei Bedingungen haben, können wir sie mit if behandeln. Da wir jedoch davon ausgehen, dass wir diese Methode in Zukunft erweitern müssen, sollten wir alle Fälle in diese Datenstruktur auslagern:
public class PatternMachingStructure { public Func<MethodInfo, bool> FilterPredicate { get; set; } public Func<MethodCallExpression, IEnumerable<Expression>> SelectorArgumentsFunc { get; set; } public Func<MethodCallExpression, IEnumerable<Expression>, Expression> ReturnFunc { get; set; } } var patternMatchingList = new List<PatternMachingStructure>()
Mit FilterPredicate bestimmen wir, um welchen der drei Fälle es sich handelt. SelectorArgumentFunc wird benötigt, um alle Argumente der Format-Methode in eine einheitliche Form zu bringen, die ReturnFunc- Methode, die den vollständigen Ausdruck zurückgibt .
Ersetzen wir nun die Interpolation durch die Verkettung, und dafür benötigen wir diese Methode:
private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) {
InterpolationToStringConcat wird vom Besucher aufgerufen und hinter ReturnFunc versteckt:
protected override Expression VisitMethodCall(MethodCallExpression node) { var pattern = patternMatchingList.First(x => x.FilterPredicate(node.Method)); var arguments = pattern.SelectorArgumentsFunc(node); var expression = pattern.ReturnFunc(node, arguments); return expression; }
Jetzt müssen wir Logik schreiben, um alle Überladungen der Format- Methode zu behandeln. Es ist ziemlich trivial und befindet sich in der patternMatchingList :
patternMatchingList = new List<PatternMachingStructure> {
Dementsprechend folgen wir dieser Liste in der VisitMethodCall- Methode bis zum ersten positiven FilterPredicate, konvertieren dann die Argumente ( SelectorArgumentFunc ) und führen ReturnFunc aus .
Schreiben wir eine Erweiterungsmethode, die wir aufrufen können, um die Interpolation zu ersetzen.
Wir können einen Ausdruck abrufen, ihn an den Besucher übergeben und dann die CreateQuery- Methode aufrufen , die den ursprünglichen Ausdrucksbaum durch unseren ersetzt:
public static IQueryable<T> ReWrite<T>(this IQueryable<T> qu) { var result = new InterpolationStringReplacer<T>().Visit(qu.Expression); var s = (IQueryable<T>) qu.Provider.CreateQuery(result); return s; }
Achten Sie auf die Umwandlung von qu.Provider.CreateQuery (Ergebnis) mit der IQueryable- Methode in IQueryable <T>. Es wird häufig für C # verwendet (siehe IEnumerable <T> -Schnittstelle!), Und es entstand aus der Notwendigkeit, alle generischen Schnittstellen mit einer Klasse zu behandeln, die IQueryable / IEnumerable erhalten möchte , und es mit allgemeinen Schnittstellenmethoden zu behandeln.
Wir hätten das vermeiden können, indem wir T auf eine Basisklasse gebracht hätten (durch Kovarianz), aber es setzt den Schnittstellenmethoden einige Grenzen.
Ergebnis
Wenden Sie ReWrite auf den linq-Ausdruck oben im Artikel an:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList();
Github