この回答は 2 つの部分で構成されています。
- あなたの問題の解決策を提供します
IEnumerable<T>
と 、およびIQueryable<T>
2 つの違いについて説明します
パート 1: 当面の問題の解決策
新しい要件は、他の要件ほど簡単には満たされません。これの主な理由は、複合キーによってグループ化される LINQ クエリによって、コンパイル時に匿名型が作成されるためです。
source.GroupBy(x => new { x.MenuText, Name = x.Role.Name })
MenuText
これにより、コンパイラによって生成された名前と 2 つのプロパティおよびを持つ新しいクラスが作成されますName
。
実行時にこれを行うことは可能ですが、IL を新しい動的アセンブリに発行する必要があるため、実際には実現可能ではありません。
私のソリューションでは、別のアプローチを選択しました。
関連するすべてのプロパティはタイプstring
のように見えるため、グループ化するキーは、セミコロンで区切られたプロパティ値の単なる連結です。
したがって、コードが生成する式は次と同等です。
source.GroupBy(x => x.MenuText + ";" + x.Role.Name)
これを実現するコードは次のようになります。
private static Expression<Func<T, string>> GetGroupKey<T>(
params string[] properties)
{
if(!properties.Any())
throw new ArgumentException(
"At least one property needs to be specified", "properties");
var parameter = Expression.Parameter(typeof(T));
var propertyExpressions = properties.Select(
x => GetDeepPropertyExpression(parameter, x)).ToArray();
Expression body = null;
if(propertyExpressions.Length == 1)
body = propertyExpressions[0];
else
{
var concatMethod = typeof(string).GetMethod(
"Concat",
new[] { typeof(string), typeof(string), typeof(string) });
var separator = Expression.Constant(";");
body = propertyExpressions.Aggregate(
(x , y) => Expression.Call(concatMethod, x, separator, y));
}
return Expression.Lambda<Func<T, string>>(body, parameter);
}
private static Expression GetDeepPropertyExpression(
Expression initialInstance, string property)
{
Expression result = null;
foreach(var propertyName in property.Split('.'))
{
Expression instance = result;
if(instance == null)
instance = initialInstance;
result = Expression.Property(instance, propertyName);
}
return result;
}
これも、前 の 2 つの回答で示した方法の拡張です。
次のように機能します。
- 提供されたディープ プロパティ文字列ごとに、 を介して対応する式を取得し
GetDeepPropertyExpression
ます。これは基本的に、以前の回答で追加したコードです。
- プロパティが 1 つしか渡されていない場合は、ラムダの本体として直接使用します。結果は、前の回答と同じ式です。
x => x.Role.Name
複数のプロパティが渡された場合は、プロパティを互いに連結し、その間に区切り文字を入れて、それをラムダの本体として使用します。私はセミコロンを選択しましたが、好きなものを使用できます。3 つのプロパティ ( ) を渡したとすると"MenuText", "Role.Name", "ActionName"
、結果は次のようになります。
x => string.Concat(
string.Concat(x.MenuText, ";", x.Role.Name), ";", x.ActionName)
これは、プラス記号を使用して文字列を連結する式に対して C# コンパイラが生成する式と同じであるため、次の式と同等です。
x => x.MenuText + ";" + x.Role.Name + ";" + x.ActionName
パート 2: 教育
質問で示した拡張方法は非常に悪い考えです。
なんで?まあ、それはで動作するからIEnumerable<T>
です。つまり、この group by はデータベース サーバーでは実行されず、アプリケーションのメモリ内でローカルに実行されます。さらに、a など、後続のすべての LINQ 句もWhere
メモリ内で実行されます。
拡張メソッドを提供する場合は、IEnumerable<T>
(メモリ内、つまり LINQ to Objects) とIQueryable<T>
(LINQ to Entity Framework などのデータベースで実行されるクエリ) の両方に対して行う必要があります。
これは、Microsoft が選択したのと同じアプローチです。ほとんどの LINQ 拡張メソッドには、2 つのバリアントが存在します。1 つは動作しIEnumerable<T>
、もう 1 つは動作しIQueryable<T>
、2 つの異なるクラスEnumerable
とQueryable
. それらのクラスのメソッドの最初のパラメーターを比較します。
だから、あなたがしたいことは次のようなものです:
public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(
this IEnumerable<TElement> source, params string[] properties)
{
return source.GroupBy(GetGroupKey<TElement>(properties).Compile());
}
public static IQueryable<IGrouping<string, TElement>> GroupBy<TElement>(
this IQueryable<TElement> source, params string[] properties)
{
return source.GroupBy(GetGroupKey<TElement>(properties));
}