Gerry
Gerry

Reputation: 130

Call SelectMany with Expression.Call - wrong argument

I want to go through relations by string.

I have a Person, a Work and Location that are connected Person N:1 Work and Work 1:N Location (each person can have 1 work and a work can have many locations).

Input for my method:

  1. A list of persons (later the IQueryable of persons in EFCore)
  2. The string "Work.Locations" to go from person to their work

So I have to call with Expressions: 1. on the persons list a list.Select(x => x.Work) 2. on that result a list.SelectMany(x => x.Locations)

I get an error when I make the Expression.Call on the SelectMany method (at the 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});

I get this exception:

System.ArgumentException: 'Expression of type 'System.Func2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection1[GenericResourceLoading.Data.Location]]' cannot be used for parameter of type 'System.Linq.Expressions.Expression1[System.Func2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable1[GenericResourceLoading.Data.Location] SelectMany[Work,Location](System.Linq.IQueryable1[GenericResourceLoading.Data.Work], System.Linq.Expressions.Expression1[System.Func2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable1[GenericResourceLoading.Data.Location]]])''

My full code looks like that:

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

Upvotes: 2

Views: 1459

Answers (2)

Ivan Stoev
Ivan Stoev

Reputation: 205629

It's failing because SelectMany<TSource, TResult> method expects

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

while you are passing

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

These are not the same and the later is not convertible to the former simply because Expression<TDelegate> is a class, and classes are invariant.

Taking your code, the expected lambda result type is like this:

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

Now you can either use Expression.Convert to change (cast) the property type:

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

or (my preferred) use another Expression.Lambda method overload with explicit delegate type (obtained via Expression.GetFuncType):

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

Either of these will solve your original issue.

Now before you get the next exception, the following line:

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

is also incorrect (because when you pass "Work.Locations", the actualType will be ICollection<Location>, not Location which ToList expects), so it has to be changed to:

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

In general you could remove actualType variable and always use IQueryable.ElementType for that purpose.

Finally as a bonus, there is no need to find manually the generic method definitions. Expression.Call has a special overload which allows you to easily "call" static generic (and not only) methods by name. For instance, the SelectMany "call" would be like this:

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

and calling Select is similar.

Also there is no need to create additional lambda expression, compile and dynamically invoke it in order to get the resulting IQueryable. The same can be achieved by using IQueryProvider.CreateQuery method:

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

Upvotes: 2

Chrᴉz remembers Monica
Chrᴉz remembers Monica

Reputation: 1904

You use your method with a type of ICollection<T>, but your expression takes a IEnumerable<T> as input. And SelectMany() takes a IQueryable<T> as input. Both IQueryable<T> and ICollection<T> are derived from IEnumerable<T>, but if you need a IQueryable<T> you cant give a ICollection<T>.

This would be the same as the following example:

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

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

They share the same inheritance from object, but still have no linear inheritance to each other.

Try casting/converting your ICollection<T> to IEnumerable<T> and then give that as parameter.

Upvotes: 0

Related Questions