Порядок по описанию перечисления

#c# #asp.net-mvc #entity-framework #linq

#c# #asp.net-mvc #entity-framework #linq

Вопрос:

Я работаю над ASP.NET MVC сначала проектирует с использованием EF-кода, и я сталкиваюсь с ситуацией, когда мне нужно упорядочивать по описанию перечисления:

 public partial class Item
{
    public enum MyEnumE
    {
        [Description("description of enum1")]
        Enum1,
        [Description("description of enum2")]
        Enum2,
        ...
    }

    public MyEnumE MyEnum { get; set; }
}
  

Вот функции Search и SortAndPaginate :

 public async Task<IPagedList<Item>> Search(ItemCriteria criteria, SortableTypeE sortName, SortOrder.TypeE sortOrder, int pageNb)
    {
        var itemFilter = GenerateFilter(criteria);
        var items = entities.Items.Where(itemFilter);

        return await SortAndPaginate(items, sortName, sortOrder, pageNb);
    }

    private async Task<IPagedList<Item>> SortAndPaginate(IQueryable<Item> items, SortableTypeE sortName, SortOrder.TypeE sortOrder, int pageNb)
    {
        IOrderedQueryable<Item> result = null;

        switch (sortName)
        {
            ...
            case SortableTypeE.Type:
                result = sortOrder == SortOrder.TypeE.ASC
                    ? items.OrderBy(i => i.MyEnum.GetDescription())
                    : items.OrderByDescending(i => i.MyEnum.GetDescription());
                result = result.ThenBy(i => i.SomeOtherProperty);
                break;
            ...
        }

        if (result != null)
        {
            return await result.ToPagedListAsync(pageNb, 10);
        }

        return PagedListHelper.Empty<Item>();
    }
  

Проблема в том, что Item таблица может быть довольно огромной.
Я думал о вызове ToListAsync сразу после entities.Items.Where(itemFilter) , но это вернет все отфильтрованные элементы, хотя мне нужна только одна страница. Не похоже на хорошую идею.

Но если я этого не сделаю, EF я не узнаю о GetDescription() mathod, и я могу думать только о двух решениях:
— Измените столбец моей базы данных на строку (описание перечисления) вместо самого перечисления (но для меня это звучит как взлом)
— Или расположите в алфавитном порядке MyEnumE компоненты непосредственно в enum объявлении (тоже кажется грязным и совершенно не поддерживаемым)

Я совершенно застрял, так как меня беспокоит производительность, если я вызываю ToListAsync сразу после фильтрации, все другие решения кажутся грязными, и мне абсолютно необходимо, чтобы IPagedList возвращаемый из Search метода.

У кого-нибудь есть идея о том, как справиться с этой проблемой?

Большое спасибо.

Обновить

Вот GetDescription метод (при необходимости его можно изменить):

 public static string GetDescription(this Enum e)
{
    FieldInfo fi = e.GetType().GetField(e.ToString());
    DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
    if (attributes.Length > 0)
        return attributes[0].Description;
    else
        return e.ToString();
}
  

Решения

Я, наконец, воспользуюсь предложением Ивана Стоева, потому что мой проект в основном основан на Linq (используя Linq вместо хранимых процедур и т.д.), Поэтому это решение кажется более подходящим для моего конкретного случая, чем создание справочных таблиц.

Однако Niyoko Yuliawan ‘s и Michael Freidgeim ‘s также являются действительно хорошими ответами для меня, любой, кто читает этот пост и имеет более подход к базе данных, должен использовать их решения;)

Большое спасибо всем вам.

Комментарии:

1. Можете ли вы опубликовать GetDescription метод?

2. Извините, не видел вашего комментария : ( Он опубликовал его 🙂

Ответ №1:

Я бы выбрал динамическое выражение. Он более гибкий и может быть легко изменен без влияния на таблицы базы данных и запросы.

Однако вместо сортировки по строкам описания в базе данных я бы создал упорядоченную карту в памяти, связав int значение «order» с каждым значением перечисления следующим образом:

 public static class EnumHelper
{
    public static Expression<Func<TSource, int>> DescriptionOrder<TSource, TEnum>(this Expression<Func<TSource, TEnum>> source)
        where TEnum : struct
    {
        var enumType = typeof(TEnum);
        if (!enumType.IsEnum) throw new InvalidOperationException();

        var body = ((TEnum[])Enum.GetValues(enumType))
            .OrderBy(value => value.GetDescription())
            .Select((value, ordinal) => new { value, ordinal })
            .Reverse()
            .Aggregate((Expression)null, (next, item) => next == null ? (Expression)
                Expression.Constant(item.ordinal) :
                Expression.Condition(
                    Expression.Equal(source.Body, Expression.Constant(item.value)),
                    Expression.Constant(item.ordinal),
                    next));

        return Expression.Lambda<Func<TSource, int>>(body, source.Parameters[0]);
    }

    public static string GetDescription<TEnum>(this TEnum value)
        where TEnum : struct
    {
        var enumType = typeof(TEnum);
        if (!enumType.IsEnum) throw new InvalidOperationException();

        var name = Enum.GetName(enumType, value);
        var field = typeof(TEnum).GetField(name, BindingFlags.Static | BindingFlags.Public);
        return field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? name;
    }
}
  

Использование было бы таким:

 case SortableTypeE.Type:
    var order = EnumHelper.DescriptionOrder((Item x) => x.MyEnum);
    result = sortOrder == SortOrder.TypeE.ASC
        ? items.OrderBy(order)
        : items.OrderByDescending(order);
    result = result.ThenBy(i => i.SomeOtherProperty);
    break;
  

который сгенерировал бы выражение, подобное этому:

 x => x.MyEnum == Enum[0] ? 0 :
     x.MyEnum == Enum[1] ? 1 :
     ...
     x.MyEnum == Enum[N-2] ? N - 2 :
     N - 1;
  

где 0,1,..N-2 — соответствующий индекс в списке значений, отсортированном по описанию.

Комментарии:

1. Более гибким подходом будет тот, который можно легко изменить, не затрагивая код, обновляя данные в таблицах базы данных. Все решения в памяти не будут хорошо работать для подкачки по большим таблицам, потому что вам нужно извлечь всю таблицу, прежде чем выполнять какую-либо сортировку

2. @MichaelFreidgeim Прежде всего, это решение для базы данных, а не в памяти (выражение будет преобразовано в CASE WHEN выражение SQL и выполнено базой данных). Во-вторых, он может легко обрабатывать динамическую локализацию, добавлять новые элементы и т.д. что сложнее с таблицей базы данных. И EF предназначен для написания запросов C #, а не чистых отчетов SQL.

3. Я понимаю, что это более старый пост, но кто-нибудь знает, как заставить это работать с перечислением, имеющим более 10 значений, чтобы избежать Case expressions may only be nested to level 10. ошибки?

4. @ewahner Вложенность уже исправлена в EFC 5.0 (выйдет в следующем месяце). Если вы не можете ждать, вы можете изменить код для использования вместо этого, например (x.MyEnum == Enum[0] ? 1 : 0) (x.MyEnum == Enum[1] ? 2 : 0) ... (x.MyEnum == Enum[N-1] ? N : 0)

5. @IvanStoev если я вас правильно понял, вы говорите, что я бы изменил выражение. Условие для выражения. Добавьте, а затем вместо передачи source.parameters[0] я бы передал source.parameters ???? Я надеюсь, что я ошибся, потому что, когда я попытался, время ожидания запроса истекло.

Ответ №2:

Альтернатива 1

Вы можете сделать это, спроектировав enum в пользовательское значение и отсортировав по нему.

Пример:

 items
    .Select(x=> new 
    {
        x,
        Desc = (
            x.Enum == Enum.One ? "Desc One" 
            : x.Enum == Enum.Two ? "Desc Two" 
            ... and so on)
    })
    .OrderBy(x=>x.Desc)
    .Select(x=>x.x);
  

Entity framework затем сгенерирует SQL что-то вроде этого

 SELECT
    *
FROM
    YourTable
ORDER BY
    CASE WHEN Enum = 1 THEN 'Desc One'
    WHEN Enum = 2 THEN 'Desc Two'
    ...and so on
    END
  

Если у вас много подобных запросов, вы можете создать метод расширения

 public static IQueryable<Entity> OrderByDesc(this IQueryable<Entity> source)
{
    return source.Select(x=> new 
    {
        x,
        Desc = (
            x.Enum == Enum.One ? "Desc One" 
            : x.Enum == Enum.Two ? "Desc Two" 
            ... and so on)
    })
    .OrderBy(x=>x.Desc)
    .Select(x=>x.x);
}
  

И вызывайте его, когда вам это нужно

 var orderedItems = items.OrderByDesc();
  

Альтернатива 2

Другим альтернативным решением является создание дополнительной таблицы, которая сопоставляет значение перечисления с описанием перечисления и присоединяет вашу таблицу к этой таблице. Это решение будет более производительным, потому что вы можете создать индекс в столбце описания перечисления.


Альтернатива 3

Если вам нужно динамическое выражение, основанное на вашем атрибуте описания перечисления, вы можете создать его самостоятельно

Вспомогательный класс

 public class Helper
{
    public MyEntity Entity { get; set; }
    public string Description { get; set; }
}
  

Получаем динамически построенное выражение

 public static string GetDesc(MyEnum e)
{
    var type = typeof(MyEnum);
    var memInfo = type.GetMember(e.ToString());
    var attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute),
        false);
    return ((DescriptionAttribute)attributes[0]).Description;
}

