2013-03-08 9 views
6

Mi sono bloccato su questo problema per una settimana e non è stata trovata alcuna soluzione.Costruire un gruppo LINQBy query utilizzando gli alberi di espressione

Ho un POCO come di seguito:

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

Voglio sapere nel corso di un arco di data specifica (diviso per mesi o anni) la quantità di riviste contare da un AuthorName o di una categoria.

Dopo mando l'oggetto queryed a JSON serializzatore quindi generato dati JSON come qui di seguito (solo utilizzando JSON per dimostrare i dati che voglio ottenere, come serializzatore un oggetto a un JSON non è il mio problema)

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

O

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

Quello che so è quello di scrivere una query LINQ in "metodo modo", come:

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

Le condizioni raggruppate per mesi o anni, AuthorName o Categoria verranno passate da due parametri del metodo di tipo stringa. Quello che non so è come usare i parametri "Magic String" in un metodo GroupBy(). Dopo alcune ricerche su google, sembra che non sia possibile raggruppare i dati passando una stringa magica come "AuthorName". Quello che dovrei fare è costruire un albero di espressioni e passarlo al metodo GroupBy().

Qualsiasi soluzione o suggerimento è gradita.

+0

Hai guardato il LINQ dinamico? – svick

+0

@svick Se ho un'altra opzione accanto a LINQ su Entity, sceglierò Dapper di StackOverflow anziché LINQ dinamico –

+0

LINQ dinamico funziona su "IQueryable", quindi non sostituisce librerie come LINQ alle Entità, in realtà richiede alcune librerie come quello di lavorare. – svick

risposta

22

Ooh, questo sembra un divertente problema :)

Quindi, prima, cerchiamo di impostare il nostro finto-source, dal momento che non ho a portata di mano il vostro 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, così ora abbiamo un insieme di dati con cui lavorare, diamo un'occhiata a quello che vogliamo ... vogliamo qualcosa con una "forma", come:

public Expression<Func<Journal, ????>> GetThingToGroupByWith(
    string[] someMagicStringNames, 
    ????) 

che ha all'incirca la stessa funzionalità come (in pseudo codice):

GroupBy(x => new { x.magicStringNames }) 

Analizziamolo un pezzo alla volta. Innanzitutto, come diamine facciamo questo dinamicamente?

x => new { ... } 

Il compilatore fa la magia per noi normalmente - ciò che fa è definire un nuovo Type, e possiamo fare la stessa cosa:

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

Quindi quello che abbiamo fatto qui è definire un personalizzato , tipo throwaway che ha un campo per ogni nome che passiamo, che è dello stesso tipo di (proprietà o campo) sul tipo di sorgente. Bello!

Ora, come possiamo fornire a LINQ ciò che desidera?

prima cosa, diamo istituito un "input" per la func torneremo:

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

Sappiamo avremo bisogno di "new up" uno dei nostri nuovi tipi dinamici ...

Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)) 

E avremo bisogno di inizializzare con i valori provenienti da quel parametro ...

Expression.MemberInit(
    Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), 
    bindings), 

ma che diamine abbiamo intenzione di usare per bindings? Hmm ... beh, vogliamo qualcosa che si lega alle corrispondenti proprietà/campi nel tipo di origine, ma li rimappa ai nostri dynamicType campi ...

var bindings = dynamicType 
     .GetFields() 
     .Select(p => 
      Expression.Bind(
       p, 
       Expression.PropertyOrField(
        sourceItem, 
        p.Name))) 
     .OfType<MemberBinding>() 
     .ToArray(); 

Oof ... brutto cercando, ma siamo ancora non fatto - quindi dobbiamo dichiarare un tipo di ritorno per lo Func che stiamo creando tramite gli alberi di espressione ... in caso di dubbio, usa object!

Expression.Convert(expr, typeof(object)) 

E, infine, ci leghiamo questo alla nostra "parametro di ingresso" via Lambda, rendendo l'intero stack:

// 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);     

Per facilità d'uso, cerchiamo di avvolgere l'intero rovinare come estensione metodo, così ora abbiamo:

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

ora, per utilizzarlo:

// 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); 

Questa soluzione non ha la flessibilità di "istruzioni nidificate", come "CreatedDate.Month", ma con un po 'di immaginazione, è possibile estendere questa idea per lavorare con qualsiasi query a mano libera.

+0

+1 per una spiegazione approfondita + hardwork. –

+0

Vorrei poter dare +10 per questo – Jeremy

+0

grazie a @JerKimball, spiegazione molto buona e interessante. –

Problemi correlati