使用 Expression.Call 调用 SelectMany - 错误的参数

Call SelectMany with Expression.Call - wrong argument

我想通过字符串遍历关系。

我有一个人、一个工作和一个位置,它们是相关联的人 N:1 工作和工作 1:N 位置(每个人可以有 1 个工作,一个工作可以有多个位置)。

我方法的输入:

  1. 人员列表(后来EFCore中人员的IQueryable)
  2. 从人到他们的工作的字符串 "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.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]]])''

我的完整代码如下所示:

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