Frage

Ich versuche, einen Ausdrucksbaum zu erstellen, die den folgenden darstellt:

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

Verkürzte für Klarheit, ich habe folgende Möglichkeiten:

//'myObject.childObjectCollection' is represented here by 'propertyExp'
//'i => i.Name == "name"' is represented here by 'predicateExp'
//but I am struggling with the Any() method reference - if I make the parent method
//non-generic Expression.Call() fails but, as per below, if i use <T> the 
//MethodInfo object is always null - I can't get a reference to it

private static MethodCallExpression GetAnyExpression<T>(MemberExpression propertyExp, Expression predicateExp)
{
    MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func<IEnumerable<T>, Boolean>)});
    return Expression.Call(propertyExp, method, predicateExp);
}

Was mache ich falsch? Jemand irgendwelche Vorschläge?

War es hilfreich?

Lösung

Es gibt mehrere Dinge falsch mit, wie Sie es fahren.

  1. Sie sind Mischabstraktionsebenen. Der T-Parameter auf GetAnyExpression<T> könnte auf den Typ-Parameter unterschiedlich sein verwendet propertyExp.Type zu instanziiert. Der T-Typ-Parameter ist ein Schritt näher in dem Abstraktion Stapel Zeit zu kompilieren - es sei denn, Sie GetAnyExpression<T> über Reflexion anrufen, wird es bei der Kompilierung bestimmt werden - aber die Art des als propertyExp gab Ausdruck eingebettet wird zur Laufzeit bestimmt. Ihre Weitergabe des Prädikats als Expression ist auch eine Abstraktion mixup -., Die der nächste Punkt ist

  2. Das Prädikat Sie GetAnyExpression geben werden, sollten ein Delegierter Wert, kein Expression jeglicher Art sein, da Sie versuchen, Enumerable.Any<T> zu nennen. Wenn Sie versuchen, einen Ausdruck Baum Version von Any zu nennen, dann sollten Sie stattdessen eine LambdaExpression passieren, die Sie zitieren würde, und ist einer der seltenen Fälle, in denen man nebenbei eine spezifischere Art als Ausdruck gerechtfertigt sein könnte, das führt mich zu meinem nächsten Punkt.

  3. Generell Sie um Expression Werte übergeben sollte. Wenn mit Ausdruck Bäume im Allgemeinen arbeiten - und das gilt für alle Arten von Compilern, nicht nur LINQ und seine Freunde - sollten Sie dies in einer Weise tun, die in Bezug auf die unmittelbare Zusammensetzung des Knotenbaum mit dem Sie arbeiten Agnostiker ist. Sie sind Vermutungs , dass Sie anrufen Any auf einem MemberExpression, aber Sie nicht wirklich wissen müssen, dass Sie mit einem MemberExpression zu tun hat, nur einen Expression vom Typ einige Instanziierung IEnumerable<>. Dies ist ein häufiger Fehler für Menschen nicht vertraut mit den Grundlagen des Compiler ASTs. Frans Bouma hat immer wieder den gleichen Fehler, wenn er zum ersten Mal mit Ausdruck Bäumen zu arbeiten begann - Denken in besonderen Fällen. Denken Sie im Allgemeinen. Sie finden sich eine Menge Ärger im Medium speichern und längere Sicht.

  4. Und hier kommt das Fleisch des Problems (obwohl der zweite und wahrscheinlich erste Probleme hätte wenig Sie, wenn Sie an ihm vorbei gekommen war) - Sie die entsprechende allgemeine Überlastung der Any-Methode finden müssen, und dann instanziiert es mit dem richtigen Typ. Reflexion ist Ihnen nicht mit einem einfach hier draußen; Sie müssen durch eine entsprechende Version iterieren und finden.

So, bricht es nach unten: Sie benötigen eine generische Methode (Any) zu finden. Hier ist eine Utility-Funktion, 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. dass von Ihrem propertyExp Expression bekommen, ist nicht ganz trivial, weil die Expression eines List<T> Typ oder einem anderen Typ sein, aber wir müssen die IEnumerable<T> Instanziierung finden und seine Art Argument zu bekommen. Ich habe, dass in ein paar Funktionen gekapselt:

static bool IsIEnumerable(Type type)
{
    return type.IsGenericType
        && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

static Type GetIEnumerableImpl(Type type)
{
    // Get IEnumerable implementation. Either type is IEnumerable<T> for some T, 
    // or it implements IEnumerable<T> for some T. We need to find the interface.
    if (IsIEnumerable(type))
        return type;
    Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
    Debug.Assert(t.Length == 1);
    return t[0];
}

So gegeben, jeder Type, können wir aus der IEnumerable<T> Instanziierung ziehen jetzt -. Und geltend machen, wenn es nicht (genau) ist

Mit dieser Arbeit aus dem Weg, ist das eigentliche Problem zu lösen nicht allzu schwierig. Ich habe Ihre Methode CallAny umbenannt und geändert, um die Parametertypen wie vorgeschlagen:

static Expression CallAny(Expression collection, Delegate predicate)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType);

    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,
            Expression.Constant(predicate));
}

Hier ist eine Main() Routine, die alle oben genannten Code verwendet und überprüft, ob es für einen trivialen Fall funktioniert:

static void Main()
{
    // sample
    List<string> strings = new List<string> { "foo", "bar", "baz" };

    // Trivial predicate: x => x.StartsWith("b")
    ParameterExpression p = Expression.Parameter(typeof(string), "item");
    Delegate predicate = Expression.Lambda(
        Expression.Call(
            p,
            typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
            Expression.Constant("b")),
        p).Compile();

    Expression anyCall = CallAny(
        Expression.Constant(strings),
        predicate);

    // now test it.
    Func<bool> a = (Func<bool>) Expression.Lambda(anyCall).Compile();
    Console.WriteLine("Found? {0}", a());
    Console.ReadLine();
}

Andere Tipps

Barry Antwort eine funktionierende Lösung für die Frage vom Verfasser gestellt. Dank dieser beiden Personen zu fragen und Antworten.

fand ich diesen Thread als ich eine Lösung für ein ganz ähnliches Problem zu entwickeln versuche: programmatisch einen Ausdruck Baum erstellen, die einen Aufruf an den Any () -Methode. Als zusätzliche Einschränkung, aber die Endziel meiner Lösung war eine solche dynamisch erstellten Ausdruck durch Linq-to-SQL zu übergeben, so dass die Arbeit der Any () Auswertung wird in der tatsächlich durchgeführt DB selbst.

Leider, wie die Lösung bisher diskutiert wird, ist nicht etwas, die Linq-to-SQL verarbeiten kann.

unter der Annahme, Betrieb, dass dies ein ziemlich beliebter Grund für den Wunsch sein könnte, einen dynamischen Ausdruck Baum zu bauen, habe ich beschlossen, den Faden mit meinen Erkenntnissen zu erweitern.

Wenn ich das Ergebnis von Barrys CallAny () als Ausdruck in einer Linq-to-SQL Wo () Klausel verwenden wollte ich eine InvalidOperationException mit den folgenden Eigenschaften erhalten:

  • HResult = -2146233079
  • Message = "Internal .NET Framework Data Provider Fehler 1025"
  • Source = System.Data.Entity

einen hartcodierte Ausdrucksbaumes zu dem dynamisch erstellt einer Verwendung CallAny Nach einem Vergleich (), fand ich, dass das Kernproblem zum Compilieren zurückzuführen war () des Prädikatenausdruckes und der Versuch, die resultierend Delegierten im CallAny aufzuzurufen (). Ohne zu graben tief in Linq to SQL-Implementierungsdetails, schien es mir sinnvoll, dass Linq to SQL würde nicht wissen, was mit einer solchen Struktur zu tun.

Deshalb wird nach einigen Experimenten konnte ich mein gewünschtes Ziel erreichen, indem er leicht die vorgeschlagene CallAny () -Implementierung Überarbeitung für die Any () Pr¨adikatenlogik eine predicateExpression anstatt einen Delegierten zu nehmen.

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 Klarheit sollte ich zeigen zunächst das Spielzeug Domänenmodell & EF Kontext ich verwende. Im Grunde genommen mein Modell ist eine einfache Blogs & Posts Domain ... wo ein Blog mehrere Beiträge hat und jeder Beitrag hat ein Datum:

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public DateTime Date { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

Mit dieser Domäne eingerichtet, hier ist mein Code schließlich die überarbeitete CallAny auszuüben () und macht Linq-to-SQL die Arbeit des ANY () auswertet. Mein besonderes Beispiel konzentrieren alle Blogs auf Rückkehr, die mindestens einen Beitrag haben, die neuer als eine bestimmte Stichtag ist.

static void Main()
{
    Database.SetInitializer<BloggingContext>(
        new DropCreateDatabaseAlways<BloggingContext>());

    using (var ctx = new BloggingContext())
    {
        // insert some data
        var blog  = new Blog(){Name = "blog"};
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
        ctx.Blogs.Add(blog);

        blog = new Blog() { Name = "blog 2" };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        ctx.Blogs.Add(blog);
        ctx.SaveChanges();


        // first, do a hard-coded Where() with Any(), to demonstrate that
        // Linq-to-SQL can handle it
        var cutoffDateTime = DateTime.Parse("12/31/2001");
        var hardCodedResult = 
            ctx.Blogs.Where((b) => b.Posts.Any((p) => p.Date > cutoffDateTime));
        var hardCodedResultCount = hardCodedResult.ToList().Count;
        Debug.Assert(hardCodedResultCount > 0);


        // now do a logically equivalent Where() with Any(), but programmatically
        // build the expression tree
        var blogsWithRecentPostsExpression = 
            BuildExpressionForBlogsWithRecentPosts(cutoffDateTime);
        var dynamicExpressionResult = 
            ctx.Blogs.Where(blogsWithRecentPostsExpression);
        var dynamicExpressionResultCount = dynamicExpressionResult.ToList().Count;
        Debug.Assert(dynamicExpressionResultCount > 0);
        Debug.Assert(dynamicExpressionResultCount == hardCodedResultCount);
    }
}

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

private Expression<Func<Blog, Boolean>> BuildExpressionForBlogsWithRecentPosts(
    DateTime cutoffDateTime)
{
    var blogParam = Expression.Parameter(typeof(Blog), "b");
    var postParam = Expression.Parameter(typeof(Post), "p");

    // (p) => p.Date > cutoffDateTime
    var left = Expression.Property(postParam, "Date");
    var right = Expression.Constant(cutoffDateTime);
    var dateGreaterThanCutoffExpression = Expression.GreaterThan(left, right);
    var lambdaForTheAnyCallPredicate = 
        Expression.Lambda<Func<Post, Boolean>>(dateGreaterThanCutoffExpression, 
            postParam);

    // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
    var collectionProperty = Expression.Property(blogParam, "Posts");
    var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
    return Expression.Lambda<Func<Blog, Boolean>>(resultExpression, blogParam);
}

Hinweis: Ich fand ein anderes scheinbar unwichtiges Delta zwischen der hartcodiert und dynamisch aufgebaut Ausdrücke. Die dynamisch aufgebaut hat man einen „extra“ Aufruf konvertieren darin, dass die hartcodierte Version nicht zu haben scheint (oder müssen?). Die Umwandlung wird in der CallAny () Umsetzung eingeführt. Linq-to-SQL scheint in Ordnung zu sein mit ihm ich es so an seinem Platz belassen (obwohl es nicht notwendig). Ich war nicht ganz sicher, ob diese Umwandlung könnte in einigen robustere Verwendungen als mein Spielzeug Probe benötigt werden.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top