动态向Lambda表达式中添加GroupBy

3
好的,我承认我还没有完全理解lambda表达式和LINQ表达式树;我所做的很多事情都是剪切和粘贴,看看哪些有效。我查看了很多文档,但我仍然没有找到我的“恍然大悟”的时刻。
话虽如此...
我试图动态添加一个GroupBy表达式到我的Linq表达式中。我遵循了这里的问题: Need help creating Linq.Expression to Enumerable.GroupBy 并尝试实现我在那里看到的内容。
首先,我有用于我的数据库的实体类和一个名为ObjCurLocViewNormalized的表。
我有一个方法来进行初始调用,
public IQueryable<ObjCurLocViewNormalized> getLocations()
{
    IQueryable<ObjCurLocViewNormalized> res = (from loc in tms.ObjCurLocViewNormalized
                               select loc);
    return res;
}

所以我可以调用:

IQueryable<MetAmericanLinqDataModel.ObjCurLocViewNormalized> locations = american.getLocations();

到目前为止还没有问题。

现在,我想通过以下方式对任意列进行分组:

var grouped = locations.addGroupBy(childLocationFieldName);

现在,我有一个方法:

static public System.Linq.IQueryable<System.Linq.IGrouping<string, TResult>> addGroupBy<TResult>(this IQueryable<TResult> query, string columnName)
{

    var providerType = query.Provider.GetType();
    // Find the specific type parameter (the T in IQueryable<T>)
    var iqueryableT = providerType.FindInterfaces((ty, obj) => ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(IQueryable<>), null).FirstOrDefault();
    var tableType = iqueryableT.GetGenericArguments()[0];
    var tableName = tableType.Name;

    var data = Expression.Parameter(iqueryableT, "query");
    var arg = Expression.Parameter(tableType, tableName);
    var nameProperty = Expression.PropertyOrField(arg, columnName);
    var lambda = Expression.Lambda<Func<TResult, string>>(nameProperty, arg);

    var expression = Expression.Call(typeof(Enumerable), 
                                    "GroupBy", 
                                    new Type[] { tableType, typeof(string) },
                                    data, 
                                    lambda);
    var predicate = Expression.Lambda<Func<TResult, String>>(expression, arg); // this is the line that produces the error I describe below
    var result = query.GroupBy(predicate).AsQueryable();
    return result;
}

所有的代码都编译成功了,但是运行时出现了错误:

。这个错误该怎么解决呢?
System.ArgumentException: Expression of type 'System.Collections.Generic.IEnumerable`1[System.Linq.IGrouping`2[System.String,MetAmericanLinqDataModel.ObjCurLocViewNormalized]]' cannot be used for return type 'System.String'

错误来自于这一行:

 var predicate = Expression.Lambda<Func<TResult, String>>(expression, arg);

我正在复制和调整我在动态添加Where子句到表达式方面的成功工作中使用的代码。所以我有点盲目地尝试。
如果有人能帮忙解决这个问题,显然发布完整的可工作代码并为我思考所有内容将是很棒的 :), 但如果你只能说明这是错误的原因,或者如何理解这些概念,那就太好了。如果你能指出可以真正帮助我填补lambda表达式基础和构建动态表达式树之间差距的文档,那就太好了。我的知识中存在明显的漏洞,但我认为这些信息可能对其他人有用。
谢谢大家花时间阅读,当然,如果我在其他地方找到答案,我会在这里发布的。
再次感谢。

1
为什么你的 predicate 行使用了 Func<TResult, string>?你的表达式返回的是一个 IGrouping,而不是一个 string - Kirk Woll
谢谢,柯克。我尝试了将该行更改为:var predicate = Expression.Lambda<Func<TResult, IGrouping<String, TResult>>>(expression, arg); 但现在我得到了编译错误:Error 5 Cannot implicitly convert type 'System.Linq.IQueryable<System.Linq.IGrouping<System.Linq.IGrouping<string,TResult>,TResult>>' to 'System.Linq.IQueryable<System.Linq.IGrouping<string,TResult>>'. An explicit conversion exists (are you missing a cast?)。这是你告诉我的错误写法吗? - donundeen
主要问题在于您混淆了对.GroupBy的调用和传递给的lambda谓词。目前没有时间进一步回答,但如果一个小时后这个问题仍未得到解答,我会更加彻底地尝试回答。 - Kirk Woll
啊,那个错误是与返回类型不匹配有关的。但我不认为我想改变返回类型,是吗? - donundeen
2个回答

