2013-03-08 6 views
6

Utknąłem na ten problem przez tydzień i nie znaleziono rozwiązania.Skonstruuj kwerendę GroupBy LINQ przy użyciu drzew wyrażeń

mam poco jak poniżej:

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

Chcę wiedzieć podczas konkretnej daty rozpiętości (pogrupowane według miesięcy lub lat) ilość czasopism liczyć przez AUTHORNAME lub kategorii.

Po wysłać queryed obiekt do JSON serializatora następnie generowane danych JSON jak poniżej (tylko za pomocą JSON do wykazania danych chcę uzyskać, jak serializer obiektu do JSON nie jest mój problem)

data: { 
    '201301': { 
     'Alex': 10, 
     'James': 20 
    }, 
    '201302': { 
     'Alex': 1, 
     'Jessica': 9 
    } 
} 

LUB

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

Co wiem, to napisać kwerendę LINQ w "metoda sposób" jak:

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() }) 
    }); 

Warunki zgrupowane według miesięcy lub lat, nazwa_dokumentu lub kategoria będą przekazywane za pomocą dwóch parametrów metody typu łańcucha. Nie wiem, jak używać parametrów "Magic String" w metodzie GroupBy(). Po pewnym wylogowaniu wydaje się, że nie mogę grupować danych, przekazując magiczny ciąg znaków, taki jak "AuthorName". To, co powinienem zrobić, to zbudować drzewo wyrażeń i przekazać je do metody GroupBy().

Każde rozwiązanie lub sugestia jest doceniane.

+0

Czy obejrzałeś dynamiczny LINQ? – svick

+0

@svick Jeśli mam inną opcję obok LINQ do Entity, wybiorę Dapper StackOverflow zamiast dynamicznego LINQ –

+0

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

Odpowiedz

22

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.

+0

+1 za dokładne objaśnienie + hardwork. –

+0

Chciałbym móc dać +10 za to – Jeremy

+0

dzięki @JerKimball, bardzo dobre i interesujące wyjaśnienie. –