2013-03-08 14 views
6

この問題は1週間続きましたが、解決策は見つかりませんでした。式ツリーを使用してLINQ GroupByクエリを作成します。

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

私は特定の日付スパン(数ヶ月または数年でグループ化された)ジャーナルの量がAUTHORNAMEまたはカテゴリでカウント中に知りたい:

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

私はJSONシリアライザにqueryedオブジェクトはその後、以下のようなJSONデータを生成し送信した後

data: { 
    '201301': { 
     'Alex': 10, 
     'James': 20 
    }, 
    '201302': { 
     'Alex': 1, 
     'Jessica': 9 
    } 
} 
(ちょうど私が取得したいデータを示すためにJSONを使用して、どのようにJSONにオブジェクトをシリアライザは私の問題ではありません)

OR

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()メソッドで "Magic String"パラメータを使用する方法です。グーグルでは、「AuthorName」のような魔法の文字列を渡すことでデータをグループ化できないようです。私がしなければならないことは、式ツリーを構築し、それをGroupBy()メソッドに渡すことです。

解決策または提案はありがたいです。

+0

ダイナミックLINQを見ましたか? – svick

+0

@svick LINQ to Entityの別のオプションがある場合は、動的LINQではなくStackOverflowのDapperを選択します –

+0

ダイナミックLINQはIQueryableの上で動作するため、LINQ to Entitiesなどのライブラリを置き換えることはありません。それが働くように。 – svick

答えて

22

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

だから、最初、私はあなたの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]を、今、我々は我々が望むものを見てみましょう、と連携するデータのセットを持っている...私たちはのような「形状」で何かをしたい:

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にはどのようにしたいのですか?

まず、我々は戻りますFUNCための「入力」を設定してみましょう:

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

我々は、我々が「アップ新しい」私たちの新しいダイナミックタイプのいずれかが必要になります知っています...

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

OOFにそれらを再マッピング何か...探して厄介をしたいが、我々はしていますまだ行われていないので、式ツリーを使用して作成しているFuncの戻り値型を宣言する必要があります。疑問がある場合はobjectを使用してください。 、使用を容易にするため

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

のは、拡張機能として全体の混乱を締めくくりましょう:

Expression.Convert(expr, typeof(object)) 

そして最後に、我々はスタック全体を作り、Lambdaを経由して私たちの「入力パラメータ」にこれを結合します方法、今私たちが持っている:

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」のような「ネストされた文」の柔軟性に欠けていますが、少し想像力があれば、任意のフリーフォームクエリで動作するようにこのアイデアを拡張できます。

+0

+徹底的な説明+ハードワーク。 –

+0

私はこのために+10を与えることができたらいいですよ – Jeremy

+0

@JerKimball、ありがとう&面白い説明に感謝します。 –

関連する問題