A better enumeration - Type safe from start to end

03/10/2021

In this small article I'll show you how to implement an alternative to the enum which is shipped by C# itself. But why doing all this hassle? Well enums have some short comings I don't like.

You can assign values which 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 mainly due to the fact that you can attributes. An one special one is: Flags. I don'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 now some of the shortcomings, let us build a better version. But we want to still 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 an enum
  • 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 which passes that key, so that 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 have also multiple propreties. I always found this a shame that you have to use extension methods if you want to give your enums a bit of logic. If you do Domain-Driven-Design this is a bit hacky and normally you 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 enums 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
2
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x