按枚举描述排序

7

我正在使用EF Code First开发ASP.NET MVC项目,遇到了一个需要按枚举描述排序的情况:

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

    public MyEnumE MyEnum { get; set; }
}

这里是SearchSortAndPaginate函数:
注:该文本原为中文,已翻译为英文。
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表可能非常庞大。
我考虑在entities.Items.Where(itemFilter)之后立即调用ToListAsync,但这将返回所有经过筛选的项,尽管我只需要一页。听起来不是一个好主意。
但如果我不这样做,EF就不会知道GetDescription()方法,我只能想到两个解决方案:
-将我的数据库列更改为字符串(枚举描述),而不是枚举本身(但对我来说听起来像是一种hack)
-或者直接在enum声明中按字母顺序排序MyEnumE组件(看起来很肮脏,也很难维护)
我陷入了困境,因为我担心如果在筛选后立即调用ToListAsync,性能会受到影响,所有其他解决方案都看起来有些问题,而我绝对需要从Search方法返回IPagedList
是否有人能想出如何处理这个问题的主意呢?
非常感谢。
更新
这是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();
}

解决方案

我最终会选择Ivan Stoev的建议,因为我的项目主要基于Linq(使用Linq而不是存储过程等),所以这个解决方案似乎比创建引用表更适合我的特殊情况。

然而,Niyoko YuliawanMichael Freidgeim的答案对我来说也非常好,任何阅读此帖子并采用更多数据库方法的人应该采用他们的解决方案;)

非常感谢大家。


你能发布一下 GetDescription 方法吗? - Ivan Stoev
抱歉,没有看到你的评论 :( 它已经发布了 :) - Flash_Back
6个回答

10
我会选择动态表达式,它更加灵活,可以轻松更改而不影响数据库表和查询。

但是,我不会在数据库中按照描述字符串进行排序,而是创建一个有序的内存映射,将每个枚举值与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
更灵活的方法是通过更新数据库表中的数据来轻松更改,而不会影响代码。所有基于内存的解决方案在处理大型表格分页时都无法很好地工作,因为您必须在执行任何排序之前检索整个表格。 - Michael Freidgeim
@MichaelFreidgeim 首先,这是数据库解决方案,不是内存解决方案(该表达式将被翻译为“CASE WHEN” SQL 表达式,并由数据库执行)。其次,它可以轻松处理动态本地化、添加新成员等操作,而这在数据库表中相对较难。而且 EF 用于编写 C# 查询,而不是纯 SQL 报告。 - Ivan Stoev
我知道这是一个较旧的帖子,但是否有人知道如何使其与具有超过10个值的枚举一起工作,以避免“Case expressions may only be nested to level 10.”错误? - ewahner
@ewahner 嵌套问题已经在 EFC 5.0 中得到解决(将于下个月发布)。如果您等不及,可以修改代码使用 +,例如 (x.MyEnum == Enum[0] ? 1 : 0) + (x.MyEnum == Enum[1] ? 2 : 0) + ... + (x.MyEnum == Enum[N-1] ? N : 0) - Ivan Stoev
如果我理解正确的话,你是说我应该将Expression.Condition更改为Expression.Add,然后不再传递source.parameters [0],而是传递source.parameters?希望我理解错了,因为我尝试过这样做,但查询超时了。 - ewahner
显示剩余3条评论

6

备选方案1

您可以通过将枚举投影到自定义值并对其进行排序来实现。

示例:

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
如果您不想重复描述,可以动态构建表达式。这可能会有点复杂,但是它是可行的。等待我的编辑,我将向您展示如何完成它。 - Niyoko
@Flash_Back 答案已更新,使用动态构建的表达式。 - Niyoko
2
@Flash_Back:参考索引引用表应该很快,而且SQL会缓存数据。 - Michael Freidgeim
1
@Flash_Back 实际上,替代方案2是首选解决方案。21张表并不多。 - Niyoko
1
如果你的21个枚举类型相似(例如节目颜色,袜子颜色等),你可以将它们放在一个引用表中,附加一列“属性类型”。 - Michael Freidgeim
显示剩余2条评论

1
将我的数据库列更改为字符串(枚举描述),而不是枚举本身(但对我来说似乎是一种黑客方法)。
相反,对于数据驱动的应用程序,最好在数据库参考表中描述Item属性MyItemProperty(MyPropKey,MyPropDescription),并在Items表中具有MyPropKey列。
它有一些好处,例如:
允许添加新的属性值而无需更改代码;
允许编写SQL报告,在数据库中拥有所有信息而无需编写C#;
可以通过请求一个页面在SQL级别上进行性能优化;
没有枚举-维护较少的代码。

0
为了保持简单和良好的性能,我建议手动排序枚举,你只需要这样做一次,它会帮助很多。
public enum MyEnumE
{
    Enum1 = 3,
    Enum2 = 1,
    Enum3 = 2, // set the order here... 
}

是的,我考虑过这个问题,但我认为它不太易于维护。每当需要更改描述时,我都必须手动重新排序枚举,并且如果枚举包含大量条目,则可能会出现错误。但目前似乎这是最好的解决方案,如果没有更好的解决方案,我将接受它 ;) - Flash_Back
2
如果你的枚举类型经常变化,那么使用枚举可能不是你所需要的。 - bto.rdz

0

这里是一个使用连接的简化示例:

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

我有一个类似的问题需要解决,只是我的排序必须是动态的,也就是按列参数进行排序是一个字符串

布尔排序也必须被定制,使得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 result;
}

这里是稍作修改的代码(来自已接受的解决方案),用于处理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]);
}

还有布尔排序。

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。为了简单起见,我在这里也不分享它。

干杯!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接