使用表达式树构建LINQ GroupBy查询

8

我已经卡在这个问题上一个星期了,但是没有找到解决方案。

我有一个类似下面的POCO:

public class Journal {
    public int Id { get; set; }
    public string AuthorName { get; set; }
    public string Category { get; set; }
    public DateTime CreatedAt { get; set; }
}

我希望知道在特定日期范围内(按月或年分组),某个作者姓名或分类别的期刊计数量。
当我将查询对象发送到JSON序列化程序后,生成的JSON数据如下所示(只是使用JSON演示我想要获取的数据,如何将对象序列化为JSON不是我的问题)。
data: {
    '201301': {
        'Alex': 10,
        'James': 20
    },
    '201302': {
        'Alex': 1,
        'Jessica': 9
    }
}

或者

data: {
    '2012': {
         'C#': 230
         'VB.NET': 120,
         'LINQ': 97
     },
     '2013': {
         'C#': 115
         'VB.NET': 29,
         'LINQ': 36
     }
}

我知道的是以“方法方式”编写LINQ查询,例如:

IQueryable<Journal> query = db.GroupBy(x=> new 
    {
        Year = key.CreatedAt.Year,
        Month = key.CreatedAt.Month
    }, prj => prj.AuthorName)
    .Select(data => new {
        Key = data.Key.Year * 100 + data.Key.Month, // very ugly code, I know
        Details = data.GroupBy(y => y).Select(z => new { z.Key, Count = z.Count() })
    });

月份、年份、作者姓名或类别等分组条件将通过两个字符串类型的方法参数传递。我不知道的是如何在GroupBy()方法中使用"魔术字符串"参数。经过一些搜索,似乎我不能通过传递像"AuthorName"这样的魔法字符串来分组数据。我应该做的是构建一个表达式树并将其传递给GroupBy()方法。
非常感谢任何解决方案或建议。

你看过动态LINQ吗? - svick
如果我除了LINQ to Entity之外还有其他选择,我会选择StackOverflow的Dapper而不是动态LINQ。 - Alex Chen
动态LINQ是建立在IQueryable之上的,因此它并不取代像LINQ to Entities这样的库,实际上需要类似的库才能工作。 - svick
1个回答

29
哦,这看起来像是一个有趣的问题 :)
首先,让我们设置我们的伪源代码,因为我没有你的数据库方便:
// SETUP: fake up a data source
var folks = new[]{"Alex", "James", "Jessica"};
var cats = new[]{"C#", "VB.NET", "LINQ"};
var r = new Random();
var entryCount = 100;
var entries = 
    from i in Enumerable.Range(0, entryCount)
    let id = r.Next(0, 999999)
    let person = folks[r.Next(0, folks.Length)]
    let category = cats[r.Next(0, cats.Length)]
    let date = DateTime.Now.AddDays(r.Next(0, 100) - 50)
    select new Journal() { 
        Id = id, 
        AuthorName = person, 
        Category = category, 
        CreatedAt = date };    

好的,现在我们有一组数据要处理,让我们看看我们想要什么... 我们想要的是一个类似于“形状”的东西:

public Expression<Func<Journal, ????>> GetThingToGroupByWith(
    string[] someMagicStringNames, 
    ????)

这大致具有与以下伪代码相同的功能:

GroupBy(x => new { x.magicStringNames })

让我们一步步来分解它。首先,我们如何动态地做到这一点?

x => new { ... }

编译器通常会为我们做这个魔术 - 它定义了一个新的“Type”,我们也可以这样做:
    var sourceType = typeof(Journal);

    // define a dynamic type (read: anonymous type) for our needs
    var dynAsm = AppDomain
        .CurrentDomain
        .DefineDynamicAssembly(
            new AssemblyName(Guid.NewGuid().ToString()), 
            AssemblyBuilderAccess.Run);
    var dynMod = dynAsm
         .DefineDynamicModule(Guid.NewGuid().ToString());
    var typeBuilder = dynMod
         .DefineType(Guid.NewGuid().ToString());
    var properties = groupByNames
        .Select(name => sourceType.GetProperty(name))
        .Cast<MemberInfo>();
    var fields = groupByNames
        .Select(name => sourceType.GetField(name))
        .Cast<MemberInfo>();
    var propFields = properties
        .Concat(fields)
        .Where(pf => pf != null);
    foreach (var propField in propFields)
    {        
        typeBuilder.DefineField(
            propField.Name, 
            propField.MemberType == MemberTypes.Field 
                ? (propField as FieldInfo).FieldType 
                : (propField as PropertyInfo).PropertyType, 
            FieldAttributes.Public);
    }
    var dynamicType = typeBuilder.CreateType();

我们所做的是定义了一个自定义的临时类型,每个名称都有一个字段,该字段与源类型上的属性或字段相同。很好!

现在我们如何满足LINQ的要求呢?

首先,让我们为我们将返回的函数设置一个“输入”:

// Create and return an expression that maps T => dynamic type
var sourceItem = Expression.Parameter(sourceType, "item");

我们知道我们需要“new up”其中一个新的动态类型...
Expression.New(dynamicType.GetConstructor(Type.EmptyTypes))

我们需要使用来自该参数的值对其进行初始化...

Expression.MemberInit(
    Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)),
    bindings), 

但我们将用什么来进行 bindings ?嗯...好吧,我们希望有些东西能够绑定到源类型中相应的属性/字段,但将它们重新映射到我们的dynamicType字段...

    var bindings = dynamicType
        .GetFields()
        .Select(p => 
            Expression.Bind(
                 p, 
                 Expression.PropertyOrField(
                     sourceItem, 
                     p.Name)))
        .OfType<MemberBinding>()
        .ToArray();

哎呀……看起来很糟糕,但我们还没有完成——因此,我们需要为通过表达式树创建的 Func 声明返回类型……当不确定时,使用 object

Expression.Convert( expr, typeof(object))

最后,我们将通过Lambda将其绑定到我们的“输入参数”,使整个堆栈完成:

    // Create and return an expression that maps T => dynamic type
    var sourceItem = Expression.Parameter(sourceType, "item");
    var bindings = dynamicType
        .GetFields()
        .Select(p => Expression.Bind(p, Expression.PropertyOrField(sourceItem, p.Name)))
        .OfType<MemberBinding>()
        .ToArray();

    var fetcher = Expression.Lambda<Func<T, object>>(
        Expression.Convert(
            Expression.MemberInit(
                Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)),
                bindings), 
            typeof(object)),
        sourceItem);                

为了方便使用,让我们将整个混乱的部分包装成一个扩展方法,现在我们有:
public static class Ext
{
    // Science Fact: the "Grouper" (as in the Fish) is classified as:
    //   Perciformes Serranidae Epinephelinae
    public static Expression<Func<T, object>> Epinephelinae<T>(
         this IEnumerable<T> source, 
         string [] groupByNames)
    {
        var sourceType = typeof(T);
    // define a dynamic type (read: anonymous type) for our needs
    var dynAsm = AppDomain
        .CurrentDomain
        .DefineDynamicAssembly(
            new AssemblyName(Guid.NewGuid().ToString()), 
            AssemblyBuilderAccess.Run);
    var dynMod = dynAsm
         .DefineDynamicModule(Guid.NewGuid().ToString());
    var typeBuilder = dynMod
         .DefineType(Guid.NewGuid().ToString());
    var properties = groupByNames
        .Select(name => sourceType.GetProperty(name))
        .Cast<MemberInfo>();
    var fields = groupByNames
        .Select(name => sourceType.GetField(name))
        .Cast<MemberInfo>();
    var propFields = properties
        .Concat(fields)
        .Where(pf => pf != null);
    foreach (var propField in propFields)
    {        
        typeBuilder.DefineField(
            propField.Name, 
            propField.MemberType == MemberTypes.Field 
                ? (propField as FieldInfo).FieldType 
                : (propField as PropertyInfo).PropertyType, 
            FieldAttributes.Public);
    }
    var dynamicType = typeBuilder.CreateType();

        // Create and return an expression that maps T => dynamic type
        var sourceItem = Expression.Parameter(sourceType, "item");
        var bindings = dynamicType
            .GetFields()
            .Select(p => Expression.Bind(
                    p, 
                    Expression.PropertyOrField(sourceItem, p.Name)))
            .OfType<MemberBinding>()
            .ToArray();

        var fetcher = Expression.Lambda<Func<T, object>>(
            Expression.Convert(
                Expression.MemberInit(
                    Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)),
                    bindings), 
                typeof(object)),
            sourceItem);                
        return fetcher;
    }
}

现在,使用它的方法如下:
// What you had originally (hand-tooled query)
var db = entries.AsQueryable();
var query = db.GroupBy(x => new 
    {
        Year = x.CreatedAt.Year,
        Month = x.CreatedAt.Month
    }, prj => prj.AuthorName)
    .Select(data => new {
        Key = data.Key.Year * 100 + data.Key.Month, // very ugly code, I know
        Details = data.GroupBy(y => y).Select(z => new { z.Key, Count = z.Count() })
    });    

var func = db.Epinephelinae(new[]{"CreatedAt", "AuthorName"});
var dquery = db.GroupBy(func, prj => prj.AuthorName);

这个解决方案缺乏“嵌套语句”(如“CreatedDate.Month”)的灵活性,但是只要稍加想象力,您可能可以扩展这个想法以适用于任何自由格式查询。

1
希望我能给这个 +10。 - Jeremy
这对我不起作用。看起来缺少动态类型的Equals和GetHashCode重写,我不确定如何在动态定义完整方法。我知道这是相当老的答案。现在是否有更好的方法来实现相同的功能? - Viru

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