Monday, November 12, 2007

.NET Enum The Next Level

Introduction


In this article, I am going to explore enums a little further. I am not going to cover the basic information about enums (you can always refer to MSDN for details). My biggest beef with enum is that it only represents numeric data. It is flexible to represent different integral types, but it can not hold a string. Enums do support the ToString() function, however with limitation (I will go into it in more details, later in the article). Basically, I want to be able to make my enumeration element be associated to more than just an integer value, to achieve this I will use Reflection and Attributes and try to keep my example as simple as possible.


Using ToString() with enums


Let's create a simple enumeration...

    public enum EmployeeType
{
RegularEmployee,
StoreManager,
ChainStoreManager,
DepartmentManager,
Supervisor
}

Based on this simple plain vanilla enumeration, we can get the following information: a String representation using ToString().

   EmployeeType employee = EmployeeType.ChainStoreManager;
Console.WriteLine(employee.ToString());
Console.WriteLine(EmployeeType.ChainStoreManager.ToString());

The output is the same for both lines of code:

ChainStoreManager
ChainStoreManager

But what if I wanted to get "Chain Store Manager" with spaces? You can not create an enum type that contains spaces, your code wouldn't compile. There are a lot of solutions to this problem.



  1. Create a mapping between enums and strings (using arrays, or hashtables).
  2. Using the enum ToString() as a key for a language resource file.
  3. Use a litte bit of reflection... I will explore option 3...

Using attributes with enums


In order to associate a string with an enumeration, I used Attributes. I will start with a simple exmaple that will assocate my enumeration with a string.

    public class EnumDescriptionAttribute : Attribute
{
private string m_strDescription;
public EnumDescriptionAttribute(string strPrinterName)
{
m_strDescription = strPrinterName;
}

public string Description
{
get { return m_strDescription; }
}
}

EnumDescriptionAttribute is a simple attribute that holds a string. The attribute has a single property to return the description. For now, I am going to keep it as simple as possible. Now that I have my description attribute, I will assocate it with each enum element.

public enum EmployeeType
{
[EnumDescription("Regular Employee")]
RegularEmploye,
[EnumDescription("Store Manager")]
StoreManager,
[EnumDescription("Chain Store Manager")]
ChainStoreManager,
[EnumDescription("Department Manager")]
DepartmentManager,
[EnumDescription("On Floor Supervisor")]
Supervisor
}

Getting the attribute value from an enum


In order to get the attribute value from an enumeration, I have to use Reflection. Here is an example:

// setup the enum
EmployeeType employee = EmployeeType.ChainStoreManager;

// get the field informaiton
FieldInfo fieldInfo = employee.GetType().GetField("ChainStoreManager");

// get the attributes for the enum field
object[] attribArray = fieldInfo.GetCustomAttributes(false);

// cast the one and only attribute to EnumDescriptionAttribute
EnumDescriptionAttribute attrib =
(EnumDescriptionAttribute)attribArray[0];

// write the description
console.WriteLine("Description: {0}", attrib.Description);

Output: Chain Store Manager


The most important line of code is: FieldInfo fieldInfo = employee.GetType().GetField("ChainStoreManager");. Notice that I hardcoded the enum element name ChainStoreManager, but if you go back and look at the ToString() function, you can see that I could have used ToString() instead.


A Generic function to get the description


Notice I used ToString() on the Enum...

public static string GetEnumDescription(Enum enumObj)
{
FieldInfo fieldInfo =
enumObj.GetType().GetField(enumObj.ToString());

object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
return String.Empty;
else
{
EnumDescriptionAttribute attrib =
attribArray[0] as EnumDescriptionAttribute;

return attrib.Description;
}
}


Let's associate more than just a string to our enum elements


So far, we only associated a description to our enumeration element. However, it is fully possible to associate any type of data to our enum. To show this, I am going to create a new enumeration (similar to the first one).

public enum ManagerType
{
StoreManager,
ChainManager,
Superivor
}

I am also creating a new attribute class, ManagerAttribute, that inherits from EnumDescription, and provides two additional pieces of information (Min Salary and Max Salary).

public class ManagerAttribute : EnumDescriptionAttribute
{
private int m_intMinSalary;
private int m_intMaxSalary;
public ManagerAttribute(string strDescription,
int intMinSalary,
int intMaxSalary) : base(strDescription)
{
m_intMinSalary = intMinSalary;
m_intMaxSalary = intMaxSalary;
}

public ManagerAttribute(string strDescription)
: base(strDescription)
{

}

public int MinSalary
{
get {return m_intMinSalary;}
set { m_intMinSalary = value; }
}
public int MaxSalary
{
get { return m_intMaxSalary;}
set { m_intMaxSalary = value; }
}
}

