C # - код для замовлення за властивістю, використовуючи ім'я властивості як рядок


92

Який найпростіший спосіб кодувати властивість у C #, коли у мене є ім’я властивості як рядок? Наприклад, я хочу дозволити користувачеві впорядковувати деякі результати пошуку за властивістю на їх вибір (за допомогою LINQ). Вони виберуть властивість "упорядкувати за" в інтерфейсі - звичайно як значення рядка. Чи є спосіб використовувати цей рядок безпосередньо як властивість запиту linq, без необхідності використовувати умовну логіку (якщо / ще, перемикач) для зіставлення рядків із властивостями. Роздум?

Логічно, ось що я хотів би зробити:

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

Оновлення: Спочатку я не вказував, що використовую Linq to Entities - схоже, відображення (принаймні підхід GetProperty, GetValue) не перекладається на L2E.


Я думаю, що вам доведеться використовувати відображення, і я не впевнений, що ви можете використовувати відображення в лямбда-виразі ... ну, майже напевно не в Linq to SQL, але, можливо, при використанні Linq проти списку чи чогось іншого.
CodeRedick

@Telos: Немає жодної причини, через яку ви не можете використовувати відображення (або будь-який інший API) у лямбда. Чи буде це працювати, якщо код буде оцінено як вираз і переведено в щось інше (як LINQ-to-SQL, як ви пропонуєте) - це вже зовсім інше питання.
Адам Робінсон,

Ось чому я розмістив коментар замість відповіді. ;) В основному звик до Linq2SQL ...
CodeRedick

1
Просто довелося подолати ту ж проблему .. див. Мою відповідь нижче. stackoverflow.com/a/21936366/775114
Mark Powell

Відповіді:


129

Я запропонував би цю альтернативу тому, що публікували всі інші.

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

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

Це дозволяє уникнути повторних викликів API відображення для отримання властивості. Зараз єдиним повторним викликом є ​​отримання значення.

Однак

Я б виступав за використання PropertyDescriptorзамість цього, оскільки це дозволить TypeDescriptorпризначити користувацькі s для вашого типу, що робить можливим полегшені операції для отримання властивостей і значень. За відсутності спеціального дескриптора він все одно повернеться до відображення.

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

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

Що стосується прискорення, ознайомтесь з HyperDescriptorпроектом Марка Гравела на CodeProject. Я використав це з великим успіхом; це рятувальник життя для високопродуктивного прив’язки даних та динамічних операцій із властивостями бізнес-об’єктів.


Зауважте, що відображене виклик (тобто GetValue) є найдорожчою частиною відображення. Отримання метаданих (тобто GetProperty) насправді є менш витратним (на порядок), тому кешуючи цю частину, ви насправді не економите настільки. Це буде коштувати майже однаково в будь-якому випадку, і ця вартість буде великою. Тільки щось на замітку.
jrista

1
@jrista: заклик є найдорожчим, щоб бути впевненим. Однак "менш дорогий" не означає "безкоштовний" або навіть близький до нього. Отримання метаданих займає нетривіальну кількість часу, тому кешування їх має перевагу і не має недоліків (якщо я чогось тут не пропускаю). По правді кажучи, насправді в PropertyDescriptorбудь-якому випадку слід використовувати (для врахування користувальницьких дескрипторів типу, які можуть зробити пошук значень полегшеною операцією).
Адам Робінсон,

Шукав годинами щось подібне, щоб обробляти програмне сортування ASP.NET GridView: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Знайти (e.SortExpression, true);
Бакстер

1
stackoverflow.com/questions/61635636 / ... Мав проблеми з відображенням вона не працює в EfCore 3.1.3. Здається, виникає помилка в EfCore 2, яку потрібно активувати для попереджень. Використовуйте відповідь @Mark нижче
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

67

Я трохи запізнився на вечірку, проте, сподіваюся, це може допомогти.

Проблема використання відображення полягає в тому, що отримане Дерево виразів майже напевно не буде підтримуватися будь-яким постачальником Linq, крім внутрішнього постачальника .Net. Це добре для внутрішніх колекцій, однак це не спрацює там, де сортування повинно виконуватися у джерелі (будь то SQL, MongoDb тощо) перед пагінацією.

Наведений нижче зразок коду забезпечує методи розширення IQueryable для OrderBy та OrderByDescending, і їх можна використовувати так:

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

З повагою, Марк.


Відмінне рішення - я шукав саме це. Мені дійсно потрібно копатись у деревах виразів. Досі дуже новачок у цьому. @Mark, будь-яке рішення для вкладених виразів? Скажімо, у мене є тип T із властивістю "Sub" типу TSub, який сам має властивість "Value". Тепер я хотів би отримати вираз Вираз <Func <T, об'єкт >> для рядка "Sub.Value".
Simon Scheurer

4
Навіщо нам потрібно , Expression.Convertщоб перетворити propertyв object? Я отримую Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.помилку, і видалення її, здається, працює.
ShuberFu

@Demodave, якщо я правильно пам’ятаю. var propAsObject = Expression.Convert(property, typeof(object));і просто використовувати propertyзамістьpropAsObject
ShuberFu

Золото. Адаптовано для .Net Core 2.0.5.
Chris Amelinckx

2
Отримана помилкаLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski

35

Мені сподобалась відповідь від @Mark Powell , але, як сказав @ShuberFu , це дає помилку LINQ to Entities only supports casting EDM primitive or enumeration types.

Видалення var propAsObject = Expression.Convert(property, typeof(object));не працювало з властивостями, що є типами значень, наприклад, цілим числом, оскільки воно не імпліцитно вставляло би int у об’єкт.

Використовуючи ідеї Крістофера Андерссона та Марка Гравелла, я знайшов спосіб побудувати функцію Queryable, використовуючи ім'я властивості, і продовжувати працювати з Entity Framework. Я також включив необов’язковий параметр IComparer. Увага: Параметр IComparer не працює з Entity Framework, і його слід пропустити, якщо ви використовуєте Linq to Sql.

Наступні роботи з Entity Framework та Linq to Sql:

query = query.OrderBy("ProductId");

І @Simon Scheurer це також працює:

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

І якщо ви не використовуєте Entity Framework або Linq to 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)
            )
        );
}
}

Господи, ти що, Microsoft? :) Цей Aggregateфрагмент чудовий! Він піклується про віртуальні подання, створені з моделі EF Core Join, оскільки я використовую такі властивості, як "T.Property". В іншому випадку замовлення після Joinбуде неможливим виготовлення InvalidOperationExceptionабо NullReferenceException. І мені потрібно замовляти ПІСЛЯ Join, оскільки більшість запитів є постійними, а замовлення у поданнях - ні.
Гаррі

@ Гаррі. Дякую, але я дійсно не можу взяти занадто багато кредитів за Aggregateфрагмент. Я вважаю, що це була комбінація коду Марка Гравелла та рекомендації, що стосуються інтелісенсу . :)
Девід Шпехт,

@DavidSpecht Я просто вивчаю Дерева виразів, тому все про них зараз для мене поки що чорна магія. Але я швидко вчусь, інтерактивне вікно C # у VS дуже допомагає.
Гаррі

як цим користуватися?
Дат Нгуєн

@Dat Nguyen Замість products.OrderBy(x => x.ProductId), ти міг використатиproducts.OrderBy("ProductId")
Девід Шпехт,

12

Так, я не думаю, що існує інший спосіб, ніж Роздум.

Приклад:

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."Будь ласка, будь-які думки чи поради?
Флорін Вірдол

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

Намагаюся згадати точний синтаксис з маківки, але я думаю, що це правильно.


2

Роздум - це відповідь!

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

Є багато речей, які ви можете зробити, щоб кешувати відображену PropertyInfo, перевірити наявність помилкових рядків, написати свою функцію порівняння запитів тощо, але в основі - це те, що ви робите.


2

Ви можете використовувати динамічний Linq - перегляньте цей блог.

Також перегляньте цю публікацію StackOverFlow ...


Це найкраща відповідь для мене
Demodave

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

Також вам може знадобитися кешувати відповідні ламбаси (наприклад, у словнику <>)


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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.