private static Expression<Func<MyEntity, Helper>> GetExpr()
{
    var descMap = Enum.GetValues(typeof(MyEnum))
        .Cast<MyEnum>()
        .ToDictionary(value => value, GetDesc);

    var paramExpr = Expression.Parameter(typeof(MyEntity), "x");
    var expr = (Expression) Expression.Constant(string.Empty);
    foreach (var desc in descMap)
    {
        // Change string "Enum" below with your enum property name in entity
        var prop = Expression.Property(paramExpr, typeof(MyEntity).GetProperty("Enum")); 
        expr = Expression.Condition(Expression.Equal(prop, Expression.Constant(desc.Key)),
            Expression.Constant(desc.Value), expr);
    }


    var newExpr = Expression.New(typeof(Helper));

    var bindings = new MemberBinding[]
    {
        Expression.Bind(typeof(Helper).GetProperty("Entity"), paramExpr),
        Expression.Bind(typeof(Helper).GetProperty("Description"), expr)
    };

    var body = Expression.MemberInit(newExpr, bindings);

    return (Expression<Func<MyEntity, Helper>>) Expression.Lambda(body, paramExpr);
}
  

Назовите это так

 var e = GetExpr();
items.Select(e)
    .OrderBy(x => x.Description)
    .Select(x => x.Entity);
  

Комментарии:

1. Я не думаю, что первое решение является хорошей идеей, поскольку, если я правильно понимаю, это дублировало бы описания перечисления везде, где мне нужно сортировать по нему. Однако идея создания таблицы map звучит чище, чем другие варианты. Поскольку к этим перечислениям обращаются очень часто, по вашему мнению, не будет ли проблем с производительностью при каждом обращении к другой таблице?

2. @Flash_Back Если вы не хотите дублировать описание, вы можете построить выражение динамически. Это будет немного сложно, но это возможно. Подождите, пока я отредактирую, я покажу вам, как это сделать.

3. @Flash_Back: Обращение к индексированной справочной таблице должно быть быстрым, и SQL будет кэшировать данные

4. @Flash_Back На самом деле альтернатива 2 является предпочтительным решением. 21 таблица — это не так много.

5. @Flash_Back если ваши 21 перечисления похожи (например, цвета шоу, цвета носков и т.д.), Вы можете поместить их в одну справочную таблицу с дополнительным столбцом TypeOfMyProperty

Ответ №3:

Измените столбец моей базы данных на строку (описание перечисления) вместо самого перечисления (но для меня это звучит как взлом).

Напротив, для приложения, управляемого данными, лучше описать свойство Item в справочной таблице базы данных MyItemProperty(MyPropKey,MyPropDescription) и иметь столбец MyPropKey в вашей таблице Items.

У него есть несколько преимуществ, например

  • позволяет добавлять новые значения свойств без необходимости изменять код;
  • позволяет писать отчеты SQL, содержащие всю информацию в базе данных, без написания c#;
  • оптимизацию производительности можно выполнить на уровне SQL, просто запросив одну страницу;
  • не требуется поддерживать код без перечисления.

Ответ №4:

Чтобы сохранить простоту и хорошую производительность, я бы упорядочил перечисление вручную, вам нужно сделать это только один раз, и это очень поможет

 public enum MyEnumE
{
    Enum1 = 3,
    Enum2 = 1,
    Enum3 = 2, // set the order here... 
}
  

Комментарии:

1. Да, я думал об этом, но для меня это звучит не очень удобно для сопровождения, всякий раз, когда описание нужно изменить, мне придется переупорядочивать перечисление вручную, и могут быть ошибки, если перечисление содержит много записей. Но на данный момент это все равно кажется лучшим решением, я приму его, если лучшего не будет 😉

2. Если ваше перечисление постоянно меняется, то использование перечисления, возможно, не то, что вам нужно

Ответ №5:

Вот упрощенный пример использования объединения:

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace ConsoleApplication
{
    public partial class Item
    {
        public enum MyEnumE
        {
            [Description("description of enum1")]
            Enum1,
            [Description("description of enum2")]
            Enum2
        }

        public Item(MyEnumE myEnum)
        {
            MyEnum = myEnum;
        }

        public MyEnumE MyEnum { get; set; }
    }

    class Program
    {
        private static IEnumerable<KeyValuePair<int, int>> GetEnumRanks(Type enumType)
        {
            var values = Enum.GetValues(enumType);
            var results = new List<KeyValuePair<int, string>>(values.Length);

            foreach (int value in values)
            {
                FieldInfo fieldInfo = enumType.GetField(Enum.GetName(enumType, value));
                var attribute = (DescriptionAttribute)fieldInfo.GetCustomAttribute(typeof(DescriptionAttribute));
                results.Add(new KeyValuePair<int, string>(value, attribute.Description));
            }

            return results.OrderBy(x => x.Value).Select((x, i) => new KeyValuePair<int, int>(x.Key, i));
        }

        static void Main(string[] args)
        {
            var itemsList = new List<Item>();
            itemsList.Add(new Item(Item.MyEnumE.Enum1));
            itemsList.Add(new Item(Item.MyEnumE.Enum2));
            itemsList.Add(new Item(Item.MyEnumE.Enum2));
            itemsList.Add(new Item(Item.MyEnumE.Enum1));

            IQueryable<Item> items = itemsList.AsQueryable();

            var descriptions = GetEnumRanks(typeof(Item.MyEnumE));

            //foreach (var i in descriptions)
            //  Console.WriteLine(i.Value);

            var results = items.Join(descriptions, a => (int)a.MyEnum, b => b.Key, (x, y) => new { Item = x, Rank = y.Value }).OrderBy(x => x.Rank).Select(x => x.Item);

            foreach (var i in results)
                Console.WriteLine(i.MyEnum.ToString());

            Console.WriteLine("nPress any key...");
            Console.ReadKey();
        }
    }
}
  

Ответ №6:

Мне нужно было решить похожую проблему, только мой порядок должен был быть динамическим, то есть параметр сортировки по столбцу является string .

boolean Сортировку также нужно было настроить в том смысле, который true предшествует false (например, «Активный» стоит перед «Неактивным»).

Я делюсь с вами полным кодом, чтобы вы могли сэкономить свое время. Если вы найдете места для улучшения, пожалуйста, не стесняйтесь поделиться в комментарии.

 private static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> query, SortField sortField)
{
    var queryParameterExpression = Expression.Parameter(typeof(T), "x");
    var orderByPropertyExpression = GetPropertyExpression(sortField.FieldName, queryParameterExpression);

    Type orderByPropertyType = orderByPropertyExpression.Type;
    LambdaExpression lambdaExpression = Expression.Lambda(orderByPropertyExpression, queryParameterExpression);

    if (orderByPropertyType.IsEnum)
    {
        orderByPropertyType = typeof(int);
        lambdaExpression = GetExpressionForEnumOrdering<T>(lambdaExpression);
    }
    else if (orderByPropertyType == typeof(bool))
    {
        orderByPropertyType = typeof(string);
        lambdaExpression =
            GetExpressionForBoolOrdering(orderByPropertyExpression, queryParameterExpression);
    }

    var orderByExpression = Expression.Call(
        typeof(Queryable),
        sortField.SortDirection == SortDirection.Asc ? "OrderBy" : "OrderByDescending",
        new Type[] { typeof(T), orderByPropertyType },
        query.Expression,
        Expression.Quote(lambdaExpression));

    return query.Provider.CreateQuery<T>(orderByExpression);
}
  

Общий GetPropertyExpression был немного упрощен, чтобы исключить обработку вложенных свойств.

 private static MemberExpression GetPropertyExpression(string propertyName, ParameterExpression queryParameterExpression)
{
    MemberExpression result = Expression.Property(queryParameterExpression, propertyName);
    return resu<
}
  

Вот слегка измененный код (из принятого решения) для обработки Enum упорядочения.

 private static Expression<Func<TSource, int>> GetExpressionForEnumOrdering<TSource>(LambdaExpression source)
{
    var enumType = source.Body.Type;
    if (!enumType.IsEnum)
        throw new InvalidOperationException();

    var body = ((int[])Enum.GetValues(enumType))
        .OrderBy(value => GetEnumDescription(value, enumType))
        .Select((value, ordinal) => new { value, ordinal })
        .Reverse()
        .Aggregate((Expression)null, (next, item) => next == null ? (Expression)
            Expression.Constant(item.ordinal) :
            Expression.Condition(
                Expression.Equal(source.Body, Expression.Convert(Expression.Constant(item.value), enumType)),
                Expression.Constant(item.ordinal),
                next));

    return Expression.Lambda<Func<TSource, int>>(body, source.Parameters[0]);
}
  

А также boolean порядок.

 private static LambdaExpression GetExpressionForBoolOrdering(MemberExpression orderByPropertyExpression, ParameterExpression queryParameterExpression)
{
    var firstWhenActiveExpression = Expression.Condition(orderByPropertyExpression,
        Expression.Constant("A"),
        Expression.Constant("Z"));

    return Expression.Lambda(firstWhenActiveExpression, new[] { queryParameterExpression });
}
  

Также GetEnumDescription был изменен для получения Type в качестве параметра, поэтому его можно вызывать без универсального.

 private static string GetEnumDescription(int value, Type enumType)
{
    if (!enumType.IsEnum)
        throw new InvalidOperationException();

    var name = Enum.GetName(enumType, value);
    var field = enumType.GetField(name, BindingFlags.Static | BindingFlags.Public);
    return field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? name;
}
  

SortField Это простая абстракция, содержащая string свойство столбца, по которому должна быть произведена сортировка, и direction свойства сортировки. Ради простоты я также не делюсь этим здесь.

Приветствия!