Wie erstelle ich einen Ausdrucksbaum, der IEnumerable aufruft <TSource> .Irgendein(...)?

.net c# expression-trees linq

Frage

Ich versuche, einen Ausdrucksbaum zu erstellen, der Folgendes darstellt:

myObject.childObjectCollection.Any(i => i.Name == "name");

Zur Verdeutlichung gekürzt habe ich folgendes:

myObject.childObjectCollection.Any(i => i.Name == "name");

Was mache ich falsch? Hat jemand irgendwelche Vorschläge?

Akzeptierte Antwort

Es gibt verschiedene Dinge, die falsch sind mit dem, wie du vorgehst.

  1. Du mischt Abstraktionslevel. Der T-Parameter für GetAnyExpression<T> könnte sich von dem Typparameter unterscheiden, der zum Instantiieren von propertyExp.Type . Der T-Typ-Parameter ist einen Schritt näher im Abstraktionsstapel, um die Zeit zu kompilieren - es sei denn, Sie rufen GetAnyExpression<T> über Reflektion auf, wird zur Kompilierungszeit bestimmt - aber der in den als propertyExp Ausdruck eingebettete Typ wird zur Laufzeit festgelegt . Ihre Weitergabe des Prädikats als Expression ist auch ein Abstraktionsmix - was der nächste Punkt ist.

  2. Das Prädikat, das Sie an GetAnyExpression sollte ein Delegate-Wert sein, kein Expression irgendeiner Art, da Sie versuchen, Enumerable.Any<T> . Wenn Sie versuchen, eine Ausdrucksbaumversion von Any LambdaExpression , sollten Sie stattdessen eine LambdaExpression , die Sie LambdaExpression würden. LambdaExpression ist einer der seltenen Fälle, in denen Sie möglicherweise einen spezifischeren Typ als Expression übergeben können. was mich zu meinem nächsten Punkt führt.

  3. Im Allgemeinen sollten Sie Expression Werte weitergeben. Wenn Sie generell mit Ausdrucksbäumen arbeiten - und dies gilt für alle Arten von Compilern, nicht nur für LINQ und seine Freunde -, sollten Sie dies auf eine Art und Weise tun, die unabhängig von der unmittelbaren Zusammensetzung des Knotenbaums ist, mit dem Sie arbeiten. Sie sind der Annahme , dass Sie anrufen Any auf einem MemberExpression , aber Sie nicht wirklich wissen müssen , dass Sie mit einem zu tun haben MemberExpression , nur ein Expression des Typs einige Instanziierung IEnumerable<> . Dies ist ein häufiger Fehler für Leute, die nicht mit den Grundlagen von Compiler-ASTs vertraut sind. Frans Bouma hat den gleichen Fehler wiederholt, als er anfing, mit Expressionsbäumen zu arbeiten - in speziellen Fällen denkend. Denke generell. Sie sparen sich mittel- und längerfristig viel Ärger.

  4. Und hier kommt das Fleisch Ihres Problems (obwohl die zweite und wahrscheinlich erste Probleme Sie gebissen hätten, wenn Sie darüber hinweggekommen wären) - Sie müssen die geeignete generische Überladung der Any-Methode finden und sie dann mit dem richtigen Typ instantiieren. Reflektion bietet dir hier kein leichtes Spiel; Sie müssen iterieren und eine geeignete Version finden.

Also, brechen Sie es auf: Sie müssen eine generische Methode ( Any ) finden. Hier ist eine Dienstprogrammfunktion, die das tut:

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

Es erfordert jedoch die Typargumente und die richtigen Argumenttypen. Das von Ihrem propertyExp Expression ist nicht ganz trivial, weil der Expression vom Typ List<T> oder einem anderen Typ sein kann, aber wir müssen die IEnumerable<T> finden und ihr type-Argument erhalten. Ich habe das in ein paar Funktionen gekapselt:

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

Bei jedem Type können wir nun die Instanziierung IEnumerable<T> daraus ziehen - und bestätigen, dass es keine (genau) gibt.

Mit dieser Arbeit ist die Lösung des Problems nicht allzu schwierig. Ich habe Ihre Methode in CallAny umbenannt und die Parametertypen wie vorgeschlagen geändert:

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

