Developer Tips: C# Enum Alternative

For today's post, our guest blogger, Andrew Hinkle, shows us an alternative to enums.

Written by Andrew Hinkle • Last Updated: • Develop •
Open Laptop with Code on the screen

Enums are great for storing constants, but they lack being strongly typed because you can't create an instance of them. I want the best of both worlds. I want my switch statements to work off constants like enums. I want to pass these constants in a strongly typed container.

While switch statements are often seen as a code smell there are still plenty of situations where they make since. Other Enum alternatives have been posted by Steve Smith (Ardalis) and Jimmy Bogard (Los Techies) that are solving a similar solution and there are similarities here.

As an example, I'm working on a project where I want to pass around the ServiceLifetime in a strongly typed container.I created the ServiceLifetimeWrapper with the same constants plus NotSet. I always add a NotSet to my enum's to avoid invalid values parsing to the first enum value. I carry that mentality with me into the wrapper class, but as you'll see, I don't really have that issue here when I parse back the instance using the FromName method.

To accomplish this enum/class combo I created a backing field to store a list of the available instances of the class as a singleton. It uses reflection magic to create the list based on the public constants within the class, but only once. The FromName method returns the SingleOrDefault and throws an exception if the name is not in the list.

I wanted to move this logic into a base class or helper class. However, I wanted to keep the constructor private or at least protected. I tried changing the "new" statement to Activator.CreateInstance and added the first parameter as an object array, but it still could not find the constructor.

If anyone wants to take a turn at trying to abstract this method as a generic to a base class, post your solution and let me know. Thanks!

This enum alternative is fairly lightweight and works with switch statements. The main difference being that instead of storing your actual value in a string variable, you pass around and use an instance of the class and access the name via the Name property. I've added plenty of unit tests to show usage.

Use the enum alternative class when you are:

  • Only interested in the name
  • You want to take advantage of switch statements
  • You want to keep the content in a strongly typed container

Following Singleton techniques, this class is thread-safe and tested in a Parallel.For test.

The code base (Tips.EnumAlternative) can be found in full along with another example in my github tips:

ServiceLifetimeWrapper.cs

// Create all of the ServiceLifetimeWrapper with a singleton list of the enumeration.
// Inspiration: https://csharpindepth.com/articles/singleton
internal class ServiceLifetimeWrapper
{
    public const string NotSet = "NotSet";

    // Transient lifetime services are created each time they're requested. This lifetime works best for lightweight, stateless services.     public const string Transient = "Transient";
    // Scoped lifetime services are created once per request.     public const string Scoped = "Scoped";
    // Singleton lifetime services are created the first time they're requested.  Every subsequent request uses the same instance.     public const string Singleton = "Singleton";
    // Inspiration: https://stackoverflow.com/questions/10261824/how-can-i-get-all-constants-of-a-type-by-reflection     // Go through the list and only pick out the constants     // IsLiteral determines if its value is written at compile time and not changeable     // IsInitOnly determines if the field can be set in the body of the constructor     // for C# a field which is readonly keyword would have both true      //   but a const field would have only IsLiteral equal to true     // Don't forget to add the .ToList() or the property will be reevaluated     //   and you will not have the same instance of the class.
    // I wanted to move this logic into a base class or helper class     // however I wanted to keep the constructor private or at least protected.     // I tried changing the "new" statement to Activator.CreateInstance     // and added the first parameter as an object array, but it still     // could not find the constructor.  If anyone wants to take     // a turn at trying to abstract this method as a generic     // to a base class, post your solution and let me know.  Thanks!     private static readonly IEnumerable<ServiceLifetimeWrapper> ServiceLifetimes =         typeof(ServiceLifetimeWrapper)         .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)         .Where(fieldInfo => fieldInfo.IsLiteral && !fieldInfo.IsInitOnly)         .Select(fieldInfo => new ServiceLifetimeWrapper(fieldInfo.Name))         .ToList();
    private ServiceLifetimeWrapper(string name)     {         Name = name;     }
    public string Name { get; }
    public static ServiceLifetimeWrapper FromName(string name)     {         var serviceLifetime = ServiceLifetimes.SingleOrDefault(x => x.Name == name);         if (serviceLifetime == null) throw new ArgumentException($"Could not get a ServiceLifetimeWrapper from name: {name}");
        return serviceLifetime;     } }

ServiceLifetimeWrapperTest.cs

[TestClass]
public class ServiceLifetimeWrapperTest
{
    [TestMethod]
    [DataRow("notSet")]
    [DataRow("TranSient")]
    [DataRow("SCOPED")]
    [DataRow("S1ngleton")]
    public void FromNameWhenPropertyDoesNotExistThenThrowException(string expectedServiceLifeTimeWrapperName)
    {
        Assert.ThrowsException<ArgumentException>(() =>
                ServiceLifetimeWrapper.FromName(expectedServiceLifeTimeWrapperName),
                $"Could not get a ServiceLifetimeWrapper from name: {expectedServiceLifeTimeWrapperName}"
            );
    }

    [TestMethod]     [DataRow("NotSet")]     [DataRow("Transient")]     [DataRow("Scoped")]     [DataRow("Singleton")]     public void FromNameWhenPropertyDoesExistThenReturnInstance(string expectedServiceLifeTimeWrapperName)     {         var actualServiceLifeTimeWrapper = ServiceLifetimeWrapper.FromName(expectedServiceLifeTimeWrapperName);         Assert.AreEqual(expectedServiceLifeTimeWrapperName, actualServiceLifeTimeWrapper.Name);     }
    [TestMethod]     [DataRow("NotSet")]     [DataRow("Transient")]     [DataRow("Scoped")]     [DataRow("Singleton")]     public void FromNameWhenPropertyDoesExistThenReturnSingleInstance(string expectedServiceLifeTimeWrapperName)     {         var list = new List<ServiceLifetimeWrapper>();         for (var i = 0; i < 100; i++)         {             list.Add(ServiceLifetimeWrapper.FromName(expectedServiceLifeTimeWrapperName));         }
        var firstItem = list.First();         Assert.AreEqual(expectedServiceLifeTimeWrapperName, firstItem.Name);
        foreach (var actualServiceLifeTimeWrapper in list)         {             Assert.AreEqual(firstItem.Name, actualServiceLifeTimeWrapper.Name);             Assert.AreSame(firstItem, actualServiceLifeTimeWrapper);         }     }
    [TestMethod]     [DataRow("NotSet")]     [DataRow("Transient")]     [DataRow("Scoped")]     [DataRow("Singleton")]     public void FromNameWhenPropertyDoesExistAndRunInParallelThenReturnSingleInstance(         string expectedServiceLifeTimeWrapperName)     {         var queue = new ConcurrentQueue<ServiceLifetimeWrapper>();
        Parallel.For(0, 1000000,             (i) => queue.Enqueue(ServiceLifetimeWrapper.FromName(expectedServiceLifeTimeWrapperName)));
        var firstItem = queue.First();         foreach (var actualServiceLifeTimeWrapper in queue)         {             Assert.AreEqual(expectedServiceLifeTimeWrapperName, actualServiceLifeTimeWrapper.Name);             Assert.AreSame(firstItem, actualServiceLifeTimeWrapper);         }     }
    [TestMethod]     [DataRow("NotSet")]     [DataRow("Transient")]     [DataRow("Scoped")]     [DataRow("Singleton")]     public void FromNameThenSwitchOnName(string expectedServiceLifeTimeWrapperName)     {         var actualServiceLifeTimeWrapper = ServiceLifetimeWrapper.FromName(expectedServiceLifeTimeWrapperName);         var actualNameServiceLifeTimeWrapperName = string.Empty;
        switch (actualServiceLifeTimeWrapper.Name)         {             case ServiceLifetimeWrapper.NotSet:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Scoped:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Transient:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Singleton:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             default:                 Assert.Fail("Switch case not found.");                 break;         }         Assert.AreEqual(expectedServiceLifeTimeWrapperName, actualNameServiceLifeTimeWrapperName);     }
    [TestMethod]     [DataRow("NotSet")]     [DataRow("Transient")]     [DataRow("Scoped")]     [DataRow("Singleton")]     public void FromNameThenSwitchOnConstant(string expectedServiceLifeTimeWrapperName)     {         var actualNameServiceLifeTimeWrapperName = string.Empty;         switch (expectedServiceLifeTimeWrapperName)         {             case ServiceLifetimeWrapper.NotSet:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Scoped:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Transient:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             case ServiceLifetimeWrapper.Singleton:                 actualNameServiceLifeTimeWrapperName = expectedServiceLifeTimeWrapperName;                 break;             default:                 Assert.Fail("Switch case not found.");                 break;         }         Assert.AreEqual(expectedServiceLifeTimeWrapperName, actualNameServiceLifeTimeWrapperName);     } }

Conclusion

The code base (Tips.EnumAlternative) can be found in full along with another example in my github tips:

Are you frustrated with the Enum's lack of class features? Do you have an alternative implementation that meets the same objectives? Do you have any suggestions on how to improve this solution? Post your comments below and let's discuss.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Andrew Hinkle

Andrew Hinkle has been developing applications since 2000 from LAMP to Full-Stack ASP.NET C# environments from manufacturing, e-commerce, to insurance.

He has a knack for breaking applications, fixing them, and then documenting it. He fancies himself as a mentor in training. His interests include coding, gaming, and writing. Mostly in that order.

comments powered by Disqus