8

私はこの問題を1週間続けましたが、解決策が見つかりませんでした。

私は以下のようなPOCOを持っています:

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

特定の期間(月または年でグループ化)に、AuthorNameまたはCategoryでカウントされるジャーナルの量を知りたい。

クエリされたオブジェクトをJSONシリアライザーに送信した後、以下のようにJSONデータを生成します(取得したいデータを示すためにJSONを使用するだけで、オブジェクトをJSONにシリアライズする方法は私の問題ではありません)

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

また

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

私が知っているのは、次のような「メソッド方式」でLINQクエリを作成することです。

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

月または年、AuthorNameまたはCategoryでグループ化された条件は、2つの文字列型メソッドパラメーターによって渡されます。私が知らないのは、GroupBy()メソッドで「マジックストリング」パラメーターを使用する方法です。グーグルした後、「AuthorName」のようなマジックストリングを渡してデータをグループ化できないようです。私がすべきことは、式ツリーを作成し、それをGroupBy()メソッドに渡すことです。

任意の解決策や提案をいただければ幸いです。

4

1 に答える 1

27

ああ、これは楽しい問題のように見えます:)

まず、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 };    

さて、これで処理するデータのセットができました。必要なものを見てみましょう...次のような「形状」を持つものが必要です。

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

これは(擬似コードで)とほぼ同じ機能を持っています:

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

一度に1つずつ分析してみましょう。まず、これを動的に行うにはどうすればよいでしょうか。

x => new { ... }

コンパイラは通常私たちのために魔法をかけます-それがすることは新しいものを定義するTypeことです、そして私たちは同じことをすることができます:

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

したがって、ここで行ったことは、渡す名前ごとに1つのフィールドを持つカスタムの使い捨てタイプを定義することです。これは、ソースタイプの(プロパティまたはフィールド)と同じタイプです。良い!

では、どのようにしてLINQに必要なものを提供するのでしょうか。

まず、返す関数の「入力」を設定しましょう。

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

新しい動的タイプの1つを「更新」する必要があることはわかっています...

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

そして、そのパラメータから入ってくる値でそれを初期化する必要があります...

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

しかし、一体何のために使用するのbindingsでしょうか?うーん...まあ、ソースタイプの対応するプロパティ/フィールドにバインドするものが必要ですが、それらをdynamicTypeフィールドに再マップします...

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

見た目は厄介ですが、まだ完了していません。そのため、Func式ツリーを介して作成しているリターンタイプを宣言する必要があります...疑わしい場合は、object!を使用してください。

Expression.Convert( expr, typeof(object))

そして最後に、これをを介して「入力パラメータ」にバインドしLambda、スタック全体を作成します。

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

使いやすさのために、混乱全体を拡張メソッドとしてまとめましょう。これで、次のようになります。

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

今、それを使用するには:

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

このソリューションには、「CreatedDate.Month」のような「ネストされたステートメント」の柔軟性がありませんが、少し想像力を働かせれば、このアイデアを拡張して任意の自由形式のクエリで機能させることができます。

于 2013-03-08T18:09:19.330 に答える