Comment créer une arborescence d'expression appelant IEnumerable .Tout(...)?

.net c# expression-trees linq

Question

J'essaie de créer un arbre d'expression qui représente ce qui suit:

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

Raccourci pour plus de clarté, j'ai les éléments suivants:

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

Qu'est-ce que je fais mal? Quelqu'un a des suggestions?

Réponse acceptée

Vous avez plusieurs problèmes avec la façon dont vous vous y prenez.

  1. Vous mélangez les niveaux d'abstraction. Le paramètre T de GetAnyExpression<T> peut être différent du paramètre de type utilisé pour instancier propertyExp.Type . Le paramètre de type T est un peu plus proche dans la pile d'abstraction à compiler (sauf si vous appelez GetAnyExpression<T> par réflexion, il sera déterminé lors de la compilation), mais le type incorporé dans l'expression passée en tant que propertyExp est déterminé à l'exécution. . Votre passage du prédicat comme une Expression est aussi une abstraction mixup - qui est le point suivant.

  2. Le prédicat que vous transmettez à GetAnyExpression doit être une valeur de délégué, et non une Expression , car vous essayez d'appeler Enumerable.Any<T> . Si vous essayez d'appeler une version arborescente d'expression de Any , vous devez plutôt passer une LambdaExpression , que vous LambdaExpression , et constitue l'un des rares cas où vous pourriez être justifié de passer un type plus spécifique que Expression, ce qui me mène à mon prochain point.

  3. En général, vous devriez faire circuler les valeurs d' Expression . Lorsque vous travaillez avec des arbres d'expression en général - et cela s'applique à tous les types de compilateurs, et pas seulement à LINQ et ses amis -, vous devez le faire de manière agnostique quant à la composition immédiate de l'arbre de noeud avec lequel vous travaillez. Vous présumez que vous appelez Any sur un MemberExpression , mais vous n'avez pas vraiment besoin de savoir que vous avez affaire à un MemberExpression , juste une Expression de type une instanciation de IEnumerable<> . Il s'agit d'une erreur courante chez les personnes non familiarisées avec les bases des AST du compilateur. Frans Bouma a commis la même erreur à plusieurs reprises lorsqu'il a commencé à travailler avec des arbres d'expression - pensant dans des cas particuliers. Pensez en général. Vous vous épargnerez beaucoup de tracas à moyen et long terme.

  4. Et voici le fond de votre problème (bien que le deuxième et probablement le premier problème vous aurait mordu si vous l'aviez dépassé) - vous devez trouver la surcharge générique appropriée de la méthode Any, puis l'instancier avec le type correct. La réflexion ne vous fournit pas une sortie facile ici; vous devez parcourir et trouver une version appropriée.

Donc, en le décomposant: vous devez trouver une méthode générique ( Any ). Voici une fonction utilitaire qui fait ça:

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

Cependant, il nécessite les arguments de type et les types d'argument corrects. Obtenir que de votre propertyExp Expression est pas tout à fait banal, parce que l' Expression peut être d'une List<T> type ou d' un autre type, mais nous devons trouver le IEnumerable<T> instanciation et obtenir son argument de type. J'ai résumé cela dans quelques fonctions:

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

Donc, pour n'importe quel Type , nous pouvons maintenant extraire l'instanciation IEnumerable<T> - et affirmer s'il n'y en a pas (exactement).

Une fois le travail accompli, résoudre le vrai problème n’est pas si difficile. J'ai renommé votre méthode CallAny et modifié les types de paramètres comme suggéré:

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

Voici une routine Main() qui utilise tout le code ci-dessus et vérifie que cela fonctionne pour un cas trivial:

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

Réponse populaire

La réponse de Barry fournit une solution de travail à la question posée par l'affiche originale. Merci à ces deux personnes d’avoir demandé et répondu.

J'ai trouvé ce fil alors que j'essayais de trouver une solution à un problème assez similaire: créer par programme un arbre d'expression qui inclut un appel à la méthode Any (). Comme contrainte supplémentaire, toutefois, le but ultime de ma solution était de transmettre une expression de ce type créée dynamiquement via Linq-to-SQL afin que le travail de l'évaluation Any () soit réellement effectué dans la base de données elle-même.

Malheureusement, la solution décrite jusqu'à présent n'est pas quelque chose que Linq-to-SQL peut gérer.

En partant du principe que cela pourrait constituer une raison assez répandue de vouloir créer un arbre d’expression dynamique, j’ai décidé d’augmenter le fil de mes découvertes.

Lorsque j'ai tenté d'utiliser le résultat de CallAny () de Barry en tant qu'expression dans une clause Where () Linq-to-SQL, j'ai reçu une exception InvalidOperationException avec les propriétés suivantes:

  • HResult = -2146233079
  • Message = "Erreur 1025 du fournisseur de données .NET Framework interne"
  • Source = System.Data.Entity

Après avoir comparé un arbre d'expression codé en dur à celui créé de manière dynamique à l'aide de CallAny (), j'ai constaté que le problème fondamental était dû à Compile () de l'expression de prédicat et à la tentative d'appel du délégué résultant dans CallAny (). Sans approfondir les détails de l'implémentation Linq-to-SQL, il me semblait raisonnable que Linq-to-SQL ne sache pas quoi faire avec une telle structure.

Par conséquent, après quelques expériences, j'ai pu atteindre mon objectif souhaité en modifiant légèrement l'implémentation CallAny () suggérée pour prendre un predicateExpression plutôt qu'un délégué pour la logique de prédicat Any ().

Ma méthode révisée est:

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

Maintenant, je vais démontrer son utilisation avec EF. Par souci de clarté, je devrais d'abord montrer le modèle de domaine de jouet et le contexte EF que j'utilise. Fondamentalement, mon modèle est un domaine simpliste Blogs & Posts ... où un blog a plusieurs posts et chaque post a une date:

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

Une fois ce domaine défini, voici mon code pour exercer à terme le CallAny () révisé et laisser Linq-to-SQL effectuer le travail d’évaluation de Any (). Mon exemple particulier portera sur le retour de tous les blogs qui ont au moins une publication plus récente qu'une date limite spécifiée.

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

Où BuildExpressionForBlogsWithRecentPosts () est une fonction d'assistance qui utilise CallAny () comme suit:

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

REMARQUE: j'ai trouvé un autre delta apparemment sans importance entre les expressions codées en dur et celles construites dynamiquement. Celui qui est construit de manière dynamique contient un appel de conversion "supplémentaire" que la version codée en dur ne semble pas avoir (ou avoir besoin?). La conversion est introduite dans l'implémentation CallAny (). Linq-to-SQL semble être d'accord avec cela, donc je l'ai laissé en place (même si c'était inutile). Je n'étais pas tout à fait sûr si cette conversion pourrait être nécessaire dans des utilisations plus robustes que mon échantillon de jouet.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi