Using Enums and Custom Attributes
As a developer you know there are many aspects of writing good code. Much of the code that we write is designed to help us keep our code clean, understandable, and reusable. These aspects of our code are never seen nor understood by the end users or business partners but none the less are very important for the overall health and maintainability of our code. This is something that senior developers and/or lead developers know all too well. How many times have you had business partners or managers ask why it takes so long to write even simple code. They don't understand that we have to spend time designing and considering our code not only from the here and now, but also from the ten years from now.
Using Enums:
If you think about your daily work, you can probably identify many instances where you needed to provide a limited number of options within your code. It's possible, as the only developer on a given project, to write your code such that it only passes in specific values, but when it comes to writing reusable code, you can't always rely on your fellow developers, or even yourself, to understand the limitations of your code years from now. Thus it becomes necessary to write your code so that it enforces specific limitations and accepts only limited values. Consider the following class...
public class Character
{
/// <summary>
/// Contains all of the moves
/// </summary>
public List<int> Moves { get; private set; }
/// <summary>
/// Add a move to the Moves list
/// </summary>
public void AddMove(int Direction)
{
this.Moves.Add(Direction);
}
/// <summary>
/// Loop through the moves and
/// do something
/// </summary>
public void DoMoves()
{
foreach (int Direction in Moves)
{
switch (Direction)
{
case 0:
Console.WriteLine("North");
break;
case 1:
Console.WriteLine("South");
break;
case 2:
Console.WriteLine("East");
break;
case 3:
Console.WriteLine("West");
break;
}
}
}
}
At first glance, this class looks pretty simple and indeed it is...right now. We've got a class named Character which contains a read-only list named Moves. We've got a method named AddMove which allows us to pass in a new move, which is added to the Moves list. Finally, we've got another method named DoMoves which uses a switch in conjunction with a foreach loop to evaluate each move in the list and write out the direction. This could easily be used to populate a drop-down or some other list.
Now, it doesn't take a rocket scientist to see the flaw in this code. Heck, even a middle manager could figure out that you can only pass in a limited number of values to the AddMove method and expect to see any kind of result from the DoMoves method. It is for scenarios like this that Enums exist.
The enum keyword is used to declare an enumeration, a distinct type that consists of a set of named constants called the enumerator list.
Take a look at the following enum which we can use to limit the acceptable inputs in our AddMove method...
public enum DIRECTION
{
North,
West,
South,
East
}
This simple enumeration allows us to define a limited number of values. When we combine this with our Character class we can limit the possible inputs from the outside world. Consider our Character class with these changes...
public class Character
{
/// <summary>
/// All the directions that are possible, no
/// you can't move diagnoly, why would you
/// want to?
/// </summary>
public enum DIRECTION
{
North,
West,
South,
East
}
/// <summary>
/// Contains all of the moves
/// </summary>
public List<DIRECTION> Moves { get; private set; } = new List<DIRECTION>();
/// <summary>
/// Add a move to the Moves list
/// </summary>
public void AddMove(DIRECTION Direction)
{
this.Moves.Add(Direction);
}
/// <summary>
/// Loop through the moves and
/// do something
/// </summary>
public void DoMoves()
{
foreach (DIRECTION Direction in Moves)
Console.WriteLine(Direction);
}
}
Here you can see that we've replaced all of the Integer values previously used with our new DIRECTION enum. The AddMove method will only accept values stored in our enum. This ensures that our list will only contain valid directions and our DoMove will spit out only valid moves. However, as I said previously, we can't just consider the here and now, but we must also consider the future. What happens if business decides a year from now that our DIRECTION enum should support not just North and West, but also North West? Let's take a look at our modified enum...
public enum DIRECTION
{
North,
NorthWest,
West,
SouthWest,
South,
SouthEast,
East,
NorthEast
}
Now, you can see that we've easily modified our enum to include our new directions. However, consider how this will impact our DoMoves method. It's fine to spit out "North" and "South", but not so fine when you're spitting out "NorthWest" or "SouthWest". Since enums don't allow spaces, we're stuck with what we've got. However, all is not lost because this problem is solvable using Custom Attributes. Let's take a look at them...
Using Custom Attributes:
You may have seen attributes in the past, even if you're weren't sure what they were. One of the most common is the [Serializable] attribute sometimes attached to classes to indicate that they can be serialized and de-serialized. In our case, we'll need to define a custom attribute.
Attributes provide a powerful method of associating metadata, or declarative information, with code
First, we'll need to define our custom attribute to hold the value. Consider the following class...
public class AlternateValueAttribute : Attribute
{
public string AlternateValue { get; protected set; }
public AlternateValueAttribute(string value)
{
this.AlternateValue = value;
}
}
As you can see, we've created a class named AlternateValueAttribute that inherits from the System.Attribute base class. This class contains a single property named AlternateValue which will be used to house our alternate value. Additionally, our class has a constructor that accepts a string value that will be stored in our AlternateValue property. This gives us the custom class that we to handle our custom attribute. Remember that this custom attribute class can be used anytime you need an alternate value, so it should be located in a Core library that can be used across all of our projects. Now let's take a look at our enumeration using our new custom attribute...
public enum DIRECTION
{
[AlternateValue("North")]
North,
[AlternateValue("North West")]
NorthWest,
[AlternateValue("West")]
West,
[AlternateValue("South West")]
SouthWest,
[AlternateValue("South")]
South,
[AlternateValue("South East")]
SouthEast,
[AlternateValue("East")]
East,
[AlternateValue("North East")]
NorthEast
}
Here you can see that we've modified our original enum to include our new custom attribute. We've defined the AlternateValue property as our attribute and we've passed in the string value that will be stored in it. We can now define our two word directions using a space. This stores our custom enum values, but how do we get them out when needed? For that, we'll use a custom function to pull out our custom attribute property. Of course, since this custom attribute is a global class that can be used for anything that uses attributes, we'll also want to make our custom function a global function in the form of an extension. Again, it should be stored in a Core library that can be attached to any enum in the future. Let's take a look at this extension...
public static string GetAlternateValue(this Enum Value)
{
Type Type = Value.GetType();
FieldInfo FieldInfo = Type.GetField(Value.ToString());
AlternateValueAttribute Attribute = FieldInfo.GetCustomAttribute(
typeof(AlternateValueAttribute)
) as AlternateValueAttribute;
return Attribute.AlternateValue;
}
This is a pretty straightforward extension. You can see that it attaches itself to the Enum datatype. First, it determines the type of the value passed in which of course is Enum. Using the type, we can get the FieldInfo for the type, which returns all of the fields contained within the Enum type. Once we have the fields, we can then pull the custom attribute from our AlternateValueAttribute class using the GetCustomAttribute method. Finally, we return the AlternateValue from our new custom attribute.
Now, let's take a look at the final version of our class using our custom attribute and extensions to store and return our custom attribute values for our enum...
public class Character
{
/// <summary>
/// All the directions that are possible, no
/// you can't move diagnoly, why would you
/// want to?
/// </summary>
public enum DIRECTION
{
[AlternateValue("North")]
North,
[AlternateValue("North West")]
NorthWest,
[AlternateValue("West")]
West,
[AlternateValue("South West")]
SouthWest,
[AlternateValue("South")]
South,
[AlternateValue("South East")]
SouthEast,
[AlternateValue("East")]
East,
[AlternateValue("North East")]
NorthEast
}
/// <summary>
/// Contains all of the moves
/// </summary>
public List<DIRECTION> Moves { get; private set; } = new List<DIRECTION>();
/// <summary>
/// Add a move to the Moves list
/// </summary>
public void AddMove(DIRECTION Direction)
{
this.Moves.Add(Direction);
}
/// <summary>
/// Loop through the moves and
/// do something
/// </summary>
public void DoMove()
{
foreach (DIRECTION Direction in Moves)
Console.WriteLine(Direction.GetAlternateValue());
}
}
Summary:
So there you have it. We have an enum named DIRECTION to limit the values that can be passed into our AddMove method. This enum uses a custom attribute class named AlternateValueAttribute to store custom values that allow us to store the name of each enum in a proper format. We use a custom extension named GetAlternateValue to extract the custom attribute from our enum and return it. This could be used to display the enums in a list or drop-down in a proper format. I'm sure with a little imagination, you can think of tons of uses for custom attributes and enums.
Happy coding!
Note: You can find the complete solution for these code samples on my GitHub: Enums and Custom Attributes Sample Code