Now, I am going to associate a ManagerAttribute to each enum element. Notice that I am using the set properties within my ManagerAttributes, so the code is more readable

public enum ManagerType
{
[Manager("Store Manager", MinSalary=40000, MaxSalary=100000)]
StoreManager,
[Manager("Chain Manager", MinSalary=50000, MaxSalary=110000)]
ChainManager,
[Manager("Store Supervisor", MinSalary=30000, MaxSalary=50000)]
Superivor
}

Next step is improving our generic function to allow us to get any type of EnumDescriptionAttribute; using Generics this is easily done!

public static T GetAttribute<T>(Enum enumObj) where T : EnumDescriptionAttribute
{
// get field informaiton for our enum element
FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());

// get all the attributes associated with our enum
object[] attribArray = fieldInfo.GetCustomAttributes(false);

if (attribArray.Length == 0)
return default(T);
else
{
// cast the attribute and return it
T attrib = (T)attribArray[0];
if (attrib != null)
return attrib;
else
return default(T);
}
}

Helper functions


So far we have got two helper functions, one to obtain a simple string description of an enum, and the other function, a more generic function to obtain any attribute that inherits from EnumDescriptionAttribute. You can add these helper functions to a class like EnumHelper; but in this example, I decided to simply add them to the existing EnumDescriptionAttribute.

public class EnumDescriptionAttribute : Attribute
{
private string m_strDescription;
public EnumDescriptionAttribute(string strEnumDescription)
{
m_strDescription = strEnumDescription;
}

public string Description { get { return m_strDescription; } }

public static string GetEnumDescription(Enum enumObj)
{
FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
return String.Empty;
else
{
EnumDescriptionAttribute attrib = attribArray[0]
as EnumDescriptionAttribute;

return attrib.Description;
}
}
public static T GetAttribute<T>(Enum enumObj)
where T : EnumDescriptionAttribute
{
FieldInfo fieldInfo = enumObj.GetType().GetField(enumObj.ToString());
object[] attribArray = fieldInfo.GetCustomAttributes(false);
if (attribArray.Length == 0)
return default(T);
else
{
T attrib = (T)attribArray[0];
if (attrib != null)
return attrib;
else
return default(T);
}
}
}

Finally... Let's see it all together


To get a string description, we can simple code this:

string desc =
EnumDescriptionAttribute.GetEnumDescription(EmployeeType.DepartmentManager);

To get a ManagerAttribute, we can code this:

ManagerAttribute manager =
EnumDescriptionAttribute.GetAttribute<ManagerAttribute>(
EmployeeType.DepartmentManager);

Console.WriteLine("Manager: {0}, Min Salary: {1}, Max Salary {2}",
attrib.Description,
manager.MinSalary,
manager.MaxSalary);

Now you can see that you can use attributes to associate addtional information to an element of an enum.


Conclusion


Okay, so you can see that it is not hard to associate additional information to your enumeration element by simply using some attributes and reflection. However, it is important to note that I do not believe attributes should replace common business objects. I simply wanted to show that an enum does not have to only represent an integral value. As you read in this article, you can associate your enum to a string or any other class. One of the nice advantages of using attributes is that it is easy to implement and it makes the code readable. You can see how readable the ManagerType enumeration is with the implementation of the ManagerAttribute. It is easy to know exactly what each enumeration item contains. I hope you enjoyed reading this article, and I welcome all comments or questions about it.

1 comment:

Fraadl said...

Hi,
nice idea. If you want to hide the attribute access, you can use Extension Methods:

public static class ManagerTypeExtensions {
public static ManagerAttribute Attribute(this ManagerType value) {
return EnumDescriptionAttribute.GetAttribute<ManagerAttribute>(value);
}

public static string Description(this ManagerType value) {
return value.Attribute().Description;
}

public static int MinSalary(this ManagerType value) {
return value.Attribute().MinSalary;
}

public static int MaxSalary(this ManagerType value) {
return value.Attribute().MaxSalary;
}
}

[TestFixture]
public class ManagerTypeTest {
[Test]
public void StoreManagerTest(){
var sm = ManagerType.StoreManager;

Assert.AreEqual("Store Manager", sm.Description());
Assert.AreEqual(40000, sm.MinSalary());
Assert.AreEqual(100000, sm.MaxSalary());
}

}