Вызовите SelectMany с Expression.Call - неверный аргумент - PullRequest
0 голосов
/ 05 июня 2018

Я хочу пройти отношения через строку.

У меня есть Лицо, Работа и Место, которые связаны. Лицо N: 1 Работа и Работа 1: N Местоположение (каждый человек может иметь 1 работу иработа может иметь много мест).

Ввод для моего метода:

  1. Список лиц (позже IQueryable людей в EFCore)
  2. Строка "Работа.Locations "перейти от человека к своей работе

Так что мне нужно вызвать с помощью выражений: 1. в списке лиц список. Выберите (x => x.Work) 2. на этот результатa list.SelectMany (x => x.Locations)

Я получаю сообщение об ошибке при выполнении Expression.Call для метода SelectMany (в 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});

Я получаю этоисключение:

System.ArgumentException: 'Выражение типа' System.Func 2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection 1 [GenericResourceLoading.Data.Location]] 'нельзя использовать для параметра типа' System.Linq.Expressions.Выражение 1[System.Func 2 [GenericResourceLoading.Data.Work, System.Collections.Generic.IEnumerable 1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable 1 [GenericResourceLoading.Data.Location] SelectMany [Работа, Местоположение] (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]]]) ''

Мой полный код выглядит так:

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

Ответы [ 2 ]

0 голосов
/ 05 июня 2018

Это сбой, потому что SelectMany<TSource, TResult> метод ожидает

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

во время передачи

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

Это не то же самое, и последний не может быть преобразован в первый просто потому, что Expression<TDelegate> - это класс , а классы инвариантны.

С учетом вашего кода ожидаемый лямбда-тип результата выглядит следующим образом:

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

Теперь вы можете использовать либоExpression.Convert для изменения (приведения) типа свойства:

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

или (мое предпочтение) использовать другую перегрузку метода Expression.Lambda с явным типом делегата (полученного через Expression.GetFuncType):

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

Любая из них решит вашу исходную проблему.

Теперь, прежде чем вы получите следующее исключение, следующая строка:

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

также неверна (потому что, когда вы пропустите "Работа".Locations ", actualType будет ICollection<Location>, а не Location, что ToList ожидает), поэтому его необходимо изменить на:

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

В общем случае вы можете удалить actualTypeпеременная и всегда используйте IQueryable.ElementType для этой цели.

Наконец, в качестве бонуса, не нужно искатьвручную общие определения методов.Expression.Call имеет специальную перегрузку, которая позволяет легко «вызывать» статические обобщенные (и не только) методы по имени.Например, SelectMany «вызов» будет выглядеть так:

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

, а вызов Select аналогичен.

Также нет необходимости создавать дополнительное лямбда-выражение, компилировать и динамически вызывать его для получения результирующего IQueryable.То же самое может быть достигнуто с помощью метода IQueryProvider.CreateQuery:

//var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
var result = queryable.Provider.CreateQuery(selectExpression);
0 голосов
/ 05 июня 2018

Вы используете свой метод с типом ICollection<T>, но ваше выражение принимает IEnumerable<T> в качестве ввода.И SelectMany() принимает IQueryable<T> в качестве ввода.И IQueryable<T>, и ICollection<T> являются производными от IEnumerable<T>, но если вам нужно IQueryable<T>, вы не можете дать ICollection<T>.

Это будет то же самое, что и в следующем примере:

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

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

Они имеют одно и то же наследование от объекта, но по-прежнему не имеют линейного наследования друг от друга.

Попробуйте преобразовать ICollection<T> в IEnumerable<T> и преобразовать его в качестве параметра.

...