Erstellen Sie eine LINQ GroupBy-Abfrage mithilfe von Ausdrucksbäumen

c# expression-trees linq

Frage

Ich habe eine Woche lang an diesem Problem festgehalten und keine Lösung gefunden.

Ich habe ein POCO wie unten:

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

Ich möchte während eines bestimmten Zeitraums (nach Monaten oder Jahren gruppiert) die Anzahl der Zeitschriften nach Autorenname oder Kategorie ermitteln.

Nachdem ich das abgefragte Objekt an den JSON-Serialisierer gesendet habe, generierte ich JSON-Daten wie unten beschrieben (nur mit JSON, um die Daten zu demonstrieren, die ich bekommen möchte, wie man ein Objekt zu einem JSON serialisiert ist nicht mein Problem)

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

ODER

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

Was ich weiß, ist eine LINQ-Abfrage in "method way" wie:

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

Die Bedingungen, die nach Monaten oder Jahren gruppiert sind, Autorenname oder Kategorie, werden von zwei Methodenmethoden des Stringtyps übergeben. Was ich nicht weiß ist, wie man "Magic String" Parameter in einer GroupBy () Methode verwendet. Nach einigem googeln scheint es, dass ich Daten nicht gruppieren kann, indem ich eine magische Zeichenfolge wie "Autorenname" übergebe. Ich sollte einen Ausdrucksbaum erstellen und ihn an die GroupBy () -Methode übergeben.

Jede Lösung oder Vorschlag ist zu schätzen.

Akzeptierte Antwort

Ooh, das sieht nach einem lustigen Problem aus :)

Also, lasst uns zuerst unsere falsche Quelle einrichten, da ich deine DB nicht zur Hand habe:

// 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, jetzt haben wir eine Reihe von Daten, mit denen wir arbeiten können, schauen wir uns an, was wir wollen ... wir wollen etwas mit einer "Form" wie:

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

Das hat ungefähr die gleiche Funktionalität wie (im Pseudocode):

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

Lassen Sie uns es Stück für Stück sezieren. Erstens, wie zum Teufel machen wir das dynamisch?

x => new { ... }

Der Compiler macht normalerweise die Magie für uns - was er tut, ist einen neuen Type definieren, und wir können dasselbe tun:

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

Also haben wir hier einen benutzerdefinierten Wegwerftyp definiert, der für jeden Namen, den wir übergeben, ein Feld hat, das den gleichen Typ wie der (entweder Eigenschaft oder Feld) des Quelltyps hat. Nett!

Wie geben wir LINQ, was es will?

Zuerst richten wir eine "Eingabe" für die Funktion ein, die wir zurückgeben werden:

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

Wir wissen, dass wir einen unserer neuen dynamischen Typen neu erstellen müssen ...

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

Und wir müssen es mit den Werten initialisieren, die von diesem Parameter kommen ...

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

Aber was zum Teufel werden wir für bindings ? Hmm ... nun, wir wollen etwas, das an die entsprechenden Eigenschaften / Felder des Quelltyps bindet, aber sie an unsere dynamicType Felder dynamicType ...

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

Oof ... fieses Suchen, aber wir sind immer noch nicht fertig - also müssen wir einen Rückgabetyp für den Func deklarieren, den wir über Expression Trees erstellen ... im Zweifelsfall object !

Expression.Convert( expr, typeof(object))

Und schließlich binden wir dies über Lambda an unseren "Eingabeparameter" und machen den ganzen 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);                

Um die Verwendung zu erleichtern, lassen Sie uns die ganze Unordnung als eine Erweiterungsmethode behandeln, also haben wir jetzt:

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

Jetzt, um es zu benutzen:

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

Dieser Lösung fehlt die Flexibilität von "verschachtelten Aussagen", wie "CreatedDate.Month", aber mit ein wenig Fantasie könnte man diese Idee auf beliebige Freeform-Abfragen erweitern.



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum