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
2has no meaning without context - The description will likely be duplicated
stringeverywhere - 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
Further Reading
- C# Records Guide — Official Microsoft documentation on the Record type.
Version Note: The examples in this series require .NET 6 or higher
Top comments (0)