Reputation: 130
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:
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.Func
2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection
1[GenericResourceLoading.Data.Location]]' cannot be used for parameter of type 'System.Linq.Expressions.Expression1[System.Func
2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable
1[GenericResourceLoading.Data.Location] SelectMany[Work,Location](System.Linq.IQueryable1[GenericResourceLoading.Data.Work], System.Linq.Expressions.Expression
1[System.Func2[GenericResourceLoading.Data.Work,System.Collections.Generic.IEnumerable
1[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
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
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