哦,这看起来像是一个有趣的问题 :)
首先,让我们设置我们的伪源代码,因为我没有你的数据库方便:
// 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);
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
{
public static Expression<Func<T, object>> Epinephelinae<T>(
this IEnumerable<T> source,
string [] groupByNames)
{
var sourceType = typeof(T);
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();
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;
}
}
现在,使用它的方法如下:
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,
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”)的灵活性,但是只要稍加想象力,您可能可以扩展这个想法以适用于任何自由格式查询。
IQueryable
之上的,因此它并不取代像LINQ to Entities这样的库,实际上需要类似的库才能工作。 - svick