Appelez SelectMany avec Expression.Call - argument incorrect

c# expression-trees linq

Question

Je veux passer en revue les relations par chaîne.

J'ai une personne, un travail et un lieu qui sont connectés Personne N: 1 Travail et Travail 1: N Lieu (Chaque personne peut avoir 1 travail et un travail peut avoir plusieurs emplacements).

Entrée pour ma méthode:

  1. Une liste de personnes (plus tard l'IQueryable de personnes dans EFCore)
  2. La chaîne "Work.Locations" pour aller de personne à travail

Je dois donc appeler avec Expressions: 1. sur la liste des personnes, une liste.Sélectionnez (x => x.Work) 2. sur ce résultat, une liste.SelectMany (x => x.Locations)

Je reçois une erreur lorsque je fais Expression.Call sur la méthode SelectMany (sur le TODO)

        var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "SelectMany" && 
            a.GetGenericArguments().Length == 2 &&
            a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType == 
            typeof(Expression<Func<object, IEnumerable<object>>>));

        var par = Expression.Parameter(origType, "x");
        var propExpr = Expression.Property(par, property);
        var lambda = Expression.Lambda(propExpr, par);

        var firstGenType = reflectedType.GetGenericArguments()[0];

        //TODO: why do I get an exception here?
        selectExpression = Expression.Call(null,
            selectMethod.MakeGenericMethod(new Type[] {origType, firstGenType}),
            new Expression[] { queryable.Expression, lambda});

Je reçois cette exception:

System.ArgumentException: 'Expression de type' System.Func 2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection 1 [GenericResourceLoading.Data.Location]] 'ne peut pas être utilisé pour le paramètre de type' System.Linq.Expressions .Expression 1[System.Func 2 [GenericResourceLoading.Data.Work, System.Collections.Generic.IEnumerable 1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable 1 [GenericResourceLoading.Data.Location], sélectionnezMany [Travail, Lieu] (System.Linq.IQueryable 1[GenericResourceLoading.Data.Work], System.Linq.Expressions.Expression 1 [System.Func 2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable 1 [GenericResourceLoading .Data.Location]]]) ''

Mon code complet ressemble à ça:

    public void LoadGeneric(IQueryable<Person> queryable, string relations)
    {
        var splitted = relations.Split('.');
        var actualType = typeof(Person);

        IQueryable actual = queryable;
        foreach (var property in splitted)
        {
            actual = LoadSingleRelation(actual, ref actualType, property);
        }

        MethodInfo enumerableToListMethod = typeof(Enumerable).GetMethod("ToList", BindingFlags.Public | BindingFlags.Static);
        var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actualType });

        var results = genericToListMethod.Invoke(null, new object[] { actual });
    }

    private IQueryable LoadSingleRelation(IQueryable queryable, ref Type actualType, string property)
    {
        var origType = actualType;
        var prop = actualType.GetProperty(property, BindingFlags.Instance | BindingFlags.Public);
        var reflectedType = prop.PropertyType;
        actualType = reflectedType;

        var isGenericCollection = reflectedType.IsGenericType && reflectedType.GetGenericTypeDefinition() == typeof(ICollection<>);

        MethodCallExpression selectExpression;

        if (isGenericCollection)
        {
            var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "SelectMany" && 
                a.GetGenericArguments().Length == 2 &&
                a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType == 
                typeof(Expression<Func<object, IEnumerable<object>>>));

            var par = Expression.Parameter(origType, "x");
            var propExpr = Expression.Property(par, property);
            var lambda = Expression.Lambda(propExpr, par);

            var firstGenType = reflectedType.GetGenericArguments()[0];

            //TODO: why do I get an exception here?
            selectExpression = Expression.Call(null,
                selectMethod.MakeGenericMethod(new Type[] {origType, firstGenType}),
                new Expression[] { queryable.Expression, lambda});
        }
        else
        {
            var selectMethod = typeof(Queryable).GetMethods().Single(a => a.Name == "Select" && 
                a.MakeGenericMethod(typeof(object), typeof(object)).GetParameters()[1].ParameterType == 
                typeof(Expression<Func<object, object>>));

            var par = Expression.Parameter(origType, "x");
            var propExpr = Expression.Property(par, property);
            var lambda = Expression.Lambda(propExpr, par);

            selectExpression = Expression.Call(null,
                selectMethod.MakeGenericMethod(new Type[] {origType, reflectedType}),
                new Expression[] {queryable.Expression, lambda});
        }

        var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
        return result;
    }

