Construire une requête LINQ GroupBy en utilisant des arbres d'expression

c# expression-trees linq

Question

Je suis bloqué sur ce problème depuis une semaine et aucune solution trouvée.

J'ai un POCO comme ci-dessous:

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

Je souhaite connaître, pendant une période donnée (groupée par mois ou par années), le nombre de revues comptabilisées par un nom d'auteur ou une catégorie.

Après avoir envoyé l'objet interrogé au sérialiseur JSON, puis généré les données JSON comme indiqué ci-dessous (utiliser JSON uniquement pour illustrer les données que je souhaite obtenir, comment sérialiser un objet sur un JSON n'est pas mon problème)

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

OU

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

Ce que je sais, c’est d’écrire une requête LINQ de la façon suivante:

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

Les conditions regroupées par mois ou par années, AuthorName ou Category seront transmises par deux paramètres de méthode de type chaîne. Ce que je ne sais pas, c'est comment utiliser les paramètres "Magic String" dans une méthode GroupBy (). Après quelques recherches sur Google, il semble que je ne puisse pas grouper les données en transmettant une chaîne magique telle que "AuthorName". Ce que je devrais faire, c'est construire un arbre d'expression et le transmettre à la méthode GroupBy ().

Toute solution ou suggestion est appréciée.

Réponse acceptée

Ooh, cela ressemble à un problème amusant :)

Commençons donc par configurer notre fausse source, car je ne dispose pas de votre base de données:

// 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, alors maintenant nous avons un ensemble de données à utiliser, regardons ce que nous voulons ... nous voulons quelque chose avec une "forme" comme:

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

Cela a à peu près les mêmes fonctionnalités que (en pseudo-code):

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

Disons le disséquer un morceau à la fois. Premièrement, comment diable faisons-nous cela de manière dynamique?

x => new { ... }

Le compilateur fait normalement la magie pour nous - il définit un nouveau Type , et nous pouvons faire de même:

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

Nous avons donc défini ici un type personnalisé, jetable, qui contient un champ pour chaque nom transmis, qui est du même type que le type (Property ou Field) du type source. Agréable!

Maintenant, comment donnons-nous à LINQ ce qu'il veut?

Commençons par définir une "entrée" pour la fonction, nous y retournerons:

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

Nous savons qu'il nous faudra "rajeunir" l'un de nos nouveaux types dynamiques ...

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

Et nous devrons l'initialiser avec les valeurs provenant de ce paramètre ...

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

Mais que diable allons-nous utiliser pour les bindings ? Hmm ... eh bien, nous voulons quelque chose qui se lie aux propriétés / champs correspondants dans le type source, mais les remappe à nos champs dynamicType ...

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

Oof ... vilain, mais nous n'avons pas encore terminé - nous devons donc déclarer un type de retour pour le Func que nous créons via des arbres d'expression ... en cas de doute, utilisez object !

Expression.Convert( expr, typeof(object))

Et enfin, nous lierons cela à notre "paramètre d'entrée" via Lambda , rendant l'ensemble de la pile:

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

Pour plus de facilité, considérons le problème comme une méthode d’extension. Nous avons maintenant:

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

Maintenant, pour l'utiliser:

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

Cette solution manque de la flexibilité des "instructions imbriquées", comme "CreatedDate.Month", mais avec un peu d'imagination, vous pourriez éventuellement étendre cette idée pour fonctionner avec n'importe quelle requête à format libre.




Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi