C # - код для упорядочивания по свойству с использованием имени свойства в виде строки


92

Какой самый простой способ кодировать свойство на C #, когда у меня есть имя свойства в виде строки? Например, я хочу разрешить пользователю упорядочивать некоторые результаты поиска по свойству по своему выбору (с использованием LINQ). Они выберут свойство «порядок по» в пользовательском интерфейсе - конечно, как строковое значение. Есть ли способ использовать эту строку непосредственно как свойство запроса linq, без использования условной логики (if / else, switch) для сопоставления строк со свойствами. Отражение?

По логике вещей я бы хотел сделать следующее:

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
Марк Пауэлл,

Ответы:


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проект Marc Gravel на CodeProject. Я использовал это с большим успехом; это спасатель для высокопроизводительной привязки данных и операций с динамическими свойствами бизнес-объектов.


Обратите внимание, что отраженный вызов (то есть GetValue) является наиболее затратной частью отражения. Получение метаданных (например, GetProperty) на самом деле дешевле (на порядок), поэтому, кэшируя эту часть, вы на самом деле не так сильно экономите. В любом случае это будет стоить примерно одинаково, и эта цена будет высокой. Просто кое-что отметить.
jrista

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

Несколько часов искал что-то подобное для программной сортировки ASP.NET GridView: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter

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». Теперь я хочу получить выражение Expression <Func <T, object >> для строки «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 02

Золото. Адаптирован для .Net Core 2.0.5.
Крис Амелинкс

2
Получил ошибкуLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski 09

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фрагмент. Я считаю, что это была комбинация кода Марка Грейвелла и рекомендации intellisense. :)
Дэвид Шпехт

@DavidSpecht Я только изучаю деревья выражений, так что все, что связано с ними, теперь для меня - черная магия. Но я быстро учусь, интерактивное окно C # в VS очень помогает.
Гарри

как это использовать?
Дат Нгуен

@Dat Nguyen Вместо этого 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."Любые мысли или советы, пожалуйста?
Флорин Вырдол,

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

Также вам может потребоваться кешировать собранные лямб-выражения (например, в 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);
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.