Hier ist eine Main() Routine, die den obigen Code verwendet und überprüft, ob sie für einen trivialen Fall funktioniert:

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

Beliebte Antwort

Barrys Antwort bietet eine funktionierende Lösung für die Frage des ursprünglichen Posters. Danke an diese beiden Personen für das Fragen und Antworten.

Ich habe diesen Thread gefunden, als ich versuchte, eine Lösung für ein ziemlich ähnliches Problem zu finden: programmatisch einen Ausdrucksbaum zu erstellen, der einen Aufruf der Any () -Methode enthält. Als zusätzliche Einschränkung war das ultimative Ziel meiner Lösung jedoch, einen solchen dynamisch erstellten Ausdruck über Linq-to-SQL zu übergeben, sodass die Arbeit der Any () - Evaluierung tatsächlich in der Datenbank selbst ausgeführt wird.

Leider ist die Lösung, wie sie bisher besprochen wurde, nicht etwas, mit dem Linq-to-SQL umgehen kann.

Ich ging davon aus, dass dies ein recht beliebter Grund dafür sein könnte, einen dynamischen Ausdrucksbaum zu erstellen, und beschloss daher, den Thread mit meinen Ergebnissen zu ergänzen.

Als ich versuchte, das Ergebnis von CallAny () von Barry als Ausdruck in einer Linq-to-SQL-Where () - Klausel zu verwenden, erhielt ich eine InvalidOperationException mit den folgenden Eigenschaften:

  • HResult = -2146233079
  • Message = "Interner .NET Framework-Datenanbieterfehler 1025"
  • Quelle = System.Data.Entity

Nach dem Vergleich eines hartcodierten Ausdrucksbaums mit dem dynamisch erstellten, der CallAny () verwendet, stellte ich fest, dass das Kernproblem auf Compile () des Prädikatausdrucks und den Versuch zurückzuführen war, den resultierenden Delegaten in CallAny () aufzurufen. Ohne tief in die Details der Linq-zu-SQL-Implementierung einzutauchen, erschien es mir vernünftig, dass Linq-to-SQL nicht wissen würde, was mit einer solchen Struktur zu tun ist.

Daher war ich nach einigen Experimenten in der Lage, mein angestrebtes Ziel zu erreichen, indem ich die vorgeschlagene CallAny () - Implementierung etwas überarbeitete, um einen Prädikatausdruck anstelle eines Delegaten für die Any () - Prädikatlogik zu verwenden.

Meine überarbeitete Methode ist:

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

Jetzt werde ich seine Verwendung mit EF demonstrieren. Aus Gründen der Übersichtlichkeit sollte ich zuerst das Spielzeugdomänenmodell und den EF-Kontext zeigen, den ich verwende. Grundsätzlich ist mein Modell eine vereinfachte Blogs- & Posts-Domain ... wo ein Blog mehrere Beiträge hat und jeder Post ein Datum hat:

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

Wenn diese Domäne eingerichtet ist, ist hier mein Code, um schließlich die überarbeitete CallAny () auszuüben und Linq-to-SQL dazu zu bringen, die Any () zu evaluieren. Mein spezielles Beispiel konzentriert sich auf die Rückgabe aller Blogs, die mindestens einen Post haben, der neuer als ein angegebener Stichtag ist.

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

Wo BuildExpressionForBlogsWithRecentPosts () ist eine Hilfsfunktion, die CallAny () wie folgt verwendet:

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

HINWEIS: Ich habe ein weiteres scheinbar unwichtiges Delta zwischen den hartcodierten und den dynamisch erstellten Ausdrücken gefunden. Der dynamisch gebaute hat einen "extra" Convert-Aufruf darin, den die hartcodierte Version nicht zu haben scheint (oder braucht?). Die Konvertierung wird in der CallAny () - Implementierung eingeführt. Linq-to-SQL scheint damit in Ordnung zu sein, also habe ich es an Ort und Stelle gelassen (obwohl es unnötig war). Ich war mir nicht ganz sicher, ob diese Umwandlung in einigen robusteren Anwendungen als meine Spielzeugprobe erforderlich sein könnte.




Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum