Search code examples
c#linqexpression-trees

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

Solution

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