Ooh, to wygląda jak zabawy problemu :)
Więc najpierw ustalmy nasze faux-source, ponieważ nie mam pod ręką swój DB:
// 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 };
OK tak teraz mamy komplet danych do pracy, spójrzmy na to, co chcemy ... chcemy coś z „kształtu”, takich jak:
public Expression<Func<Journal, ????>> GetThingToGroupByWith(
string[] someMagicStringNames,
????)
który ma mniej więcej takie same funkcjonalne ity jako (pod pseudo kodem):
GroupBy(x => new { x.magicStringNames })
Rozłóżmy to po jednym kawałku na raz. Po pierwsze, jak do cholery robimy to dynamicznie?
x => new { ... }
Kompilator robi magię dla nas normalnie - co robi jest zdefiniowanie nowego Type
, a my możemy zrobić to samo:
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();
Więc co zrobiliśmy tutaj jest zdefiniowanie niestandardowego typ throwaway, który ma jedno pole dla każdej nazwy, którą przekazujemy, która jest tego samego typu co (właściwość lub pole) w typie źródła. Miły!
Teraz, jak możemy dać LINQ to, czego chce?
Najpierw założyć „wejście” dla func wrócimy:
// Create and return an expression that maps T => dynamic type
var sourceItem = Expression.Parameter(sourceType, "item");
Wiemy musimy „nowy up” jeden z naszych nowych typów dynamicznych ...
Expression.New(dynamicType.GetConstructor(Type.EmptyTypes))
I musimy go zainicjować z wartościami, pochodzących z tego parametru ...
Expression.MemberInit(
Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)),
bindings),
Ale co do cholery mamy zamiar użyć do bindings
? Hmm ... no, chcemy coś, co wiąże się z odpowiednich właściwości/pól w rodzaju źródła, ale remaps je do naszych dynamicType
pól ...
var bindings = dynamicType
.GetFields()
.Select(p =>
Expression.Bind(
p,
Expression.PropertyOrField(
sourceItem,
p.Name)))
.OfType<MemberBinding>()
.ToArray();
oof ... paskudny wygląd, ale jesteśmy wciąż tego nie robimy - więc musimy zadeklarować typ zwrotu dla Func
, który tworzymy poprzez drzewa wyrażeń ... jeśli masz wątpliwości, użyj object
!
Expression.Convert(expr, typeof(object))
I wreszcie będziemy powiązać to do naszego „parametru wejściowego” poprzez Lambda
, dzięki czemu cały stos:
// 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);
Dla łatwości obsługi, niech owinąć cały bałagan jako rozszerzenie metoda, więc teraz mamy:
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;
}
}
teraz, aby go użyć:
// 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);
W tym rozwiązaniu brakuje elastyczności "instrukcji zagnieżdżonych", takich jak "CreatedDate.Month", ale przy odrobinie wyobraźni można rozszerzyć ten pomysł, aby działał z dowolnym zapytaniem swobodnym.
Czy obejrzałeś dynamiczny LINQ? – svick
@svick Jeśli mam inną opcję obok LINQ do Entity, wybiorę Dapper StackOverflow zamiast dynamicznego LINQ –
Dynamiczny LINQ działa na górze 'IQueryable', więc nie zastępuje bibliotek takich jak LINQ do Entities, w rzeczywistości wymaga pewnej biblioteki tak to działa. – svick