Costruire un gruppo LINQBella query utilizzando gli alberi di espressione

c# expression-trees linq

Domanda

Sono rimasto bloccato su questo problema per una settimana e non è stata trovata alcuna soluzione.

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 durante un intervallo di date specifico (raggruppato per mesi o anni) la quantità di riviste conta da un AuthorName o una categoria.

Dopo aver inviato l'oggetto interrogato al serializzatore JSON, ho generato i dati JSON come di seguito (usando solo JSON per dimostrare i dati che voglio ottenere, come serializzare un oggetto su un JSON non è un 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 è scrivere una query LINQ in "metodo" 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.

Risposta accettata

Ooh, questo sembra un problema divertente :)

Quindi, per prima cosa, impostiamo la nostra fonte di errore, dal momento che non ho a portata di mano il tuo 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, ora abbiamo una serie di dati con cui lavorare, diamo un'occhiata a ciò che vogliamo ... vogliamo qualcosa con una "forma" come:

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

Ha all'incirca la stessa funzionalità di (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 lo stesso:

    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 tipo di scambio, personalizzato che ha un campo per ogni nome che passiamo, che è lo stesso tipo di (proprietà o campo) sul tipo di origine. Bello!

Ora, come possiamo dare a LINQ ciò che vuole?

Per prima cosa, impostiamo un "input" per la funzione che restituiremo:

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

Sappiamo che avremo bisogno di "rinnovare" uno dei nostri nuovi tipi dinamici ...

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

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

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

Ma che diamine useremo per le bindings ? Hmm ... beh, vogliamo qualcosa che si dynamicType ai corrispondenti campi / proprietà nel tipo sorgente, ma li dynamicType nostri campi dynamicType ...

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

Oof ... brutto aspetto, ma non abbiamo ancora finito - quindi abbiamo bisogno di dichiarare un tipo di ritorno per il Func che stiamo creando tramite gli alberi di espressione ... quando in dubbio, usa l' object !

Expression.Convert( expr, typeof(object))

E infine, legheremo questo al nostro "parametro di input" tramite 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, avvolgiamo l'intero casino come metodo di estensione, quindi 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 usarlo:

// 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à delle "istruzioni nidificate", come "CreatedDate.Month", ma con un po 'di immaginazione potresti estendere questa idea per lavorare con qualsiasi query a mano libera.



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché