Enumerations

August 25, 2018 — September 7, 2018

Enums in C# are very useful. Unfortunately, using them introduces risk into our programs in both obvious and subtle ways.

Let’s explore these problems and look at a safer alternative.

enum

An Enumeration or Enumerated type is a type that defines a set of named values. An instance of that type can only be one of those values.

A typical example:

enum TransactionState
{
  Queued = 1,
  Ongoing = 2,
  Done = 3,
  Cancelled = 4
}

And its usage:

class Transaction
{
  public TransactionState State { get; private set; }
  //...

  public string GetLabel()
  {
    switch (State)
    {
      case TransactionState.Queued:
        return "It is queued"; break;
      case TransactionState.Ongoing:
        return "It is ongoing"; break;
      case TransactionState.Done:
        return "It is done"; break;
      case TransactionState.Cancelled:
        return "It is cancelled"; break;
      default
        return "it is a mistery";
    }
  }

  public void Retry()
  {
    if (State == TransactionState.Cancelled)
      // retry logic...
  }
}

At first glance this looks quite fine: We’re using a proper type instead of just numbers for the different states and the code using the enum is readable enough.

Still, the “proper type” part is where our dissapointment starts: you can have an enum with a value wich was not defined as one of its possible values. var legit = (TransactionState)7 is not a compile time error.

Historically enums are a formalization of the common pattern of defining constants like this:

const int TRANSACTIONSTATE_QUEUED = 1;
const int TRANSACTIONSTATE_ONGOING = 2;
const int TRANSACTIONSTATE_DONE = 3;
const int TRANSACTIONSTATE_CANCELLED = 4;

Today still, enums keep a close connection to ints. Sometimes this connection can be useful. For example:

  • when an enum is represented in serialization or communication by an int.
  • when an enum represents the options of a bitfield.
  • when it makes sense to add or subtract the values of an enum

Unfortunately, when the enum values don’t have a int equivalent and the enum is better represented by something else, like a two letter ISO country code, the int representation can not be opted out of.

Another, more subtle issue is the use of switch case. If a function accepts a parameter of type TransactionState, it creates the expectation that it can handle any of its possible values. This expectation is not backed by a guarantee. When we add a possible value to our enum TransactionState, every switch case on it has become incomplete and probably incorrect but our code still compiles.

This is not good for maintainability. Not handling the full set of possible values of an enum should result in a compilation failure.

Rigid design

Often a concept starts out as a simple set of possible values, but over time either the concept itself or our understanding of the domain will evolve. The modeling of this concept needs to evolve along, but enums are not extendible. We can not add properties or behaviors to them.

We either have to replace the enum with a class at that point or settle for a worse design by keeping these associated properties and behaviors somewhere else.

An Enumeration class as safer alternative

I was first introduces to this idea when Jan Van Ryswyck showed me this blogpost from 2008. The Enumeration class I’m currently using, is an improvement on that implementation. Mostly by stealing some ideas from F#.

Let’s start by rewriting our example:

class TransactionState: Enumeration<TransactionState>
{
  private TransactionState(string name): base(name) {}
  
  public static readonly TransactionState Queued = new TransactionState(nameof(Queued));
  public static readonly TransactionState Ongoing = new TransactionState(nameof(Ongoing));
  public static readonly TransactionState Done = new TransactionState(nameof(Done));
  public static readonly TransactionState Cancelled = new TransactionState(nameof(Cancelled));
  
  public T Match<T>(
    T queued,
    T ongoing,
    T done,
    T cancelled)
  {
    if (this == Queued)
      return queued;
    if (this == Ongoing)
      return ongoing;
    if (this == Done)
      return done;
    if (this == Cancelled)
      return cancelled;
      
    throw new NotImplementedException();
  }
}

And its usage:

class Transaction
{
  public TransactionState State { get; private set; }
  //...

  public string GetLabel()
    => State.Match(
        queued: "It is queued",
        ongoing: "It is ongoing",
        done: "It is done",
        cancelled: "It is cancelled");

  public void Retry()
  {
    if (State == TransactionState.Cancelled)
      // retry logic...
  }
}

So, what’s the difference? The first observation is that the definition of TransactionState has become more verbose. This is caused by moving logic from outside to inside the Enumeration. Instead of an unsafe switch statement, we now have something like exhaustive pattern matching through the Match method.

This Match method gives us a compile time guarantee that adding a possible value to our Enumeration will have to be dealt with everywhere we match on TransactionState.

But what about that if statement?

class Transaction
{
  ...
  public void Retry()
  {
    if (State == TransactionState.Cancelled)
      // retry logic...
  }
}

The if here is like a switch case where all but one possible values are ignored. What if a new retryable state is added? We should use TransactionState.Match here as well.

It seems cumbersome to have to do a Match every time you need to know whether a Transaction.IsCancelled or not; The thing is though, you don’t need to. Extract just this match expression to a method and let its name reflect why you want to know whether it is cancelled. CanRetry, IsRetryable or maybe the opposite IsFinal. Our code is now more robust under change and communicates its intention better. Hurray!

You will still need a match for every different reason to match, but I consider that a good thing. It makes intent explicit.

Since TransactionState is a class now, it can contain this definition if it needs to.

class TransactionState : Enumeration<TransactionState>
{
  //...
  public bool IsRetryable => Match(
    queued: false,
    ongoing: false,
    done: false,
    cancelled: true
  );
}

class Transaction
{
  //...
  public void Retry()
  {
    if (State.IsRetryable)
      // retry logic...
  }
}

If we’re trying to avoid if statements, how about conditional behaviour like this?

class TransactionState : Enumeration<TransactionState>
{
  //...
  public void Cancel()
  {
    if (State == TransactionState.Queued)
      Queue.DeQueue(this.TransactionId);
      
    if (State == TransactionState.Ongoing)
      Queue.Abort(this.TransactionId);
  }
}

The match approach can be used here as well:

class TransactionState : Enumeration<TransactionState>
{
  //...
  public void Cancel()
    => State.Match<Action>(
      queued: () => Queue.DeQueue(this.TransactionId),
      ongoing: () => Queue.Abort(this.TransactionId),
      done: () => {},
      cancelled: () => {}			
    )();
}

Notice that we do have to take an action for every possible state, but that we can make the explicit decision that that action should do nothing.

Mapped Enumerations

Often a value originates in an external system and needs to be mapped to an Enumeration. When that value leaves our system again, it needs to be translated back to the external representation.

For this I’m using MappedEnumerations.

Specifically IntMappedEnumeration

public class Priority : IntMappedEnumeration<Priority>
{
  protected Priority(string name, int mappedValue) : base(name, mappedValue) { }
  
  public static readonly Priority High = new Priority(nameof(High), 1);
  public static readonly Priority Medium = new Priority(nameof(Medium), 2);
  public static readonly Priority Low = new Priority(nameof(Low), 3);
}

and StringMappedEnumeration.

public class Language : StringMappedEnumeration<Language>
{
  protected Language(string name, string mappedValue) : base(name, mappedValue) { }
  
  public static readonly Language Dutch = new Language(nameof(Dutch), "NL");
  public static readonly Language French = new Language(nameof(French), "FR");
  public static readonly Language English = new Language(nameof(English), "EN");
}

Having an explicit distinction between how a value is represented inside and outside of our system, allows for better naming inside our system. It also makes the impact of changes more predictable.

Sorting

Since the mapped value is usualy not under my control, I don’t use it for sorting.

Both Enumeration and IndexEnumeration are IComparable. The sort order is the order in which the values are defined in the class.

I typically dont’t like this kind of hard to discover implicit behavior, but it avoids having to pass a sorting parameter to every instance of Enumeration. I’m still on the fence about this one.

From int or string to Enumeration

Since not every int or string represents a valid Enumeration, the constructor is not public.

Every Enumeration has a static Create method to get an Option<Enumeration> based on the name of the Enumeration.

For example:

Language GetLanguageOrDefault(string languageCode)
  => Language.Create(languageCode).ValueOr(Language.English);

MappedEnumerations additionally have a public static FromMappedValue method to create an Option<Enumeration> from the external representation.

For example:

public static class MessageMapper
{
  public static Message Map(MessagePoco poco)
    => new Message(
      language: Language.FromMappedValue(poco.Language).ValueOrThrowException(),
      priority: Language.FromMappedValue(poco.Priority).ValueOr(Priority.Medium)
      //...
    );
}

MVC modelbinding

In my current ASP.NET Core MVC project, I have custom modelbinding for Enumeration, so my form models and controller actions can use the Enumeration types directly.

This binding only uses Enumeration.Create and not Enumeration.FromMappedValue. if I need to cross system boundaries, I only want to do that explicitly.

[HttpPost]
public async Task<IActionResult> SetLanguage(Language language)
{
  // if Language.Create returned None, there will be a validation error for language
  if (!ModelState.IsValid)
    return BadRequest();
    
  // ...
}

Enumerators

Just like with an enum, you can iterate over the members of an Enumeration.

// enum
foreach (var value in (Language[])Enum.GetValues(typeof(Language)))
{
  //...
}

// Enumeration
foreach (Language.Enumerators)
{
  //...
}    

One drawback…

The major thing that bothers me is that the code below will work with an enum, but not with an Enumeration.

[Theory]
[InlineData(Language.Dutch)]
ALanguageDependentTest(Language language)
{
  //...
}

As an enum, Language.Dutch is a compile-time constant, but as an Enumeration it isn’t. The not very elegant workaround looks like this:

[Theory]
[InlineData(nameof(Language.Dutch))]
ALanguageDependentTest(string languageCode)
{
  var language = Language.CreateOrThrowException(languageCode);
  //...
}

Is all of this practical?

