C # - kode untuk memesan berdasarkan properti menggunakan nama properti sebagai string


92

Apa cara termudah untuk membuat kode terhadap properti di C # ketika saya memiliki nama properti sebagai string? Misalnya, saya ingin mengizinkan pengguna untuk memesan beberapa hasil pencarian berdasarkan properti pilihan mereka (menggunakan LINQ). Mereka akan memilih properti "urutkan menurut" di UI - tentu saja sebagai nilai string. Apakah ada cara untuk menggunakan string itu secara langsung sebagai properti kueri linq, tanpa harus menggunakan logika kondisional (if / else, switch) untuk memetakan string ke properti. Refleksi?

Logikanya, inilah yang ingin saya lakukan:

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

Pembaruan: Saya awalnya tidak menentukan bahwa saya menggunakan Linq untuk Entitas - tampaknya refleksi itu (setidaknya pendekatan GetProperty, GetValue) tidak diterjemahkan ke L2E.


Saya pikir Anda harus menggunakan refleksi, dan saya tidak yakin Anda dapat menggunakan refleksi dalam ekspresi lambda ... yah, hampir pasti tidak dalam Linq ke SQL tetapi mungkin saat menggunakan Linq terhadap daftar atau sesuatu.
CodeRedick

@Telos: Tidak ada alasan Anda tidak dapat menggunakan refleksi (atau API lainnya) di lambda. Apakah itu akan berfungsi atau tidak jika kode dievaluasi sebagai ekspresi dan diterjemahkan ke dalam sesuatu yang lain (seperti LINQ-to-SQL, seperti yang Anda sarankan) adalah pertanyaan lain sepenuhnya.
Adam Robinson

Inilah mengapa saya memposting komentar alih-alih jawaban. ;) Kebanyakan digunakan untuk Linq2SQL ...
CodeRedick

1
Cuma harus mengatasi masalah yang sama .. lihat jawaban saya di bawah ini. stackoverflow.com/a/21936366/775114
Mark Powell

Jawaban:


130

Saya akan menawarkan alternatif ini untuk apa yang orang lain telah posting.

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

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

Ini menghindari panggilan berulang-ulang ke API refleksi untuk mendapatkan properti. Sekarang satu-satunya panggilan berulang adalah mendapatkan nilainya.

Namun

Saya akan menganjurkan menggunakan a PropertyDescriptorsebagai gantinya, karena ini akan memungkinkan kustom TypeDescriptoruntuk ditetapkan ke jenis Anda, sehingga memungkinkan untuk memiliki operasi ringan untuk mengambil properti dan nilai. Jika tidak ada deskriptor khusus, deskripsi akan kembali ke refleksi.

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

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

Sedangkan untuk mempercepatnya, lihat HyperDescriptorproyek Marc Gravel di CodeProject. Saya telah menggunakan ini dengan sukses besar; ini adalah penyelamat untuk pengikatan data berkinerja tinggi dan operasi properti dinamis pada objek bisnis.


Perhatikan bahwa permintaan yang direfleksikan (yaitu GetValue) adalah bagian refleksi yang paling mahal. Pengambilan metadata (yaitu GetProperty) sebenarnya lebih murah (dengan urutan besarnya), jadi dengan menyimpan bagian itu ke cache, Anda tidak benar-benar menghemat banyak uang. Ini akan memakan biaya yang hampir sama, dan biaya itu akan menjadi berat. Hanya sesuatu yang perlu diperhatikan.
jrista

1
@jrista: pemanggilan adalah yang paling mahal, pastinya. Namun, "lebih murah" tidak berarti "gratis", atau bahkan mendekati itu. Pengambilan metadata membutuhkan waktu yang tidak sepele, jadi ada keuntungan untuk menyimpan dalam cache dan tidak ada kerugian (kecuali saya melewatkan sesuatu di sini). Sebenarnya ini harus benar-benar menggunakan PropertyDescriptortoh (untuk memperhitungkan deskriptor tipe kustom, yang dapat membuat pengambilan nilai menjadi operasi yang ringan).
Adam Robinson

Mencari selama berjam-jam untuk sesuatu seperti ini untuk menangani pengurutan ASP.NET GridView secara terprogram: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter

1
stackoverflow.com/questions/61635636/… Memiliki masalah dengan refleksi, itu tidak berhasil di EfCore 3.1.3. Tampaknya membuang kesalahan di EfCore 2 yang perlu diaktifkan untuk peringatan. Gunakan jawaban dari
@Mark di

1
Saya menerima berikut ini: InvalidOperationException: Ekspresi LINQ 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Address"). GetValue (obj: t, index: null) .GetType ()) 'tidak dapat diterjemahkan. Tulis ulang kueri dalam bentuk yang bisa diterjemahkan, atau beralih ke evaluasi klien secara eksplisit dengan memasukkan panggilan ke AsEnumerable (), AsAsyncEnumerable (), ToList (), atau ToListAsync ().
bbrinck

68

Saya agak terlambat ke pesta, namun, saya harap ini bisa membantu.

Masalah dengan menggunakan refleksi adalah bahwa Pohon Ekspresi yang dihasilkan hampir pasti tidak akan didukung oleh penyedia Linq selain penyedia .Net internal. Ini bagus untuk koleksi internal, namun ini tidak akan berfungsi di mana penyortiran dilakukan pada sumber (baik itu SQL, MongoDb, dll.) Sebelum penomoran halaman.

Contoh kode di bawah ini menyediakan metode ekstensi yang dapat disesuaikan untuk OrderBy dan OrderByDescending, dan dapat digunakan seperti ini:

query = query.OrderBy("ProductId");

Metode Ekstensi:

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

Salam, Mark.


Solusi luar biasa - Saya mencari hal itu. Saya benar-benar perlu menggali pohon Expression. Masih sangat pemula dalam hal itu. @ Mark, ada solusi untuk melakukan ekspresi bersarang? Katakanlah saya memiliki tipe T dengan properti "Sub" dari tipe TSub yang memiliki properti "Nilai". Sekarang saya ingin mendapatkan ekspresi Expression <Func <T, object >> untuk string "Sub.Value".
Simon Scheurer

4
Mengapa kita perlu Expression.Convertuntuk pindah propertyke object? Saya mendapatkan Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.kesalahan, dan menghapusnya sepertinya berhasil.
ShuberFu

@ Demodave jika saya ingat dengan benar. var propAsObject = Expression.Convert(property, typeof(object));dan hanya digunakan propertydi tempatpropAsObject
ShuberFu

Emas. Diadaptasi untuk .Net Core 2.0.5.
Chris Amelinckx

2
Mendapat kesalahanLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski

35

Saya menyukai jawaban dari @Mark Powell , tetapi seperti yang dikatakan @ShuberFu , itu memberikan kesalahan LINQ to Entities only supports casting EDM primitive or enumeration types.

Penghapusan var propAsObject = Expression.Convert(property, typeof(object));tidak berfungsi dengan properti yang merupakan tipe nilai, seperti integer, karena tidak secara implisit mengotakkan int ke objek.

Menggunakan Ide dari Kristofer Andersson dan Marc Gravell Saya menemukan cara untuk membangun fungsi Querable menggunakan nama properti dan masih berfungsi dengan Entity Framework. Saya juga menyertakan parameter IComparer opsional. Perhatian: Parameter IComparer tidak bekerja dengan Entity Framework dan harus ditinggalkan jika menggunakan Linq ke Sql.

Berikut ini bekerja dengan Entity Framework dan Linq to Sql:

query = query.OrderBy("ProductId");

Dan @Simon Scheurer ini juga berfungsi:

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

Dan jika Anda tidak menggunakan Entity Framework atau Linq ke Sql, ini berfungsi:

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

Ini kodenya:

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

Ya ampun, apakah kamu Microsoft? :) AggregateFragmen itu luar biasa! Ini menangani tampilan virtual yang dibuat dari model EF Core dengan Join, karena saya menggunakan properti seperti "T.Property". Jika tidak, memesan setelahnya Jointidak mungkin menghasilkan salah satu InvalidOperationExceptionatau NullReferenceException. Dan saya perlu memesan SETELAH Join, karena sebagian besar kueri konstan, urutan dalam tampilan tidak.
Harry

@Harry. Terima kasih, tapi saya benar-benar tidak bisa mengambil terlalu banyak pujian untuk Aggregatepotongan itu. Saya percaya itu adalah kombinasi dari kode Marc Gravell dan rekomendasi yang masuk akal. :)
David Specht

@DavidSpecht Saya baru saja mempelajari Pohon Ekspresi, jadi segala sesuatu tentang mereka sekarang menjadi sihir hitam bagi saya. Tapi saya belajar dengan cepat, jendela interaktif C # di VS banyak membantu.
Harry

bagaimana cara menggunakan ini?
Dat Nguyen

@Dat Nguyen Alih-alih products.OrderBy(x => x.ProductId), Anda dapat menggunakanproducts.OrderBy("ProductId")
David Specht

12

Ya, saya rasa tidak ada cara lain selain Refleksi.

Contoh:

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

Saya menerima kesalahan "LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."Ada pemikiran atau saran, tolong?
Florin Vîrdol

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

Mencoba mengingat sintaks yang tepat dari atas kepala saya tetapi saya pikir itu benar.


2

Refleksi adalah jawabannya!

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

Ada banyak hal yang dapat Anda lakukan untuk meng-cache PropertyInfo yang direfleksikan, memeriksa string yang buruk, menulis fungsi perbandingan kueri Anda, dll., Tetapi intinya, inilah yang Anda lakukan.



2

Lebih produktif daripada ekstensi refleksi untuk item pesanan dinamis:

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

Contoh:

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

Anda juga mungkin perlu menyimpan lambas yang sesuai dengan cache (misalnya dalam Kamus <>)


1

Ekspresi Dinamis juga dapat mengatasi masalah ini. Anda dapat menggunakan kueri berbasis string melalui ekspresi LINQ yang dapat dibuat secara dinamis pada waktu proses.

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

0

Saya pikir kita dapat menggunakan alat yang kuat bernama Expression an dalam hal ini menggunakannya sebagai metode ekstensi sebagai berikut:

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);
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.