Utilities for Enumeration Field Attribute – DEVELOPPARADISE
25/06/2018

Utilities for Enumeration Field Attribute


Introduction

This is an idea to define enumeration along with associated data in a simple form in C#. Mapping enumeration values with those related information requires a certain amount of code, and it makes a bit of mess around the code of enumeration definitions. This is a solution to improve such code to be simpler, more intuitive, and more readable by borrowing the power of attribute.

Problem

Whe defining a property that have a limited set of valid values, we use enumerations. By nature, it makes such values readable, but it is neither enough to be key name nor enough to display to users. For example, when there’s following key values and their captions for display:

Key Caption
PS-4 Sony PlayStation 4
XBOX-ONE Microsoft Xbox One
SWITCH Nintendo Switch

The keys are not numeric values so that they cannot be enumeration values, and some of the key names cannot be enumeration value name because they have hyphen. Therefore, we need to have methods to translate enum value to the key and to translate the key back to enum value.

When it comes to display them to users as options for the property, it is not good UX to display enum value names. We also need to provide a caption for each enum value.

The following test code simulates that users firstly receive a list of options and choose one of them, and system aquires an enumeration value from the chosen option’s key.

[TestFixture] public class EnumUtilityTest {     [Test]     public void Test()     {         var options = GameConsoleUtility.GetAll();          var optionDisplay = options             .ToDictionary(o => o.ToKey(), o => $"{o.ToCaption()} [{o.ToKey()}]");          var selectedKey = "XBOX-ONE";         var enumValue = selectedKey.ToGameConsole();          Assert.That(enumValue, Is.EqualTo(GameConsole.XboxOne));     } } 

Background

Before we get into my solution, let’s see two other attempts firstly. Those also solve the proble, but I don’t like them, which is why I’m writting this tips. Those might be too nun-sense, but please consider them only as a contrast against my solution that comes later.

One Attempt

The following implementation can cover the requirements by using 3 sets of one-to-one mappings. But I don’t like it.

In such implementation, if we need to add a new enumeration value, we need to make sure to add a line in other two parts, which would be error-prone if we’re working in a team with many developers. In addition, it is not much readable, where the mapping definition for each enumeration value is separated.

    public enum GameConsole     {         PS4,         XboxOne,         Switch     }      public static class GameConsoleUtility     {         static Dictionary<gameconsole, string=""> _keyMapping = new Dictionary<gameconsole, string="">         {             { GameConsole.PS4, "PS-4" },             { GameConsole.XboxOne, "XBOX-ONE" },             { GameConsole.Switch, "SWITCH" },         };          static Dictionary<string, gameconsole=""> _antiKeyMapping;          static Dictionary<gameconsole, string=""> _captionMapping = new Dictionary<gameconsole, string="">         {             { GameConsole.PS4, "Sony PlayStation 4" },             { GameConsole.XboxOne, "Microsoft Xbox One" },             { GameConsole.Switch, "Nintendo Switch" },         };          public static string ToKey(this GameConsole enumValue)         {             string key;             if (!_keyMapping.TryGetValue(enumValue, out key))                 throw new Exception($"No mapping is specified for {enumValue.ToString()}");             return key;         }          public static GameConsole ToGameConsole(this string key)         {             if (_antiKeyMapping == null)             {                 _antiKeyMapping = new Dictionary<string, gameconsole="">();                 foreach (var pair in _keyMapping)                     _antiKeyMapping.Add(pair.Value, pair.Key);             }              GameConsole enumValue;             if (!_antiKeyMapping.TryGetValue(key, out enumValue))                 throw new Exception($"Invalid key for GameConsole: {key}");             return enumValue;         }          public static string ToCaption(this GameConsole enumValue)         {             string caption;             if (!_captionMapping.TryGetValue(enumValue, out caption))                 throw new Exception($"No mapping is specified for {enumValue.ToString()}");             return _captionMapping[enumValue];         }          public static List<gameconsole> GetAll()         {             return _keyMapping.Keys.ToList();         }     } </gameconsole></string,></gameconsole,></gameconsole,></string,></gameconsole,></gameconsole,>

Another Attempt

The implementation below combins the sepate mappings to one set of mappings. I think this is better at the point of readability. However, I cannot like it either.