Yes. I have been using this in my current project for a year now.

  • In this project I have
    • 36 concrete T : Enumeration<T> classes
    • 20 concrete T : IntMappedEnumeration<T> classes
    • 2 concrete T : StringMappedEnumeration<T> classes
  • The largest Match statement has 40 parameters.
    This may sound unwieldy, but especially in this case I find it more pleasant to use Match than non-exhaustive if or switch case statements. When I changed the original implementation into a Match, several forgotten cases where discovered.
  • Many of these Enumerations have properties or methods beyond the enumerators themselves.
  • They help a lot towards the “if it compiles it’s probably correct” experience.

My Enumeration class

This is a slightly simplified version of the Enumeration class I’m currently using.

using Core.Extensions;
using Optional;
using Optional.Collections;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Core.Types
{
  public class IntMappedEnumeration<T> : MappedEnumeration<T, int>
  where T : IntMappedEnumeration<T>
  {
      protected IntMappedEnumeration(string name, int index)
          : base(name, index) { }

      public static explicit operator int(IntMappedEnumeration<T> obj)
          => obj._mappedValue;

      public static Option<T> FromMappedValue(int? mappedValue)
          => mappedValue.ToOption().FlatMap(v => CreateFromMappedValue(v));
  }

  public class StringMappedEnumeration<T> : MappedEnumeration<T, string>
  where T : StringMappedEnumeration<T>
  {
      protected StringMappedEnumeration(string name, string mappedValue)
          : base(name, mappedValue) { }
  }

  public abstract class MappedEnumeration<T, TMapped> : Enumeration<T>
  where T : MappedEnumeration<T, TMapped>
  {
      private static IDictionary<TMapped, Enumeration<T>> _mapLookup 
          = new Dictionary<TMapped, Enumeration<T>>();
          
      protected readonly TMapped _mappedValue;

      protected MappedEnumeration(string name, TMapped mappedValue)
          : base(name)
      {
          _mappedValue = mappedValue;
          _mapLookup.Add(_mappedValue, this);
      }

      public TMapped ToMappedValue()
          => _mappedValue;

      public static Option<T> FromMappedValue(TMapped mappedValue)
          => CreateFromMappedValue(mappedValue);

      protected static Option<T> CreateFromMappedValue(TMapped mappedValue)
      {
          EnsureStaticFieldsAreInitialized();
          return mappedValue.SomeNotNull()
              .Filter(key => _mapLookup.ContainsKey(key))
              .Map(key => (T)_mapLookup[key]);
      }
  }

  public class Enumeration<T> : IComparable
    where T : Enumeration<T>
  {
    private static IDictionary<string, Enumeration<T>> _nameLookup
      = new Dictionary<string, Enumeration<T>>();

    protected readonly string _name;
    protected readonly int _sortOrder;

    protected Enumeration() { }

    protected Enumeration(string name)
    {
      _name = name.ToKebabCase();
      _nameLookup.Add(_name, this);
      _sortOrder = _nameLookup.Count;
    }

    public override string ToString()
      => _name;

    public static explicit operator string(Enumeration<T> obj)
      => obj._name;

    public static Option<T> Create(string name)
    {
      EnsureStaticFieldsAreInitialized();
      return name.SomeNotNull()
        .Map(key => key.ToKebabCase())
        .Filter(key => _nameLookup.ContainsKey(key))
        .Map(key => (T)_nameLookup[key]);
    }
    
    public static T CreateOrThrowException(string name)
      => Create(name).ValueOrThrowException();

    protected static void EnsureStaticFieldsAreInitialized()
    {
      // The initialization of the static fields of a class 
      // can be postponed until the first static field of that class gets used.
      // Since the values of the static fields of our concrete Enumeration classes 
      // register themselves in a lookuplist when constructed,
      // lookup (and thus Create) fails when these fields have not been instantiated yet.
      // Currently, we use reflection to work around this.

      if (_nameLookup.IsEmpty())
        typeof(T)
          .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
          .FirstOrNone()
          .Map(f => f.GetValue(null));
    }

    public override bool Equals(object obj)
      => obj is Enumeration<T> enumeration
        ? _name == enumeration._name
        : false;

    public override int GetHashCode() => _name.GetHashCode();

    public int CompareTo(object obj)
    {
      if (obj is Enumeration<T> enumeration)
        return obj == null
          ? 1
          : _sortOrder.CompareTo(enumeration._sortOrder);
      else
        throw new ArgumentException(
          $"An instance of type \"{obj.GetType().Name}\" " +
          $"can not be compared to an instance of type \"{nameof(Enumeration<T>)}\"");
    }

    private static IEnumerable<T> _enumerators;
    public static IEnumerable<T> Enumerators
    {
      get
      {
        if (null == _enumerators)
        {
          EnsureStaticFieldsAreInitialized();
          _enumerators = _nameLookup
            .OrderBy(v => v.Value._sortOrder)
            .Select(v => (T)v.Value);
        }
        return _enumerators;
      }
    }
  }
}