27 votes

Comment puis-je créer une arborescence d'expression appelant IEnumerable<TSource>.Tout(...)?

Je suis en train de créer une arborescence d'expression qui représente le suivant:

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

Raccourcie pour des raisons de clarté, j'ai le texte suivant:

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

Ce que je fais mal? Quelqu'un a des suggestions?

68voto

Barry Kelly Points 30330

Il y a plusieurs choses de mal avec la façon dont vous allez parler.

  1. Vous êtes de mélange niveaux d'abstraction. Le paramètre T de GetAnyExpression<T> pourraient être différents pour le paramètre de type utilisé pour instancier propertyExp.Type. Le paramètre de type T est un pas de plus dans l'abstraction de la pile au moment de la compilation - à moins que vous appelez GetAnyExpression<T> par réflexion, il sera déterminé au moment de la compilation - mais le type intégré dans l'expression passée en tant que propertyExp est déterminé au moment de l'exécution. Votre passage du prédicat comme un Expression est aussi une abstraction mixup - ce qui est le point suivant.

  2. Le prédicat vous êtes de passage à GetAnyExpression devrait être un délégué de la valeur, pas un Expression de toute nature, depuis que vous essayez d'appeler Enumerable.Any<T>. Si vous essayez d'appeler une expression de l'arbre-version de Any, alors vous devez passer un LambdaExpression au lieu de cela, ce qui vous serait le citer, et est l'un des rares cas où vous avez peut être justifiée dans le passage d'un plus type spécifique de l'Expression, ce qui m'amène à mon prochain point.

  3. En général, vous devriez passer autour de Expression valeurs. Lorsque vous travaillez avec des arbres d'expression en général - et cela s'applique sur toutes sortes de compilateurs, et pas seulement LINQ et ses amis - vous devez le faire d'une manière qui est agnostique que l'effet immédiat de la composition du nœud de l'arbre avec lequel vous travaillez. Vous êtes en supposant que vous appelez Any sur MemberExpression, mais vous n'avez pas réellement besoin de savoir que vous avez affaire à un MemberExpression, juste un Expression de type certains instanciation d' IEnumerable<>. C'est une erreur commune pour les personnes qui ne connaissent pas les bases de compilateur ASTs. Frans Bouma a à plusieurs reprises fait la même erreur quand il a d'abord commencé à travailler avec des arbres d'expression - de la pensée dans des cas particuliers. Pense généralement. Vous éviterez bien des tracas dans le moyen et le long terme.

  4. Et ici vient la viande de votre problème (même si le deuxième et probablement les premières questions ont peu de vous, si vous aviez franchi it) - vous avez besoin pour trouver le générique de surcharge de la Toute méthode, puis l'instancier avec le type correct. La réflexion n'est pas de vous fournir une solution facile ici; vous avez besoin de parcourir et de trouver la bonne version.

Donc, en le décomposant: vous avez besoin de trouver une méthode générique (Any). Voici une fonction d'utilité qui fait que:

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 requiert le type des arguments et de les corriger types d'argument. Obtenir à partir de votre propertyExp Expression , ce n'est pas trivial, car l' Expression peut être d'un List<T> type, ou d'un autre type, mais nous avons besoin de trouver de l' IEnumerable<T> d'instanciation et d'obtenir son type d'argument. J'ai encapsulé dans un couple de 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, étant donné un Type, nous pouvons maintenant tirer l' IEnumerable<T> instanciation d'en sortir, et d'affirmer s'il n'y a pas (exactement) une.

Avec cette œuvre hors de la voie, la résolution du problème réel n'est pas trop difficile. J'ai renommé votre méthode pour CallAny, et changé les types de paramètres, comme l'a 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 un Main() de routine qui utilise tout le code ci-dessus et vérifie qu'il 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();
}

6voto

Aaron Heusser Points 41

Barry réponse fournit une solution à la question posée par le posteur d'origine. Grâce à ces deux personnes pour poser et de répondre.

J'ai trouvé ce fil que j'essayais de trouver une solution à tout problème similaire: par programme la création d'une arborescence d'expression qui comprend un appel à la Toute méthode. Comme une contrainte supplémentaire, cependant, le but ultime de ma solution a été de passer une telle créé dynamiquement expression par le biais de Linq-to-SQL, afin que le travail de chacun() l'évaluation est en fait effectuée dans la base de données elle-même.

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

D'exploitation en vertu de l'hypothèse que cela pourrait être un très populaire raison de vouloir construire une expression dynamique de l'arbre, j'ai décidé d'augmenter le fil avec mes conclusions.

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

  • HResult=-2146233079
  • Message="Interne .NET Framework Fournisseur de Données erreur de 1025"
  • Source=Système.Les données.Entité

Après avoir comparé codé en dur expression d'arbre en arbre créé dynamiquement une aide CallAny(), j'ai trouvé que le problème était dû à la Compilation() de l'expression de prédicat et de la tentative d'invoquer la résultante délégué dans la CallAny(). Sans creuser profondément dans Linq-to-SQL détails de mise en œuvre, il semble raisonnable pour moi que Linq-to-SQL ne sais pas quoi faire avec une telle structure.

Donc, après quelques essais, j'ai pu atteindre mon objectif souhaité par une légère révision de l'suggéré CallAny() de la mise en œuvre de prendre un predicateExpression plutôt que d'un délégué pour le Tout() logique des prédicats.

Ma nouvelle méthode 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. Pour plus de clarté, je tiens d'abord à montrer le jouet modèle de domaine & EF contexte, je suis en utilisant. Fondamentalement, mon modèle est simpliste les Blogs et les Postes de domaine ... où un blog a plusieurs postes et chaque poste 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; }
}

Avec ce domaine établi, voici mon code pour en fin de compte l'exercice de la version révisée de la CallAny() et Linq-to-SQL faire le travail d'évaluation du Tout(). Mon exemple particulier va se concentrer sur le retour de tous les Blogs qui ont au moins un Post qui est plus récente que déterminée à la date butoir.

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 apparemment sans importance delta entre codées en dur et dynamique, construit des expressions. La dynamique construit un a un "extra" convertir un appel pour que les codée en dur version ne semble pas avoir (ou le besoin?). La conversion est introduit dans le CallAny() de la mise en œuvre. Linq-to-SQL semble être ok avec elle, alors je l'ai laissé en place (bien que c'était inutile). Je n'étais pas tout à fait certain si cette conversion peut être nécessaire dans certains plus robuste usages que mon jouet de l'échantillon.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X