C#-プロパティ名を文字列として使用してプロパティで並べ替えるコード


93

プロパティ名を文字列として持っている場合、C#でプロパティに対してコーディングする最も簡単な方法は何ですか?たとえば、ユーザーが選択したプロパティで検索結果を並べ替えることができるようにしたい(LINQを使用)。もちろん、文字列値として、UIの「orderby」プロパティを選択します。文字列をプロパティにマップするために条件付きロジック(if / else、switch)を使用せずに、その文字列をlinqクエリのプロパティとして直接使用する方法はありますか?反射?

論理的には、これが私がやりたいことです。

query = query.OrderBy(x => x."ProductId");

更新:元々、Linq to Entitiesを使用していることを指定していませんでした-リフレクション(少なくともGetProperty、GetValueアプローチ)はL2Eに変換されないようです。


リフレクションを使用する必要があると思いますが、ラムダ式でリフレクションを使用できるかどうかはわかりません...まあ、ほぼ確実に、Linq to SQLではなく、リストなどに対してLinqを使用する場合です。
codeRedick 2009年

@Telos:ラムダでリフレクション(または他のAPI)を使用できない理由はありません。コードが式として評価され、他の何か(あなたが提案するように、LINQ-to-SQLなど)に変換された場合に機能するかどうかは、まったく別の問題です。
アダムロビンソン

これが私が答えの代わりにコメントを投稿した理由です。;)ほとんど... Linq2SQLに使用
CodeRedick

1
同じ問題を克服しなければなりませんでした。以下の私の答えを参照してください。stackoverflow.com/a/21936366/775114
Mark Powell

回答:


132

私は他の誰もが投稿したものに代わるこの代替案を提供します。

System.Reflection.PropertyInfo prop = typeof(YourType).GetProperty("PropertyName");

query = query.OrderBy(x => prop.GetValue(x, null));

これにより、プロパティを取得するためにリフレクションAPIを繰り返し呼び出す必要がなくなります。ここで繰り返される呼び出しは、値を取得することだけです。

しかしながら

PropertyDescriptor代わりに使用するTypeDescriptorことをお勧めします。これにより、カスタムをタイプに割り当てることができ、プロパティと値を取得するための軽量な操作が可能になります。カスタム記述子がない場合は、とにかくリフレクションにフォールバックします。

PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(YourType)).Find("PropertyName");

query = query.OrderBy(x => prop.GetValue(x));

スピードアップについては、HyperDescriptorCodeProjectでMarcGravelのプロジェクトをチェックしてください。私はこれを大成功で使用しました。これは、ビジネスオブジェクトに対する高性能のデータバインディングと動的なプロパティ操作の命を救います。


リフレクトされた呼び出し(つまりGetValue)は、リフレクションの中で最もコストのかかる部分であることに注意してください。メタデータの取得(つまり、GetProperty)は、実際には(桁違いに)コストが低いため、その部分をキャッシュすることで、それほど節約することはできません。これはどちらの方法でもほぼ同じコストになり、そのコストは高額になります。注意すべき点があります。
jrista 2009年

1
@jrista:確かに、呼び出しは最もコストがかかります。ただし、「低コスト」とは「無料」、またはそれに近いという意味ではありません。メタデータの取得には重要な時間がかかるため、メタデータをキャッシュすることには利点があり、欠点はありません(ここで何かが欠けている場合を除く)。実際、これはPropertyDescriptorとにかく実際に使用する必要があります(値の取得を軽量な操作にする可能性のあるカスタム型記述子を説明するため)。
アダムロビンソン

ASP.NET GridViewの並べ替えをプログラムで処理するために、次のようなものを何時間も検索しました。PropertyDescriptorprop = TypeDescriptor.GetProperties(typeof(ScholarshipRequest))。Find(e.SortExpression、true);
バクスター

1
stackoverflow.com/questions/61635636/… リフレクションに問題があり、EfCore3.1.3では機能しませんでした。警告のためにアクティブ化する必要があるEfCore2でエラーをスローするようです。下の@マークの答えを使用してください
armourshield

1
次を受け取ります:InvalidOperationException:LINQ式 'DbSet <MyObject> .Where(t => t.IsMasterData).OrderBy(t => t.GetType()。GetProperty( "Address")。GetValue(obj:t、 index:null).GetType()) 'を変換できませんでした。翻訳可能な形式でクエリを書き直すか、AsEnumerable()、AsAsyncEnumerable()、ToList()、またはToListAsync()のいずれかに呼び出しを挿入して、クライアント評価に明示的に切り替えます。
bbrinck

70

私はパーティーに少し遅れていますが、これが何らかの助けになることを願っています。

リフレクションを使用する場合の問題は、結果の式ツリーが、内部の.Netプロバイダー以外のLinqプロバイダーによってほぼ確実にサポートされないことです。これは内部コレクションには問題ありませんが、ページ付けの前にソース(SQL、MongoDbなど)で並べ替えが行われる場合は機能しません。

以下のコードサンプルは、OrderByおよびOrde​​rByDescendingのIQueryable拡張メソッドを提供し、次のように使用できます。

query = query.OrderBy("ProductId");

拡張方法:

public static class IQueryableExtensions 
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderBy(ToLambda<T>(propertyName));
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderByDescending(ToLambda<T>(propertyName));
    }

    private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var parameter = Expression.Parameter(typeof(T));
        var property = Expression.Property(parameter, propertyName);
        var propAsObject = Expression.Convert(property, typeof(object));

        return Expression.Lambda<Func<T, object>>(propAsObject, parameter);            
    }
}

よろしく、マーク。


優れたソリューション-まさにそれを探していました。私は本当にExpressionツリーを掘り下げる必要があります。それでも非常に新人です。@Mark、ネストされた式を実行するためのソリューションはありますか?タイプTSubのプロパティ「Sub」を持つタイプTがあり、それ自体がプロパティ「Value」を持っているとします。ここで、文字列「Sub.Value」の式Expression <Func <T、object >>を取得したいと思います。
Simon Scheurer 2016

5
なぜ我々は必要なのですかExpression.Convert変換することpropertyobject?私は取得していますUnable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.エラーが、動作するように思わ取り除きます。
ShuberFu 2016

正しく覚えていれば@Demodave。代わりにvar propAsObject = Expression.Convert(property, typeof(object));使用するpropertypropAsObject
ShuberFu 2017

1
ゴールド。.Net Core2.0.5に適合。
Chris Amelinckx 2018

2
ガットエラーLINQ to Entities only supports casting EDM primitive or enumeration types
はMateuszPuwałowski

38

@Mark Powellからの回答が気に入りましたが、@ ShuberFuが言ったように、エラーが発生しますLINQ to Entities only supports casting EDM primitive or enumeration types

削除var propAsObject = Expression.Convert(property, typeof(object));は、整数などの値型のプロパティでは機能しませんでした。これは、intをオブジェクトに暗黙的にボックス化しないためです。

KristoferAnderssonMarcGravellのアイデアを使用して、プロパティ名を使用してQueryable関数を構築し、それEntityFrameworkで引き続き機能させる方法を見つけました。オプションのIComparerパラメーターも含めました。注意: IComparerパラメーターはEntity Frameworkでは機能しないため、Linq toSqlを使用する場合は省略してください。

以下は、EntityFrameworkとLinqtoSqlで機能します。

query = query.OrderBy("ProductId");

そして@SimonScheurerこれも機能します:

query = query.OrderBy("ProductCategory.CategoryId");

また、EntityFrameworkまたはLinqto Sqlを使用していない場合、これは機能します。

query = query.OrderBy("ProductCategory", comparer);

コードは次のとおりです。

public static class IQueryableExtensions 
{    
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
}

public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenBy", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer);
}

/// <summary>
/// Builds the Queryable functions using a TSource property name.
/// </summary>
public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
        IComparer<object> comparer = null)
{
    var param = Expression.Parameter(typeof(T), "x");

    var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

    return comparer != null
        ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param),
                Expression.Constant(comparer)
            )
        )
        : (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param)
            )
        );
}
}

ねえ、男、あなたはマイクロソフトですか?:)そのAggregateフラグメントは素晴らしいです!Join「T.Property」などのプロパティを使用しているため、EFCoreモデルから作成された仮想ビューを処理します。そうしJoinないと、後で注文すると、InvalidOperationExceptionまたはのいずれかを生成できなくなりますNullReferenceException。またJoin、ほとんどのクエリは一定であるため、ビュー内の順序は一定ではないため、後で注文する必要があります。
ハリー

@DavidSpecht私はエクスプレッションツリーを学んでいるところなので、それらに関するすべては今でも私にとって黒魔術です。しかし、私はすぐに学びます。VSのC#インタラクティブウィンドウは大いに役立ちます。
ハリー

これを使用する方法は?
ダットグエン

1
@Datグエン代わりにproducts.OrderBy(x => x.ProductId)、あなたが使用することができますproducts.OrderBy("ProductId")
デヴィッド・シュペヒト

12

はい、Reflection以外の方法はないと思います。

例:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

エラーが表示されます"LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."考えやアドバイスはありますか?
フローリンVîrdol

5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

頭のてっぺんから正確な構文を思い出そうとしていますが、それは正しいと思います。


2

リフレクションがその答えです!

typeof(YourType).GetProperty("ProductId").GetValue(theInstance);

反映されたPropertyInfoをキャッシュしたり、不正な文字列をチェックしたり、クエリ比較関数を記述したりするためにできることはたくさんありますが、基本的にはこれがあなたの仕事です。



2

動的注文アイテムへのリフレクション拡張よりも生産性が高い:

public static class DynamicExtentions
{
    public static object GetPropertyDynamic<Tobj>(this Tobj self, string propertyName) where Tobj : class
    {
        var param = Expression.Parameter(typeof(Tobj), "value");
        var getter = Expression.Property(param, propertyName);
        var boxer = Expression.TypeAs(getter, typeof(object));
        var getPropValue = Expression.Lambda<Func<Tobj, object>>(boxer, param).Compile();            
        return getPropValue(self);
    }
}

例:

var ordered = items.OrderBy(x => x.GetPropertyDynamic("ProductId"));

また、準拠したlambasをキャッシュする必要がある場合もあります(例:Dictionary <>)


1

また、動的式はこの問題を解決できます。実行時に動的に構築された可能性のあるLINQ式を介して文字列ベースのクエリを使用できます。

var query = query
          .Where("Category.CategoryName == @0 and Orders.Count >= @1", "Book", 10)
          .OrderBy("ProductId")
          .Select("new(ProductName as Name, Price)");

0

強力なツール名Expressionを使用できると思います。この場合、次のように拡張メソッドとして使用します。

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string ordering, bool descending)
{
    var type = typeof(T);
    var property = type.GetProperty(ordering);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExp = Expression.Lambda(propertyAccess, parameter);
    MethodCallExpression resultExp = 
        Expression.Call(typeof(Queryable), (descending ? "OrderByDescending" : "OrderBy"), 
            new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
    return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(resultExp);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.