Order by enum description

前端 未结 6 1599
佛祖请我去吃肉
佛祖请我去吃肉 2021-01-13 10:55

I am working on an ASP.NET MVC projet using EF code first, and I am facing a situation where I need to order by an enum description:

public partial class Ite         


        
相关标签:
6条回答
  • 2021-01-13 11:12

    Here's a simplified example using a join:

    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();
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-13 11:17

    To keep it simple and with a good performance, I would order the enum manually, you only have to do it once, and it will help a lot

    public enum MyEnumE
    {
        Enum1 = 3,
        Enum2 = 1,
        Enum3 = 2, // set the order here... 
    }
    
    0 讨论(0)
  • 2021-01-13 11:17

    I had a similar problem to solve, only that my ordering had to be dynamic, that is the sort by column parameter is a string.

    The boolean sorting also had to be customized in that sense that true comes before false (e.g. 'Active' is before 'Inactive').

    I'm sharing here the complete code with you, so you can spare your time. In case you find spots for improvement, please feel free to share in a comment.

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

    The shared GetPropertyExpression has been simplified a bit, to exclude the nested property handling.

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

    Here is the slightly modified code (from the accepted solution) to handle the Enum ordering.

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

    And the boolean ordering as well.

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

    Also the GetEnumDescription has been modified to receive the Type as the parameter, so it can be called without a generic.

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

    The SortField is a simple abstraction containing the string column property to be sorted upon and the direction of the sort. For the sake of simplicity I am also not sharing that one here.

    Cheers!

    0 讨论(0)
  • 2021-01-13 11:18

    I would go with dynamic expression. It's more flexible and can easily be changed w/o affecting the database tables and queries.

    However, instead of sorting by description strings in the database, I would create ordered map in memory, associating int "order" value with each enum value like this:

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

    The usage would be like this:

    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;
    

    which would generate expression like this:

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

    where 0,1,..N-2 is the corresponding index in the value list sorted by description.

    0 讨论(0)
  • 2021-01-13 11:23

    Change my database column to a string (the enum description) instead of the enum itself (but sounds like a hack to me).

    Opposite, for data-driven application it's better to describe Item property in the database reference table MyItemProperty(MyPropKey,MyPropDescription) and have MyPropKey column in your Items table.

    It has a few benefits, e.g.

    • allow to add new property values without need to change code;
    • allow to write SQL reports having all information in the database without writing c#;
    • performance optimisation can be done on SQL level just by requesting one page;
    • no enum - less code to maintain.
    0 讨论(0)
  • 2021-01-13 11:25

    Alternative 1

    You can do it by projecting enum into custom value and sort by it.

    Example:

    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 then will generate SQL something like this

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

    If you have a lot of query like this, you can create extension method

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

    And call it when you need it

    var orderedItems = items.OrderByDesc();
    

    Alternative 2

    Another alternative solution is to create additional table that map enum value to enum description and join your table to this table. This solution will be more performant because you can create index on enum description column.


    Alternative 3

    If you want dynamic expression based on your enum description attribute, you can build yourself

    Helper Class

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

    Get dynamically built expression

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

    Call it like this

    var e = GetExpr();
    items.Select(e)
        .OrderBy(x => x.Description)
        .Select(x => x.Entity);
    
    0 讨论(0)
提交回复
热议问题