Extending Enumerations (fun with Enum<enum> and Attributes)

Enums are just great, but not very user friendly once you get past simple one word names. In this article I will show you how to make them far more usable via Attributes. We will be able to give them a user friendly name, parse strings to enum based on the enum’s value, member description, and user friendly description, and created orders lists and dictionaries.

public enum enRates
{
  Main = 0, //Hourly
  OffSiteFullDay = 22, //Off Site Full Day
  OnSiteFullDay = 21, //On Site Full Day
  OffSite = 202, //Off Site Hourly
  OnSite = 201, //On Site Hourly
}

Notice how the values are not orders the way I want them and the names don’t match the user descriptions.

Now lets see what we can do by extending them through attributes:

public enum enRates
{
  [EnumInformation("Hourly", true, 1)] Main = 0,
  [EnumInformation("Off Site Full Day", true, 4)] OffSiteFullDay = 22,
  [EnumInformation("On Site Full Day", true, 5)] OnSiteFullDay = 21,
  [EnumInformation("Off Site Hourly", true, 2)] OffSite = 202,
  [EnumInformation("On Site Hourly", true, 3)] OnSite = 201,
}

Now lets see what we can do with these attributes

a. Get a user friendly description for an enum

Console.WriteLine("Enum<enRates>.Description(enRates.Main) = {0}", Enum<enRates>.Description(enRates.Main));

gets:

Enum<enRates>.Description(enRates.Main) = Hourly

b. Parse values to get enum