Having such much code for each enumeration type sounds not ideal for me.

    public enum GameConsole     {         PS4,         XboxOne,         Switch     }      public static class GameConsoleUtility     {         internal class GameConsoleMapping         {             public GameConsole EnumValue { get; set; }             public string Key { get; set; }             public string Caption { get; set; }             public GameConsoleMapping(GameConsole gameConsole, string key, string caption)             {                 EnumValue = gameConsole;                 Key = key;                 Caption = caption;             }         }          static List<gameconsolemapping> _mappings = new List<gameconsolemapping>         {             new GameConsoleMapping(GameConsole.PS4, "PS-4", "Sony PlayStation 4"),             new GameConsoleMapping(GameConsole.XboxOne, "XBOX-ONE", "Microsoft Xbox One"),             new GameConsoleMapping(GameConsole.Switch, "SWITCH", "Nintendo Switch"),         };          public static string ToKey(this GameConsole enumValue)         {             var key = _mappings.FirstOrDefault(m => m.EnumValue == enumValue)?.Key;             if (key == null)                 throw new Exception($"No mapping is specified for {enumValue.ToString()}");             return key;         }          public static GameConsole ToGameConsole(this string key)         {             var enumValue = _mappings.FirstOrDefault(m => m.Key == key)?.EnumValue;             if (!enumValue.HasValue)                 throw new Exception($"Invalid key for GameConsole: {key}");             return enumValue.Value;         }          public static string ToCaption(this GameConsole enumValue)         {             var caption = _mappings.FirstOrDefault(m => m.EnumValue == enumValue)?.Caption;             if (caption == null)                 throw new Exception($"No mapping is specified for {enumValue.ToString()}");             return caption;         }          public static List<gameconsole> GetAll()         {             return _mappings.Select(m => m.EnumValue).ToList();         }     } </gameconsole></gameconsolemapping></gameconsolemapping>

So, I need a solution that is:

  • more readable and intuitive
  • simpler code for each enumeration type

Solution

Firstly, I want put such mapping code somewhere close to enumeration type definition.

Secondly, I want to make each enumeration definition and code around simpler.

For my first desire, I thought usage of attribute would be ideal, and came out with the solution below. For the later one, it needs commonization.

Let’s see the code I extracted for the common use first.

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public class AliasAttribute : Attribute {     public string[] Aliases { get; }     public AliasAttribute(params string[] aliases)     {         Aliases = aliases;     }     public string this[int i]     {         get         {             if (Aliases.Length > i)                 return Aliases[i];             return null;         }     } }  public static class AttributeUtility {     class AliasMapping     {         public IConvertible EnumValue { get; }         public AliasAttribute Aliases { get; }         public AliasMapping(IConvertible enumValue, AliasAttribute aliases)         {             EnumValue = enumValue;             Aliases = aliases;         }     }      static Dictionary<Type, IEnumerable<AliasMapping>> _mappingsMap = new Dictionary<Type, IEnumerable<AliasMapping>>();      static IEnumerable<T> getCustomAttributes<T, U>(this U enumValue)         where T : Attribute         where U : struct, IConvertible     {         var fieldName = enumValue.ToString();         return typeof(U).GetField(fieldName).GetCustomAttributes<T>(true);     }      static IEnumerable<AliasMapping> getMappings<T>() where T : struct, IConvertible     {         IEnumerable<AliasMapping> mappings;         if (!_mappingsMap.TryGetValue(typeof(T), out mappings))         {             mappings = Enum.GetValues(typeof(T)).Cast<T>().Select(e => e.getCustomAttributes<AliasAttribute, T>().Select(a => new AliasMapping(e, a)).FirstOrDefault());             _mappingsMap.Add(typeof(T), mappings);         }         return mappings;     }      public static string ToAlias<T>(this T enumValue, int index) where T : struct, IConvertible     {         var mapping = enumValue.getCustomAttributes<AliasAttribute, T>().FirstOrDefault();         if (mapping == null)             throw new Exception($"No mapping is defined for {enumValue.ToString()}");         return mapping[index];     }      public static T ToEnum<T>(this string alias, int index) where T: struct, IConvertible     {         var mapping = getMappings<T>().FirstOrDefault(m => m.Aliases[index] == alias);         if (mapping == null)             throw new Exception($"Invalid alias for {nameof(T)}: {alias}");         return (T)mapping.EnumValue;     }      public static List<T> GetAll<T>() where T : struct, IConvertible     {         return Enum.GetValues(typeof(T)).Cast<T>().ToList();     }  } 

Having such code as common, here’s the code around an enumeration definition.

public enum GameConsole {     [KeyCaption(key: "PS-4", caption: "Sony PlayStation 4")]     PS4,     [KeyCaption(key: "XBOX-ONE", caption: "Microsoft Xbox One")]     XboxOne,     [KeyCaption(key: "SWITCH", caption: "Nintendo Switch")]     Switch }  [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] public class KeyCaptionAttribute : AliasAttribute {     public KeyCaptionAttribute(string key, string caption) : base(key, caption)     {     } }  public static class GameConsoleUtility {     public static string ToKey(this GameConsole enumValue)     {         return enumValue.ToAlias<GameConsole>(0);     }      public static GameConsole ToGameConsole(this string key)     {         return key.ToEnum<GameConsole>(0);     }      public static string ToCaption(this GameConsole enumValue)     {         return enumValue.ToAlias<GameConsole>(1);     }      public static List<GameConsole> GetAll()     {         return AttributeUtility.GetAll<GameConsole>();     } } 

Here’s much less code and thus tidy. Not only it is more readable, but also easier in code maintainance, in contrast with the former two attempts.

I hope it could help you to make your code tidy and to improve quality in readability and maintainability.

Remarks

For caption or something to display on UI, there would be another concirn, which is about globalization. I regard it is another problem. This article focuses on associated data with enumeration. For example, even in globalized program or system, you could have one and more associated keys, such as caption key, tool-tip message key, sub-option references.

History

2018/06/26: Published first edition.