3

参照プロパティの LINQ Expression の投稿に従って、 Daniel Hilgarth のおかげで Group By Extension を実装しました。

_unitOfWork.MenuSetRepository.Get().GroupBy("Role.Name","MenuText");

延長方法

public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(this IEnumerable<TElement> elements,string property)
    {
        var parameter = Expression.Parameter(typeof(TElement), "groupCol");
        Expression<Func<TElement, string>> lambda;
        if (property.Split('.').Count() > 1)
        {
            Expression body = null;
            foreach (var propertyName in property.Split('.'))
            {
                Expression instance = body;
                if (body == null)
                    instance = parameter;
                body = Expression.Property(instance, propertyName);
            }
            lambda = Expression.Lambda<Func<TElement, string>>(body, parameter);
        }
        else
        {
            var menuProperty = Expression.PropertyOrField(parameter, property);
            lambda = Expression.Lambda<Func<TElement, string>>(menuProperty, parameter);    
        }

        var selector= lambda.Compile();
       return elements.GroupBy(selector);
    }
4

1 に答える 1

7

この回答は 2 つの部分で構成されています。

  1. あなたの問題の解決策を提供します
  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 つの回答で示した方法の拡張です。

次のように機能します。

  1. 提供されたディープ プロパティ文字列ごとに、 を介して対応する式を取得しGetDeepPropertyExpressionます。これは基本的に、以前の回答で追加したコードです。
  2. プロパティが 1 つしか渡されていない場合は、ラムダの本体として直接使用します。結果は、前の回答と同じ式です。x => x.Role.Name
  3. 複数のプロパティが渡された場合は、プロパティを互いに連結し、その間に区切り文字を入れて、それをラムダの本体として使用します。私はセミコロンを選択しましたが、好きなものを使用できます。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 つの異なるクラスEnumerableQueryable. それらのクラスのメソッドの最初のパラメーターを比較します。

だから、あなたがしたいことは次のようなものです:

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));
}
于 2013-07-17T07:49:39.490 に答える