首页 > 解决方案 > 使用 Expression.Call 调用 SelectMany - 参数错误

问题描述

我想通过字符串来处理关系。

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

我的方法的输入:

  1. 人员列表(后来 EFCore 中的人员 IQueryable)
  2. 字符串“Work.Locations”从人到他们的工作

所以我必须用表达式调用: 1. 在人员列表上列出一个 list.Select(x => x.Work) 2. 在该结果上列出一个 list.SelectMany(x => x.Locations)

在 SelectMany 方法上进行 Expression.Call 时出现错误(在 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});

我得到这个例外:

System.ArgumentException:'System.Func 2[GenericResourceLoading.Data.Work,System.Collections.Generic.ICollection1 [GenericResourceLoading.Data.Location]] 类型的表达式不能用于'System.Linq.Expressions.Expression 1[System.Func2 [GenericResourceLoading.Data.Work,System.Collections.Generic类型的参数.IEnumerable 1[GenericResourceLoading.Data.Location]]]' of method 'System.Linq.IQueryable1[GenericResourceLoading.Data.Location] SelectMany[Work,Location](System.Linq.IQueryable 1[GenericResourceLoading.Data.Work], System.Linq.Expressions.Expression1[System.Func 2[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;
    }

标签: c#linqexpression-trees

解决方案


它失败了,因为SelectMany<TSource, TResult>方法期望

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

当你经过时

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

这些是不一样的,后者不能仅仅因为Expression<TDelegate>是一个而转换为前者,而类是不变的。

使用您的代码,预期的 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>不是预期的),因此必须将其更改为:LocationToList

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

一般来说,您可以删除actualType变量并始终IQueryable.ElementType用于该目的。

最后作为奖励,无需手动查找泛型方法定义。Expression.Call有一个特殊的重载,它允许您通过名称轻松地“调用”静态泛型(而不仅仅是)方法。例如,SelectMany“呼叫”将是这样的:

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

推荐阅读