In this small article, I'll show you how to implement an alternative to the enum
shipped by C# itself.
But why do all this hassle? Well enum
s have some shortcomings I don't like.
You can assign values that are not valid for the enum
This code is totally valid, will compile and will throw no runtime exception:
enum WeekendDays
{
Saturday = 0,
Sunday = 1
}
var weekend = (WeekendDays)2;
Console.WriteLine(weekend); // Prints 2
Enums, as they are value types, are initialized with 0 even if you enum doesn't start with 0
Let's take this small sample:
enum Colors
{
Red= 1,
Green = 2,
Blue = 3,
}
public class ColorMixer
{
public Colors Color { get; set; }
}
var mixer = new ColorMixer();
var color = mixer.Color; // What is Color here???
Console.WriteLine(color); // This will print 0
ToString()
uses reflection
ToString()
of an enum uses Reflection to get your name. Now you might be surprised, but this is mainly due to the fact that you can add attributes. And one special one is: Flags
. I won't go into detail if this is new to you: head over to stackoverflow.
If you want to know more, here a nice video from Nick Chapsas on Youtube.
A better enumeration
So now that we know some of the shortcomings, let us build a better version. But we still want to access the enumerations in the classic way var day = Weekdays.Monday;
. To achieve that we can use static readonly
fields. Those fields basically indicate the range of our values. These are all excepted values. So an example enumeration
would look like this:
public class Weekdays
{
public static Monday = new Weekdays(nameof(Monday));
public static Tuesday = new Weekdays(nameof(Tuesday));
Now we have to take care of a few things:
- We need a list of all possible values the consumer can give us
- The consumer is not allowed to create an out of range value
ToString()
should behave like anenum
- We can compare our enumeration against each other or even against just the key (this I'll leave as an homework for you 😉 )
- I can define custom methods
- I don't want to repeat my code. So I need this behavior in a base class
Now the last thing is easy, we will just put everything in one single abstract base class and every enumeration will derive from us like this:
public abstract class Enumeration<TEnumeration> where TEnumeration : Enumeration<TEnumeration>
public MyEnumeration : Enumeration<MyEnumeration>
{
public static readonly One = new MyEnumeration(nameof(One));
// Code left for bravery
- I don't want to repeat my code. So I need this behavior in a base class
Check, next:
Enumerate all possible value
Now we need some kind of List that holds all possible values. We know that we will use public static readonly fields, so we should just use this and get this via Reflection. Hold up, Reflection? Unfortunately we have to do it.. but we have to do it only once. On the plus side, we'll offer a public property so that the consumer could also enumerate through all the possible values:
public abstract class Enumeration<TEnumeration> where TEnumeration : Enumeration<TEnumeration>
{
public static IReadOnlyCollection<TEnumeration> All => GetEnumerations();
private static TEnumeration[] GetEnumerations()
{
var enumerationType = typeof(TEnumeration);
return enumerationType
// Get all fields, which are public and static
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
// We only want fields which are typeof our enumeration. The user could provide other fields we don't want to count
.Where(info => info.FieldType == typeof(TEnumeration))
// Create the enumeration
.Select(info => (TEnumeration)info.GetValue(null))
.ToArray();
}
}
- * We need a list of all possible values the consumer can give us
Next up: Make our class safe so that we always have a valid model
Always valid model - Our enumeration doesn't accept values which are not defined
Now this next bit comes a bit with a "personalized" flavor. You can merge the two next steps, I'll show at the end what I mean.
First, we create a protected constructor which accepts a key, this key is basically one value of enumeration. So every derived class has to provide a key as well. The derived class should only have either a private or protected c'tor that passes that key, so you have to call a public static Create
method, which does the verification if our value exists. You can see that our All
from earlier comes in handy.
Opinion: I am not a big fan of exceptions in the c'tor, I don't like to verify user input in a c'tor and eventually throw an exception. C'tors are for initializing some values and that is all. Consumers normally don't expect an exception from a c'tor therefore, I like to have a separate method. But feel free to put the Create
logic inside a public c'tor.
public abstract class Enumeration<TEnumeration>
where TEnumeration : Enumeration<TEnumeration>
{
protected Enumeration(string key)
{
// Don't allow null or empty strings. If this is a valid case for you, remove this block
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("The enum key cannot be null or empty");
}
Key = key;
}
public static IReadOnlyCollection<TEnumeration> All => GetEnumerations();
public string Key { get; }
public static TEnumeration Create(string key)
{
// Look if we have this key in our lookup
var enumeration = All.SingleOrDefault(p => p.Key == key);
// If not throw an exception. Once the enum is created it has to be valid
if (enumeration is null)
{
throw new InvalidOperationException($"{key} is not a valid value for {typeof(TEnumeration).Name}");
}
return enumeration;
}
- The consumer is not allowed to create an out-of-range value Perfect. Once that thing is created and returned the consumer can be sure it is valid. If you want to now more about Always-Valid-Model check out that awesome blog post from Vladmir Khorikov. He is a genius when it comes to domain driver design. More later.
ToString() should behave like an enum
Okay this is an easy one. Just expose the key.
public override string ToString() => Key;
- ToString() should behave like an enum
Compare against each other
As we have a defined set of allowed values which are saved inside the key, we can just compare this key of instance a and instance b.
For convenience we overload ==
so it behaves like an enum
. Bonus: GetHashCode
also works
public static bool operator ==(Enumeration<TEnumeration> a, Enumeration<TEnumeration> b)
{
if (a is null || b is null)
{
return false;
}
return a.Key.Equals(b.Key);
}
public static bool operator !=(Enumeration<TEnumeration> a, Enumeration<TEnumeration> b)
{
return !(a == b);
}
public override int GetHashCode() => Key.GetHashCode();
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (obj.GetType() != typeof(TEnumeration))
{
return false;
}
return ((TEnumeration)obj).Key == Key;
}
Usage:
public class MyEnum : Enumeration<MyEnum>
{
public static readonly MyEnum One = new MyEnum(nameof(One));
public static readonly MyEnum Two= new MyEnum(nameof(Two));
private MyEnum(string key) : base(key) {}
}
var one = MyEnum.One;
var alsoOne = MyEnum,Create("One");
var isSame = one == alsoOne; // true
isSame ) MyEnum.Two == alsoOne; // false
- Compare enumerations
HOMEWORK TIME: You can implement the same behavior, but this time the consumer passes you a string
instead of TEnumeration
. If you need help/ideas, ping me or write me in the comments.
Custom methods
Now the best part. Custom methods are totally possible. Furthermore, you can also have multiple properties. I always found this a shame that you have to use extension methods if you want to give your enum
s a bit of logic. If you do Domain-Driven-Design, this is a bit hacky, and you usually want to have your business logic inside your business object.
Small example
public class Weekdays : Enumeration<Weekdays>
{
public static readonly Weekdays Monday = new Weekdays(nameof(Monday), true);
public static readonly Weekdays Saturday = new Weekdays(nameof(Saturday), true);
private Weekdays(string key, bool isWorkday) : base(key)
{
IsWorkday = isWorkday;
}
public bool IsWorkday { get; }
public void WriteToConsole()
{
Console.Write($"{Key} is workday? {IsWorkday}");
}
}
var monday = Weekdays.Monday;
monday.WriteToConsole(); // Monday is workday? true
- I can define custom methods
The whole source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace LinkDotNet
{
public abstract class Enumeration<TEnumeration>
where TEnumeration : Enumeration<TEnumeration>
{
protected Enumeration(string key)
{
if (string.IsNullOrWhiteSpace(key))
{
throw new ArgumentException("The enum key cannot be null or empty");
}
Key = key;
}
public static IReadOnlyCollection<TEnumeration> All => GetEnumerations();
public string Key { get; }
public static bool operator ==(Enumeration<TEnumeration> a, Enumeration<TEnumeration> b)
{
if (a is null || b is null)
{
return false;
}
return a.Key.Equals(b.Key);
}
public static bool operator !=(Enumeration<TEnumeration> a, Enumeration<TEnumeration> b)
{
return !(a == b);
}
public static TEnumeration Create(string key)
{
var enumeration = All.SingleOrDefault(p => p.Key == key);
if (enumeration is null)
{
throw new InvalidOperationException($"{key} is not a valid value for {typeof(TEnumeration).Name}");
}
return enumeration;
}
public override int GetHashCode() => Key.GetHashCode();
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (obj.GetType() != typeof(TEnumeration))
{
return false;
}
return ((TEnumeration)obj).Key == Key;
}
public override string ToString() => Key;
private static TEnumeration[] GetEnumerations()
{
var enumerationType = typeof(TEnumeration);
return enumerationType
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(info => info.FieldType == typeof(TEnumeration))
.Select(info => (TEnumeration)info.GetValue(null))
.ToArray();
}
}
}
Advantages
- Always-Valid - Once you have an instance, you can be sure it is valid
- Type safe - The range of values is clear and values out of bound will produce an error
- string-representation - Instead of magic numbers in DTO's you have nice names
- I can define custom methods
Disadvantages
- Flags - With this implementation you don't have the Flag-behavior as normal
enum
s do - Not compile time constant - unfortunately, no switch cases. They need compile-time constant values
Resources
- This blog uses exactly that enumeration, if you want to see the source: here you go. It also shows how to use the enumeration with Entity Framework
- I made a reference implementation for CSharpFunctionalExtensions which also uses my Enumeration see here