Console.WriteLine("Enum<enRates>.Parse(\"Main\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("Main", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"Hourly\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("Hourly", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"0\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("0", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"On Site Full Day\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("On Site Full Day", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"OnSiteFullDay\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("OnSiteFullDay", enRates.OnSiteFullDay));
Console.WriteLine("Enum<enRates>.Parse(\"UnKnown\", enRates.OnSiteFullDay) = {0}", Enum<enRates>.Parse("UnKnown", enRates.OnSiteFullDay));

gets:

Enum.Parse("Main", enRates.OnSiteFullDay) = Main
Enum.Parse("Hourly", enRates.OnSiteFullDay) = Main
Enum.Parse("0", enRates.OnSiteFullDay) = Main
Enum.Parse("On Site Full Day", enRates.OnSiteFullDay) = OnSiteFullDay
Enum.Parse("OnSiteFullDay", enRates.OnSiteFullDay) = OnSiteFullDay
Enum.Parse("UnKnown", enRates.OnSiteFullDay) = OnSiteFullDay

c. Get an ordered list for an enum (notice the order its ordered by the third value in the attribute, not the value of the member)

Console.WriteLine("ToList:");
List<enRates> list = Enum<enRates>.ToList();
foreach (enRates rate in list)
{
  Console.WriteLine("  {0}", rate);
}

gets:

ToList:
  Main
  OffSite
  OnSite
  OffSiteFullDay
  OnSiteFullDay

d. Get an ordered user friendly list of descriptions for an enum

Console.WriteLine("ToStringList:");
List<string> stringList = Enum<enRates>.ToStringList();
foreach (string rate in stringList)
{
  Console.WriteLine("  {0}", rate);
}

gets:

ToStringList:
  Hourly
  Off Site Hourly
  On Site Hourly
  Off Site Full Day
  On Site Full Day

e. Get an ordered user friendly dictionary with the member string value and user friendly value

Dictionary<string, string> stringDictionary = Enum<enRates>.ToStringDictionary();
foreach (KeyValuePair<string, string> rate in stringDictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToStringDictionary:
  Main = Hourly
  OffSite = Off Site Hourly
  OnSite = On Site Hourly
  OffSiteFullDay = Off Site Full Day
  OnSiteFullDay = On Site Full Day

f. Get an ordered user friendly dictionary with the int value of the member and user friendly value

Dictionary<int, string> dictionary = Enum<enRates>.ToDictionary();
foreach (KeyValuePair<int, string> rate in dictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToDictionary:
  0 = Hourly
  202 = Off Site Hourly
  201 = On Site Hourly
  22 = Off Site Full Day
  21 = On Site Full Day

g. Get an ordered user friendly dictionary with the member and user friendly value

Dictionary<enRates, string> valueDictionary = Enum<enRates>.ToValueDictionary();
foreach (KeyValuePair<enRates, string> rate in valueDictionary)
{
  Console.WriteLine("  {0} = {1}", rate.Key, rate.Value);
}

gets:

ToValueDictionary:
  Main = Hourly
  OffSite = Off Site Hourly
  OnSite = On Site Hourly
  OffSiteFullDay = Off Site Full Day
  OnSiteFullDay = On Site Full Day

Here’s the source code:

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public class EnumInformationAttribute : Attribute
{
  public string Description { get; internal set; }
  public bool Active { get; internal set; }
  public int? Order { get; internal set; }

  public EnumInformationAttribute(string description, bool active, int? order)
  {
    Description = description;
    Active = active;
    Order = order;
  }

  public EnumInformationAttribute(string description, bool active, int order) : this(description, active, (int?)order) { }

  public EnumInformationAttribute(string description, bool active) : this(description, active, null) { }

  public EnumInformationAttribute(string description) : this(description, true, null) { }
}

public class EnumParseException : Exception
{
  public EnumParseException() : base() { }
  public EnumParseException(string message) : base(message) { }
  public EnumParseException(string message, Exception innerException) : base(message, innerException) { }
}

public static class Enum<T>
{
  #region EnumInformation
  public class EnumInformation
  {
    public T Value { get; internal set; }
    public string DefaultDescription { get; internal set; }
    public string WithSpaces { get; internal set; }
    public bool Active { get; internal set; }
    public int Order { get; internal set; }

    internal EnumInformation(T value, string defaultDescription, string withSpaces, bool active, int order)
    {
      Value = value;
      DefaultDescription = defaultDescription;
      WithSpaces = withSpaces;
      Active = active;
      Order = order;
    }

    internal EnumInformation(T value) : this(value, value.ToString(), RemoveTitleCase(value.ToString()), true, Convert.ToInt32(value)) { }
  }
  #endregion

  #region Static Constructor
  internal static List<EnumInformation> _List = null;
  internal static Dictionary<T, EnumInformation> _Descriptions = null;
  internal static Dictionary<string, T> _LookupValues = null;

  static Enum()
  {
    string key;
    Type type = typeof(T);
    EnumInformationAttribute[] enumInformationAttributes = null;

    EnumInformation enumInformation = null;

    _List = new List<EnumInformation>();
    _Descriptions = new Dictionary<T, EnumInformation>();
    _LookupValues = new Dictionary<string, T>();

    if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) type = (new System.ComponentModel.NullableConverter(type)).UnderlyingType;

    if (!(type.IsEnum)) throw new Exception("Cannot create Enum<T> when T is not a Enum");

    foreach (T value in Enum.GetValues(type))
    {
      enumInformation = new EnumInformation(value);
      enumInformationAttributes = (EnumInformationAttribute[])(type.GetField(value.ToString()).GetCustomAttributes(typeof(EnumInformationAttribute), false));

      if (enumInformationAttributes.Length > 0)
      {
        enumInformation.DefaultDescription = enumInformationAttributes[0].Description;
        enumInformation.WithSpaces = enumInformationAttributes[0].Description;
        enumInformation.Active = enumInformationAttributes[0].Active;
        enumInformation.Order = enumInformationAttributes[0].Order ?? enumInformation.Order;
      }

      _List.Add(enumInformation);
      _Descriptions.Add(value, enumInformation);

      key = Convert.ToInt32(value).ToString().ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.Value.ToString().ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.DefaultDescription.ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
      key = enumInformation.WithSpaces.ToLower(); if (!_LookupValues.ContainsKey(key)) _LookupValues.Add(key, value);
    }

    _List.Sort((x, y) => x.Order.CompareTo(y.Order));
  }
  #endregion

  #region RemoveTitleCase
  private static string RemoveTitleCase(string value)
  {
    string result = value;

    result = System.Text.RegularExpressions.Regex.Replace(result, @"(?<begin>(\w*?))(?<end>[A-Z]+)", string.Format(@"${{begin}}{0}${{end}}", " ")).Trim();

    return result;
  }
  #endregion

  #region Description
  public static string Description(T value)
  {
    return Description(value, true);
  }
  public static string Description(T value, bool addSpaceBetweenWords)
  {
    string result = string.Empty;

    if (_Descriptions.ContainsKey(value)) result = (addSpaceBetweenWords ? _Descriptions[value].WithSpaces : _Descriptions[value].DefaultDescription);

    return result;
  }
  #endregion

  #region Count
  public static int Count()
  {
    return _Descriptions.Count;
  }
  #endregion

  #region Parse
  public static T Parse(string value)
  {
    T result;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
    {
      result = _LookupValues[value];
    }
    else
    {
      throw new EnumParseException(string.Format("No Match for \"{0}\" in \"{1}\"", value, typeof(T).Name));
    }

    return result;
  }

  public static T Parse(string value, T defaultValue)
  {
    T result;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
      result = _LookupValues[value];
    else
      result = defaultValue;

    return result;
  }

  public static bool TryParse(string value, out T parsedValue)
  {
    bool result = false;

    value = (value ?? string.Empty).ToLower();

    if (_LookupValues.ContainsKey(value))
    {
      parsedValue = _LookupValues[value];
      result = true;
    }
    else
    {
      parsedValue = default(T);
    }

    return result;
  }
  #endregion

  #region ToList
  public static List<T> ToList()
  {
    return ToList(false);
  }

  public static List<T> ToList(bool activeOnly)
  {
    List<T> result = new List<T>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(enumInformation.Value);
    }

    return result;
  }
  #endregion

  #region ToStringList
  public static List<string> ToStringList()
  {
    return ToStringList(true);
  }

  public static List<string> ToStringList(bool addSpaceBetweenWords)
  {
    return ToStringList(addSpaceBetweenWords, false);
  }

  public static List<string> ToStringList(bool addSpaceBetweenWords, bool activeOnly)
  {
    List<string> result = new List<string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add((addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion

  #region ToStringDictionary
  public static Dictionary<string, string> ToStringDictionary()
  {
    return ToStringDictionary(true);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords)
  {
    return ToStringDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords, bool useDescriptionAsKey)
  {
    return ToStringDictionary(addSpaceBetweenWords, useDescriptionAsKey, false);
  }

  public static Dictionary<string, string> ToStringDictionary(bool addSpaceBetweenWords, bool useDescriptionAsKey, bool activeOnly)
  {
    Dictionary<string, string> result = new Dictionary<string, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      string description = (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription);
      string key = (useDescriptionAsKey ? description : enumInformation.Value.ToString());

      if (!activeOnly || enumInformation.Active) result.Add(key, description);
    }

    return result;
  }
  #endregion

  #region ToDictionary
  public static Dictionary<int, string> ToDictionary()
  {
    return ToDictionary(true);
  }

  public static Dictionary<int, string> ToDictionary(bool addSpaceBetweenWords)
  {
    return ToDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<int, string> ToDictionary(bool addSpaceBetweenWords, bool activeOnly)
  {
    Dictionary<int, string> result = new Dictionary<int, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(Convert.ToInt32(enumInformation.Value), (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion

  #region ToValueDictionary
  public static Dictionary<T, string> ToValueDictionary()
  {
    return ToValueDictionary(true);
  }

  public static Dictionary<T, string> ToValueDictionary(bool addSpaceBetweenWords)
  {
    return ToValueDictionary(addSpaceBetweenWords, false);
  }

  public static Dictionary<T, string> ToValueDictionary(bool addSpaceBetweenWords, bool activeOnly)
  {
    Dictionary<T, string> result = new Dictionary<T, string>();

    foreach (EnumInformation enumInformation in _List)
    {
      if (!activeOnly || enumInformation.Active) result.Add(enumInformation.Value, (addSpaceBetweenWords ? enumInformation.WithSpaces : enumInformation.DefaultDescription));
    }

    return result;
  }
  #endregion
}
Published 10/22/2009 21:35:15 (UTC) by Ron Muth

kick it on DotNetKicks.com  Shout it  vote it on WebDevVote.com

Comments

DotNetBurner - C# Extending Enumerations (fun with Enum and Attributes)

10/22/2009 21:48:09 by DotNetBurner - C#
DotNetBurner - burning hot .NET content

DotNetShoutout Extending Enumerations (fun with Enum and Attributes)

10/23/2009 09:47:28 by DotNetShoutout
Thank you for submitting this cool story - Trackback from DotNetShoutout

Servefault.com Extending Enumerations (fun with Enum and Attributes)

11/10/2009 20:00:39 by Servefault.com
Thank you for submitting this cool story - Trackback from Servefault.com

eti  re: Extending Enumerations (fun with Enum<enum> and Attributes)
10/25/2009 09:02:47 by eti
Hi there, (it's Sunday and i'm just starting to drink my coffee so i might be wrong on the next remarks) First, to be consistent with the rest of the framework, the Parse method should throw if it's unable to parse the value and TryParse should be the no-throw version. Second, you might consider adding something to convert (safely) to and from the integer value. I've always needed this when persisting to database. Anyway thanks for the nice article and the idea of applying attributes to enums. PS: posting first time with js disabled resulted in an error page

eti  re: Extending Enumerations (fun with Enum<enum> and Attributes)
10/25/2009 09:05:08 by eti
Followup: since the default title for a comment is "re: Extending Enumerations (fun with Enum [ enum ] and Attributes)" the enum paramenter is seen as a possible tag injection by the page validation in asp and posting the comment results in an error page. You might consider changing the default title on the comments...

Ron Muth  re: Extending Enumerations (fun with Enum<enum> and Attributes)
10/26/2009 05:44:53 by Ron Muth
eti, thank you for the remarks, I have fixed the page validation on the blog, and I agree with you on the Parse, I've updated the post

Khaja Minhajuddin  re: Extending Enumerations (fun with Enum<enum> and Attributes)
10/27/2009 03:44:17 by Khaja Minhajuddin
Really interesting post, I am gonna use this in my next project. I've always had issues with the not being to attach more info to an Enum. Thank you!

Alessandro Cavalieri  re: Extending Enumerations (fun with Enum<enum> and Attributes)
10/28/2009 17:39:53 by Alessandro Cavalieri
I did someting like this but less complete. Your example is more extended. Thanks.

Leave a Comment

(required)
(required)
(optional, never published)
(optional)
(required)