使用 Expression.Call 调用 SelectMany - 错误的参数
Call SelectMany with Expression.Call - wrong argument
我想通过字符串遍历关系。
我有一个人、一个工作和一个位置,它们是相关联的人 N:1 工作和工作 1:N 位置(每个人可以有 1 个工作,一个工作可以有多个位置)。
我方法的输入:
- 人员列表(后来EFCore中人员的IQueryable)
- 从人到他们的工作的字符串 "Work.Locations"
所以我必须调用表达式:
1. 在人员名单上 a list.Select(x => x.Work)
2. 在该结果上 list.SelectMany(x => x.Locations)
当我在 SelectMany 方法(在 TODO 处)上执行 Expression.Call 时出现错误
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: 'Expression of type
'System.Func2[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]]])''
我的完整代码如下所示:
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;
}
您使用类型为 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());
}
它们共享来自对象的相同继承,但彼此之间仍然没有线性继承。
尝试 casting/converting 你的 ICollection<T>
到 IEnumerable<T>
然后把它作为参数。
它失败了,因为 SelectMany<TSource, TResult>
方法需要
Expression<Func<TSource, IEnumerable<TResult>>>
当你经过时
Expression<Func<TSource, ICollection<TResult>>>
它们不一样,后者不能转换为前者,因为 Expression<TDelegate>
是 class,而 class es 是不变的。
根据您的代码,预期的 lambda 结果类型如下所示:
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 });
也是不正确的(因为当你传递 "Work.Locations" 时,actualType
将是 ICollection<Location>
,而不是 ToList
期望的 Location
,所以它必须更改为:
var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actual.ElementType });
一般来说,您可以删除 actualType
变量并始终为此目的使用 IQueryable.ElementType
。
最后作为奖励,无需手动查找泛型方法定义。 Expression.Call
有一个特殊的重载,它允许您轻松地 "call" 静态泛型(而不仅仅是)名称方法。例如,SelectMany
"call" 将是这样的:
selectExpression = Expression.Call(
typeof(Queryable), nameof(Queryable.SelectMany), new [] { origType, firstGenType },
queryable.Expression, lambda);
和调用Select
是相似的。
也不需要创建额外的 lambda 表达式、编译和动态调用它以获得结果 IQueryable
。同样可以通过使用IQueryProvider.CreateQuery
方法来实现:
//var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
var result = queryable.Provider.CreateQuery(selectExpression);
我想通过字符串遍历关系。
我有一个人、一个工作和一个位置,它们是相关联的人 N:1 工作和工作 1:N 位置(每个人可以有 1 个工作,一个工作可以有多个位置)。
我方法的输入:
- 人员列表(后来EFCore中人员的IQueryable)
- 从人到他们的工作的字符串 "Work.Locations"
所以我必须调用表达式: 1. 在人员名单上 a list.Select(x => x.Work) 2. 在该结果上 list.SelectMany(x => x.Locations)
当我在 SelectMany 方法(在 TODO 处)上执行 Expression.Call 时出现错误
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: '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]]])''
我的完整代码如下所示:
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;
}
您使用类型为 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());
}
它们共享来自对象的相同继承,但彼此之间仍然没有线性继承。
尝试 casting/converting 你的 ICollection<T>
到 IEnumerable<T>
然后把它作为参数。
它失败了,因为 SelectMany<TSource, TResult>
方法需要
Expression<Func<TSource, IEnumerable<TResult>>>
当你经过时
Expression<Func<TSource, ICollection<TResult>>>
它们不一样,后者不能转换为前者,因为 Expression<TDelegate>
是 class,而 class es 是不变的。
根据您的代码,预期的 lambda 结果类型如下所示:
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 });
也是不正确的(因为当你传递 "Work.Locations" 时,actualType
将是 ICollection<Location>
,而不是 ToList
期望的 Location
,所以它必须更改为:
var genericToListMethod = enumerableToListMethod.MakeGenericMethod(new[] { actual.ElementType });
一般来说,您可以删除 actualType
变量并始终为此目的使用 IQueryable.ElementType
。
最后作为奖励,无需手动查找泛型方法定义。 Expression.Call
有一个特殊的重载,它允许您轻松地 "call" 静态泛型(而不仅仅是)名称方法。例如,SelectMany
"call" 将是这样的:
selectExpression = Expression.Call(
typeof(Queryable), nameof(Queryable.SelectMany), new [] { origType, firstGenType },
queryable.Expression, lambda);
和调用Select
是相似的。
也不需要创建额外的 lambda 表达式、编译和动态调用它以获得结果 IQueryable
。同样可以通过使用IQueryProvider.CreateQuery
方法来实现:
//var result = Expression.Lambda(selectExpression).Compile().DynamicInvoke() as IQueryable;
var result = queryable.Provider.CreateQuery(selectExpression);