C# Smart Enums: escape magic number hell

The Problem: Magic Number Hell

We’ve all seen code like this:

if (product.StatusId == 2) 
{
    // What is 2? Pending? Deleted? Available?
    product.StatusName = "?"; 
}

This is Magic Number Hell. It’s hard to read, impossible to maintain, and a magnet for bugs. You have to search the codebase and figure out the meaning:

This code has several issues:

  • The number 2 has no meaning without context
  • The description will likely be duplicated string everywhere
  • It leads to errors when setting id or description
  • Refactoring is a struggle

Let’s look at the traditional ways often adopted, and finally see the Smart Enum pattern and why it would be the best option.

Traditional approaches : why they fall short ?

const Properties

Following you see a valid often used approach using constants:

public static class Statuses
{
    public const int Available = 1;
    public const int Unavailable = 2;

    public const string AvailableDescription = "Available";
    public const string UnavailableDescription = "Unavailable";
}

if (product.StatusId == Statuses.Available)
{
    product.StatusName = Statuses.AvailableDescription;
}

Downsides:

  • Descriptions are separate from IDs
  • Can’t iterate
  • Not type-safe

enum with Attributes

Built-in enum is better than const, and often used as well. See the following example of its usage:

public enum Statuses
{
    [Description("Available")]
    Available = 1,
    [Description("Unavailable")]
    Unavailable = 2
}

// Usage
product.StatusId = (int)Statuses.Available;
product.StatusName = GetDescription(Statuses.Available);

But it also has downsides:

  • Reflection is slow when used to retrieve and item description (the following method shows an example using reflection)
  • Alternatives to Reflection are messy helper methods
  • It´s verbose
  • It´s not LINQ-friendly
// A reflection version method for retrieving the item description
public static string GetDescription(Statuses status)
{
    var field = status.GetType().GetField(status.ToString());
    var attribute = field?.GetCustomAttribute<DescriptionAttribute>();
    return attribute?.Description ?? status.ToString();
}

The Solution: Smart Enum pattern with C# Records

Although traditional approaches are valid, Smart Enum pattern offers a modern way to solve the magic number nightmare and without any downside. See how it looks like:

public record StatusValue(int Id, string Description);

public static class Status
{
    public static readonly StatusValue Pending = new(1, "Pending Approval");
    public static readonly StatusValue Available = new(2, "Available for Sale");
    public static readonly StatusValue OutOfStock = new(3, "Out of Stock");

    public static readonly StatusValue[] All = { Pending, Available, OutOfStock };
}

How to use it today

Considering the prior Smart Enum sample type, its usage would produce some lines like the following:

product.StatusId = Status.Available.Id;
product.StatusName = Status.Available.Description;

if (product.StatusId == Status.Available.Id) { ... }

Even without complex architecture, you can use standard LINQ to clean up your logic:

// Retrieve safely
var status = Status.All.SingleOrDefault(x=>x.Id == userInput);
if (status!=null)
{
   product.StatusId = status.Id;
   product.StatusDescription = status.Description;+
}

// Compare with confidence
if (product.StatusId == Status.Available.Id) { ... }

// dropdown list
var dropdownList = Status.All.Select(s => new
{ 
        Key = s.Id, 
        Value= s.Description 
});

Comparison

Approach Type-safe LINQ-ready Performance Maintainable Metadata
Magic numbers No No Fast No No
enum Yes Sort of Slow Sort of Limited
const Sort of No Fast Sort of
Smart Enum Yes Yes Fast Yes Rich

Try It yourself

Part 2

In the part 2 of the Smart Enum series, we will optimize this post O(n) smart enum version for performance using O(1) approach, also removing the LINQ boilerplate!

Leave a Reply