Flash_Back Flash_Back - 1 month ago 16
C# Question

Order by enum description

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 Item
{
public enum MyEnumE
{
[Description("description of enum1")]
Enum1,
[Description("description of enum2")]
Enum2,
...
}

public MyEnumE MyEnum { get; set; }
}


Here is the
Search
and
SortAndPaginate
functions:

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


The problem is that the
Item
table can be quite huge.

I thought about calling
ToListAsync
right after
entities.Items.Where(itemFilter)
but this will get back all filtered items although I only need one page. Does not sound like a good idea.

But if I don't do that
EF
won't know about
GetDescription()
mathod and I can only think about two solutions:

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

- Or alphabetically order
MyEnumE
components directly in the
enum
declaration (seems dirty and quite unmaintainable too)

I'm quite stuck since I'm concerned about performances if I call
ToListAsync
right after filtering, all other solutions seem dirty, and I absolutely need a
IPagedList
returned from the
Search
method.

Would anyone have an idea about how to deal with this issue ?

Thanks a lot.

UPDATE

Here is the
GetDescription
method (can change it if necessary):

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


SOLUTIONS

I'll finally go for Ivan Stoev's suggestion because my project is mainly based on
Linq
(using
Linq
instead of stored procedures etc.), so this solution seems more suitable to my particular case than creating reference tables.

However
Niyoko Yuliawan
's and
Michael Freidgeim
's are also really good answers to me, anyone reading this post and having a more database approach should go for their solutions ;)

Thanks a lot to all of you.

Answer

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.

Comments