Réponse acceptée

Elle échoue car la SelectMany<TSource, TResult> s’attend à

Expression<Func<TSource, IEnumerable<TResult>>>

pendant que tu passes

Expression<Func<TSource, ICollection<TResult>>>

Ce ne sont pas les mêmes et le dernier n'est pas convertible au précédent simplement parce que Expression<TDelegate> est une classe et que les classes sont invariantes.

En prenant votre code, le type de résultat attendu lambda est le suivant:

var par = Expression.Parameter(origType, "x");
var propExpr = Expression.Property(par, property);
var firstGenType = reflectedType.GetGenericArguments()[0];
var resultType = typeof(IEnumerable<>).MakeGenericType(firstGenType);

Vous pouvez maintenant utiliser Expression.Convert pour changer (transtyper) le type de propriété:

var lambda = Expression.Lambda(Expression.Convert(propExpr, resultType), par);

ou (à mon goût) utilisez une autre surcharge de méthode Expression.Lambda avec un type de délégué explicite (obtenue via Expression.GetFuncType ):

var lambda = Expression.Lambda(Expression.GetFuncType(par.Type, resultType), propExpr, par);

L'un ou l'autre résoudra votre problème initial.

Maintenant, avant que vous obteniez la prochaine exception, la ligne suivante:

var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actualType });

est également incorrect (car, lorsque vous passez "Work.Locations", le type actualType sera ICollection<Location> , et pas un Location ToList ).

var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actual.ElementType });

En général, vous pouvez supprimer la variable actualType et toujours utiliser IQueryable.ElementType à cette fin.

Enfin, en prime, il n’est pas nécessaire de rechercher manuellement les définitions de méthodes génériques. Expression.Call a une surcharge spéciale qui vous permet "d'appeler" facilement des méthodes génériques statiques (et pas seulement) par leur nom. Par exemple, l'appel " SelectMany " ressemblerait à ceci:

selectExpression = Expression.Call(
    typeof(Queryable), nameof(Queryable.SelectMany), new [] { origType, firstGenType },
    queryable.Expression, lambda);

et appeler Select est similaire.

De plus, il n'est pas nécessaire de créer une expression lambda supplémentaire, de la compiler et de l'invoquer de manière dynamique pour obtenir l' IQueryable résultant. La même chose peut être obtenue en utilisant la méthode IQueryProvider.CreateQuery :

//var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
var result = queryable.Provider.CreateQuery(selectExpression);

Réponse populaire

Vous utilisez votre méthode avec un type ICollection<T> , mais votre expression prend un IEnumerable<T> en entrée. Et SelectMany() prend un IQueryable<T> en entrée. IQueryable<T> et ICollection<T> sont tous deux dérivés de IEnumerable<T> , mais si vous avez besoin d'un IQueryable<T> vous ne pouvez pas donner ICollection<T> .

Ce serait la même chose que l'exemple suivant:

class MyIEnumerable
{ }
class MyICollection : MyIEnumerable
{ }
class MyIQueryable : MyIEnumerable
{ }
private void MethodWithMyIQueryable(MyIQueryable someObj)
{ }

private void DoSth()
{
    //valid
    MethodWithMyIQueryable(new MyIQueryable());
    //invalid
    MethodWithMyIQueryable(new MyICollection());
}

Ils partagent le même héritage d'objet, mais n'ont toujours pas d'héritage linéaire entre eux.

Essayez de convertir / convertir votre ICollection<T> en IEnumerable<T> , puis donnez-le comme paramètre.




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