2
解决方案应该相当简单:
public static IQueryable<IGrouping<TColumn, T>> DynamicGroupBy<T, TColumn>(
    IQueryable<T> source, string column)
{
    PropertyInfo columnProperty = typeof(T).GetProperty(column);
    var sourceParm = Expression.Parameter(typeof(T), "x");
    var propertyReference = Expression.Property(sourceParm, columnProperty);
    var groupBySelector = Expression.Lambda<Func<T, TColumn>>(propertyReference, sourceParm);

    return source.GroupBy(groupBySelector);
}

假设有这样一个示例类:
public class TestClass
{
    public string TestProperty { get; set; }
}

您可以这样调用它:
var list = new List<TestClass>();
var queryable = list.AsQueryable();
DynamicGroupBy<TestClass, string>(queryable, "TestProperty");

看起来你的答案和我的类似,但是现在更加完整,并且进行了代码重构 =) - Restuta
嗯,看起来很有前途,但是“方法'Parameter'的重载不接受1个参数。”所以它不喜欢这一行:var sourceParm = Expression.Parameter(typeof(T)); 我使用的是3.5版本。这是.NET 4.0的事情吗?我很快就要升级了。 - donundeen
@donundeen,是的,那是 .Net 4 的东西,但这并不重要。只需将任何您认为有助于标识参数的字符串作为第二个参数传递,或者只是 "x"。这无关紧要。 :) - Kirk Woll

0

让它工作所需的所有步骤如下:

    static public IQueryable<IGrouping<TValue, TResult>> addGroupBy<TValue, TResult>(
                this IQueryable<TResult> query, string columnName)
            {
            var providerType = query.Provider.GetType();

            // Find the specific type parameter (the T in IQueryable<T>)
            const object EmptyfilterCriteria = null;
            var iqueryableT = providerType
                .FindInterfaces((ty, obj) => ty.IsGenericType && ty.GetGenericTypeDefinition() == typeof(IQueryable<>), EmptyfilterCriteria)
                .FirstOrDefault();
            Type tableType = iqueryableT.GetGenericArguments()[0];
            string tableName = tableType.Name;

            ParameterExpression data = Expression.Parameter(iqueryableT, "query");
            ParameterExpression arg = Expression.Parameter(tableType, tableName);
            MemberExpression nameProperty = Expression.PropertyOrField(arg, columnName);
            Expression<Func<TResult, TValue>> lambda = Expression.Lambda<Func<TResult, TValue>>(nameProperty, arg);
            //here you already have delegate in the form of "TResult => TResult.columnName"
            return query.GroupBy(lambda);

        /*var expression = Expression.Call(typeof(Enumerable), 
                                        "GroupBy", 
                                        new Type[] { tableType, typeof(string) },
                                        data, 
                                        lambda);
        var predicate = Expression.Lambda<Func<TResult, String>>(expression, arg); // this is the line that produces the error I describe below
        var result = query.GroupBy(predicate).AsQueryable();
        return result;*/
    }

接下来你可以这样调用你的表达式:

var grouped = locations.addGroupBy<string, ObjCurLocViewNormalized>(childLocationFieldName);

第一个通用参数 "string" 用于明确说明你正在分组的元素类型。例如,你可以按 "int" 字段进行分组,方法调用将如下所示:

var grouped = locations.addGroupBy<int, ObjCurLocViewNormalized>(someFieldNameWithTheTypeOfInt);

编辑 只是为了按照你的方式完成这个解决方案:

 //return query.GroupBy(lambda);
    MethodCallExpression expression = Expression.Call(typeof (Enumerable),
                                                      "GroupBy",
                                                      new[] { typeof(TResult), typeof(TValue) },
                                                      data,
                                                      lambda);

    var result = Expression.Lambda(expression, data).Compile().DynamicInvoke(query);
    return ((IEnumerable<IGrouping<TValue, TResult>>)result).AsQueryable();

为什么在注释“//查找特定类型参数(IQueryable<T>中的T)”之后还有那个奇怪的代码呢?你肯定已经在'TResult'中拥有了那个信息,不是吗? - Kirk Woll
我更喜欢不要触碰原始代码,因为问题是关于其他事情的。当然,代码可以改进。 - Restuta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接