DEV Community

Cover image for C# Smart Enums: advanced
helder sousa
helder sousa

Posted on

C# Smart Enums: advanced

The Problem: The Copy-Paste Trap

By Part 2, we had a high-performance O(1) dictionary lookup. However, if your application has dozens of status types (Order, Payment, User, etc.), you might find yourself copy-pasting that same dictionary and lookup logic repeatedly.


A Suggestion: An Advanced Generic Base Class

To keep your code DRY (Don't Repeat Yourself), we can move the "heavy lifting" into a single abstract base class. This allows your specific status classes to focus purely on defining their values while inheriting all the optimized lookup logic for free.

This implementation uses the Curiously Recurring Template Pattern (CRTP). It ensures that each specific Smart Enum (like ProductStatus) maintains its own private dictionary in memory, preventing data collisions between different types.


The Implementation:

We define a simple interfaceto ensure every value has an Id, followed by a base classproviding multiple ways to access your data safely.

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public interface ISmartEnumValue 
{ 
    int Id { get; } 
}

public abstract class SmartEnum<TValue, TSelf> 
    where TValue : class, ISmartEnumValue
    where TSelf : SmartEnum<TValue, TSelf>
{
    private static readonly Dictionary<int, TValue> _lookup = new();

    protected static TValue Register(TValue value)
    {
        _lookup[value.Id] = value;
        return value;
    }

    // Forces the static constructor of the child class to run immediately
    public static void Initialize() 
    {
        RuntimeHelpers.RunClassConstructor(typeof(TSelf).TypeHandle);
    }

    // 1. Strict Get: Throws if ID is missing (Use when existence is mandatory)
    public static TValue Get(int id)
    {
        if (_lookup.TryGetValue(id, out var value))
            return value;

        throw new KeyNotFoundException(
            $"Value with ID {id} not found in {typeof(TSelf).Name}");
    }

    // 2. Safe Get: Returns null if ID is missing (Use for optional data)
    public static TValue? GetOrDefault(int id) => _lookup.GetValueOrDefault(id);

    // 3. Pattern Matching Get: Returns bool (Standard .NET 'Try' pattern)
    public static bool TryGet(int id, out TValue? value)
    {
        return _lookup.TryGetValue(id, out value);
    }

    public static bool Exists(int id) => _lookup.ContainsKey(id);
    public static IEnumerable<TValue> GetAll() => _lookup.Values;
}
Enter fullscreen mode Exit fullscreen mode

Usage examples:

Once initialized, you have total flexibility in how you consume your Smart Enums.

// Initialize once at startup (e.g., Program.cs)
ProductStatus.Initialize();

// Example 1: Strict access (Expects ID to exist)
var status = ProductStatus.Get(1); 
Console.WriteLine(status.Description);

// Example 2: Safe access with null check
var maybeStatus = ProductStatus.GetOrDefault(99);
if (maybeStatus != null) { /* Do something */ }

// Example 3: Pattern matching for clean branching
if (ProductStatus.TryGet(2, out var foundStatus))
{
    Console.WriteLine($"Found: {foundStatus?.Description}");
}
Enter fullscreen mode Exit fullscreen mode

Why this is a Robust Architectural Choice

  • Flexible Consumption: You can choose between exceptions, nulls, or booleans based on your specific business flow.
  • Strict Type Safety: The TSelf constraint ensures that ProductStatus.Get() returns a ProductStatusValue directly, with no casting required.
  • No Reflection: By using Initialize(), we avoid the performance overhead of assembly scanning.
  • Zero Boilerplate: Your specific Enum classes focus entirely on the data, while the engine remains encapsulated in the base.

Try it yourself


Version Note

This advanced implementation requires .NET 6 or higher. The use of CRTP and modern generic constraints ensures type safety and performance in modern C# environments.


Further Reading & Resources


Let's Discuss!

How are you currently handling magic numbers? Do you prefer a simple Record approach or a Generic Base Class for larger systems?

Drop a comment below and let’s talk Clean Code!


Top comments (0)