How to implement a type-safe enum pattern in C#

The strongly typed enum pattern to get around the shortcomings of the enum type and provide a type safe, elegant and flexible way to represent enums in your applications

An enum is a value type. Value types in .Net are generally stored in the stack. You typically use enums to represent named constants in your application. There are two types of enums: simple enums and flag enums. While the former type is used to represent a closed set of values, the latter is used to provide support for bitwise operations using the enum values.

This article presents a discussion on enums, what they are, why they are useful, and the design constraints when using enums in applications and how to implement a type-safe enum pattern with code examples wherever appropriate.

Why should we use enums? How are they helpful?

Enums are helpful as you can set your domain-specific keywords that would be treated as integer constants -- these all help you to write clean, readable code in your application. You can use enums in switch statements as well. You can use enums in lieu of static constants in your application. Here’s a nice article from MSDN that outlines the design guidelines that should be adhered to when working with enums: https://blogs.msdn.microsoft.com/kcwalina/2004/05/18/design-guidelines-update-enum-design/

Design and usage constraints

While enums are great in helping you to write clean, readable code in your applications, they do have certain constraints as well. The .Net framework represents enums as integers. So, even if you declare an enum as having certain integer values, there is no way you can prevent your developer from assigning some other integer value to the enum you have declared. The other design constraint in using enums is that enums are not extendable. Here's exactly where the Strongly Typed Enum design pattern comes to the rescue.

The strongly typed enum pattern

The strongly typed enum pattern or the type-safe enum pattern as it is called, can be used to mitigate the design and usage constraints we discussed in the earlier section when working with enums. This pattern makes sure that the type is extensible and you can use it much like an enum.

Imagine that you were to build a custom logger that can log data to various configured log targets. Such log targets can be a text file, database or the event log. To design such a framework you might want to take advantage of an enum to declare the various log targets that the framework should provide support for. Now that we already know the design and usage constraints of an enum, let’s learn how we can build a type – safe representation of the same. Refer to the LogTarget class given below.

public sealed class LogTarget
    {
        public static readonly LogTarget Database = new LogTarget("Database");
        public static readonly LogTarget EventLog = new LogTarget("EventLog");
        public static readonly LogTarget File = new LogTarget("File");
        public readonly string TargetName;
        private LogTarget(string targetName)
        {
            TargetName = targetName;
        }
    }

Note that the class has been marked as "sealed" to prevent further inheritance. The constructor is private to prevent instantiation as an instance of this class is not needed. You just have a few public readonly fields that correspond to a particular LogTarget.

Assume now that you have implemented a class named CustomeLogger as shown below. Note that this is just for illustration purposes only - it is just a dummy class and is incomplete.

 public class CustomLogger
    {
       //Define the members of the class here
        public static void Log(string data, LogTarget logTarget)
        {
            //Write the necessary code here to log data to the appropriate log target
        }
    }

The following code snippet shows how you can use the LogTarget class we defined earlier and call the Log method on the CustomLogger class.

string data = "Data to be logged.";
CustomLogger.Log(data, LogTarget.File);

The caveats

Albeit the fact that this design is flexible, there are certain caveats in using this pattern. You should be aware that this is nullaable (unlike an enum) and the values (the log targets that we have defined in the LogTarget class) cannot be used in "switch - case" statements. Even persistence and serialization of objects that implement the pattern we discussed in this post would need you to write some extra code.

Copyright © 2017 IDG Communications, Inc.