简体   繁体   中英

Get Friendly Enum Name as IQueryable

We used to store our Enums in a database table that had a Code property that corresponded to Enums in the application. The point of this was that we could give the Enums in the database a friendly name which allowed us to access it easily when needed.

Recently we stopped having an Enum table in the database and used the Description attribute on each Enum and used reflection to get the Description as the friendly name. This was great as it meant we had less tables in the database.

Here is the Description extension method:

public static string Description(this Enum source)
{
    var field = source.GetType().GetField(source.ToString());

    var attributes = (DescriptionAttribute[])field.GetCustomAttributes(
        typeof(DescriptionAttribute), false);

    return attributes.Length > 0 ? attributes[0].Description : source.ToString();
}

Now I've come across the problem when performing a Linq Select statement on the DatabaseContext (I need to keep it as an IQueryable ) that we can't use the extension method on the Enum to get the friendly name as Entity Framework wont recognise methods.

The current code is shown below. The ItemPriority is assigned the Enumn description attribute which uses reflection. This code falls over as EF can't recognise the method.

return await OrderItems(items).Skip(pageIndex * pageSize).Take(pageSize)
                .Select(item => new ItemViewModel
                {
                    Id = item.Id,
                    Description = item.Description,
                    ItemPriority = item.Priority.Description(),
                }).ToListAsync();

Is there another way I can apply friendly names to Enums or is having the friendly names in the database the only way to go? If I used the database I could just do the following:

return await OrderItems(items).Skip(pageIndex * pageSize).Take(pageSize)
                .Select(item => new ItemViewModel
                {
                    Id = item.Id,
                    Description = item.Description,
                    ItemPriority = item.Priority.Name,
                }).ToListAsync();

In general you should store the enum value in the view model and let the view format it with friendly description (using your extension method) or whatever it likes.

But let say for some reason you need that functionality in LINQ to Entities query. It can be achieved by dynamically building EF compatible value to description translation expression like this:

source == value1 ? description1 :
source == value2 ? description2 :
…
source == valueN ? descriptionN :

""

In order to do that, first you need to make the Description method generic . This way the call will include the information about actual enum type which we'll need later (since we are going to process the query expression tree, the method will not actually be called):

public static class DescriptionExtensions
{
    public static string Description<TEnum>(this TEnum source) where TEnum : struct, Enum
        => typeof(TEnum).GetField(source.ToString()).Description();

    public static string Description(this FieldInfo source)
        => source.GetCustomAttribute<DescriptionAttribute>()?.Description ?? source.Name;
}

Note that this is utilizing the C# 7.3 introduced enum constraint . For pre C# 7.3 use where TEnum : struct and assert typeof(TEnum).IsEnum inside the method.

Then we'll use a custom extension method which finds the Description method calls inside the query expression tree and replaces it with the aforementioned value to description translation expression, which is based on the TEnum type of the call. As usual with expression tree, the processing is done with custom ExpressionVisitor .

public static class QueryConverter
{
    public static IQueryable<T> Convert<T>(this IQueryable<T> source)
    {
        var expression = new ExpressionConverter().Visit(source.Expression);
        if (expression == source.Expression) return source;
        return source.Provider.CreateQuery<T>(expression);
    }

    class ExpressionConverter : ExpressionVisitor
    {
        static readonly MethodInfo EnumDescriptionMethod = Expression.Call(
            typeof(DescriptionExtensions), nameof(DescriptionExtensions.Description), new[] { typeof(ExpressionType) },
            Expression.Constant(default(ExpressionType)))
            .Method.GetGenericMethodDefinition();

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == EnumDescriptionMethod)
                return TranslateEnumDescription(Visit(node.Arguments[0]));
            return base.VisitMethodCall(node);
        }

        static Expression TranslateEnumDescription(Expression arg)
        {
            var names = Enum.GetNames(arg.Type);
            var values = Enum.GetValues(arg.Type);
            Expression result = Expression.Constant("");
            for (int i = names.Length - 1; i >= 0; i--)
            {
                var value = values.GetValue(i);
                var description = arg.Type.GetField(names[i], BindingFlags.Public | BindingFlags.Static).Description();
                // arg == value ? description : ...
                result = Expression.Condition(
                    Expression.Equal(arg, Expression.Constant(value)),
                    Expression.Constant(description),
                    result);
            }
            return result;
        }
    }
}

Now all you need to get EF compatible IQueryable<T> from query containing Description calls is to call the custom Convert method at the end:

var query = OrderItems(items)
    .Skip(pageIndex * pageSize)
    .Take(pageSize)
    .Select(item => new ItemViewModel
    {
        Id = item.Id,
        Description = item.Description,
         ItemPriority = item.Priority.Description(),
    })
    .Convert();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM