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

我已经坚持这个问题一个星期,没有找到解决方案。

我有一个像下面的POCO:

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

我想知道在特定日期范围内(按月或年分组)期刊的数量由AuthorName或Category计算。

在我将查询对象发送到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 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() }) }); 

按月或年,AuthorName或Category分组的条件将通过两个字符串类型方法参数传递。 我不知道的是如何在GroupBy()方法中使用“Magic String”参数。 经过一些谷歌搜索,似乎我不能通过传递像“AuthorName”这样的魔术字符串来分组数据。 我应该做的是构建一个表达式树并将其传递给GroupBy()方法。

任何解决方案或建议都表示赞赏。

哦,这看起来像一个有趣的问题:)

首先,让我们设置我们的虚假源码,因为我没有你的数据库方便:

 // 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> GetThingToGroupByWith( string[] someMagicStringNames, ????) 

它具有与(伪代码)大致相同的function:

 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(); var fields = groupByNames .Select(name => sourceType.GetField(name)) .Cast(); 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提供它想要的东西?

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

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

我们知道我们需要“新手”我们的新动态类型之一……

 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() .ToArray(); 

Oof ……看起来很讨厌,但我们还没有完成 – 所以我们需要为我们通过Expression树创建的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() .ToArray(); var fetcher = Expression.Lambda>( 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> Epinephelinae( this IEnumerable 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(); var fields = groupByNames .Select(name => sourceType.GetField(name)) .Cast(); 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() .ToArray(); var fetcher = Expression.Lambda>( 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”,但是有了一点想象力,你可以扩展这个想法来处理任何自由forms的查询。