[英]Construct a LINQ GroupBy query using expression trees
我已經堅持這個問題一個星期,沒有找到解決方案。
我有一個像下面的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<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() })
});
按月或年分組的條件,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<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提供它想要的東西?
首先,讓我們為我們將返回的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<MemberBinding>()
.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<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”,但是有了一點想象力,你可以擴展這個想法來處理任何自由形式的查詢。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.