Comment créer une arborescence d&#39;expression appelant IEnumerable <TSource> .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.Any(i => i.Name == "name");

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

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

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

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

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:

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

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

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

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

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