String Enums in C#: Everything You Need to Know

A thumbnail showing code. Everything you need to know about string enums in C# as a web developer.

Developers often wonder about how to choose the right data type for their needs.

If you are storing grades from a grading system, is it better to use integers or enum?

If you've been using C#, one thing that might have bugged you is the fact that there's no way to define an enumerated type that has string value.

We can reference the enum name rather than the value by using ToString(), but what happens if we want to add spaces to the name?

Trying to use string enum in C#
Using string-based enums in C# is not supported and throws a compiler error.

Since C# doesn't support enum with string value, in this blog post, we'll look at alternatives and examples that you can use in code to make your life easier.

Introduction to the Enum type

An enumeration type is a value type that has a set of named constants.

To declare an enumeration type, use the enum keyword and specify the names of enum members.

public enum Country
{
    UnitedStates,
    Canada
}

Enum operations

Since enum values are of integer types, these are the operators that we can use:

  • Enumeration comparison operators == , !=, <, >, <=, >=
  • Addition and Subtraction operators +, -
  • Enumeration logical operators ^, &, |
  • Bitwise complement operator ++, --

Default enum value

By default, the association constants of enum members are of type int. They start at zero and increase by one following the order in which you define them.

public enum Country
{
    None, 
    UnitedStates,
    Canada
}
Console.WriteLine((int)Country.None); // 0
Console.WriteLine((int)Country.UnitedStates); // 1
Console.WriteLine((int)Country.Canada); // 2

If you don't specify a value in the declaration for each enumeration element, it will get the default value from its position in the list of enums. This means that if you leave out a number or put one in wrong order, then all those numbers after it will have the default value.

public enum Country
{
    None,
    UnitedStates = 5,
    Canada
}
Console.WriteLine((int)Country.None); // 0
Console.WriteLine((int)Country.UnitedStates); // 5
Console.WriteLine((int)Country.Canada); // 6

Supported Enum Values

Enumeration's underlying type has to be integral type.

// Won't work:
public enum Country : string
{
    None,
    UnitedStates = "United States",
    Canada
}

If we try to compile the code above, we get a compile error.

Output - Compilation error:

  • Type byte, sbyte, short, ushort, int, uint, long, or ulong expected.
  • Cannot implicitly convert type 'string' to 'int'

Since the underlying type for enum needs to be numeric (integral), let's look at some enum string alternatives that will help us in situations similar to UnitedStates ⇒ "United States" above.

Use a public static readonly string

A common alternative to string enum in C# is to create a class with constant strings. This is preferable because it makes the intent of each constant clearer, and also allows you to add methods or other extended functionality.

A good example of using a static class as an alternative to enum is HttpMethods in NET Core.

A code showing HttpMethods class in .NET CORE.

It allows for writing enum-like syntax, but it returns a string. For example:

Console.WriteLine(HttpMethods.Get); // GET

static readonly vs const

Should we use static readonly or const when defining constants?

Compiler embeds const value in each assembly that uses them, giving each assembly a unique value for the constant. On the other hand, static readonly makes sure that the same reference gets passed across assemblies.

That means that using static readonly for string comparison checks for reference equality rather than value, which is faster.

The comment in the source code of "HttpMethods" explains the benefits of using static readonly as opposed to const:

We are intentionally using 'static readonly' here instead of 'const'. 'const' values would be embedded into each assembly that used them and each consuming assembly would have a different 'string' instance. Using .'static readonly' means that all consumers get these exact same 'string' instance, which means the 'ReferenceEquals' checks below work and allow us to optimize comparisons when these constants are used.

The biggest drawback of this approach is loosing a compile-time check of supporting values. For example, in the code below, compile-time check is not validating if variable httpMethod is one of the supported HttpMethods, and nothing is stopping developers from assigning it an invalid value:

string httpMethod = HttpMethods.Get;
httpMethod = "Invalid"; // this would work

Custom Enumeration Class

In the blog post from 2008, Jimmy Bogard introduces the pattern of creating a custom enumeration class which has the primary goal to minimize scattering of enum logic throughout the code base.

While using enum for control flow or more robust abstractions can be appropriate, it often leads to fragile code with many control flows checking enum values.

Instead, we can create Enumeration classes that enable all the rich features of an object-oriented language.

Implementing the abstract class Enumeration, makes it easy to use enum-like synax with string names while getting all the benefits of compiler checks:

public class CountryType : Enumeration
{
    public static CountryType UnitedStates => new(1, "United States");
    public static CountryType Canda => new(2, "Canada");

  public CountryType(int id, string name)
      : base(id, name)
  {
  }
}

Console.WriteLine(CountryType.UnitedStates); // "United States"

In this pattern, the logic is in one place. This is good when the data is static and we know it at the compile time. But if you have settings or database values, then you'll need a different solution.

Attribute and Extension methods

We can use the Description attributes to provide the string description of the enum, and with some help of extension methods we can make it easy to extract that value.

DescriptionAttribute

Usually, we need a human-readable description next to the raw numeric value of an enumeration. For this, there is the DescriptionAttribute available natively within .NET which allow to add any string as a description attribute to the enum.

Code that is using DescriptionAttribute.

The downside of using DescriptionAttribute is that it might not be available in different frameworks that we use. We need to have access to System.ComponentModel from .NET to use it.

Another downside of using DescriptionAttribute and is that it requires you to recompile your code every time you want to update the string associated with.

Note that although this is useful when you need some additional information on enumerated values in your code; It doesn't help us if we are displaying something outside of a C# becau

The good thing about this approach is that it comes with .NET through System.ComponentModel.

Extension method

Extension method allows us to extract the Description attributes easily, using just a single method call:

Country.UnitedStates.DisplayName(); //United States

User cocowalla on GitHub put together a very useful extension method. It also caches the results. Caching the results is important because reflection can be relatively expensive.

// Credits: https://gist.github.com/cocowalla

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace Acme.Utils
{
    public static class EnumExtensions
    {
        // Note that we never need to expire these cache items, so we just use ConcurrentDictionary rather than MemoryCache
        private static readonly 
            ConcurrentDictionary<string, string> DisplayNameCache = new ConcurrentDictionary<string, string>();

        public static string DisplayName(this Enum value)
        {
            var key = $"{value.GetType().FullName}.{value}";

            var displayName = DisplayNameCache.GetOrAdd(key, x =>
            {
                var name = (DescriptionAttribute[])value
                    .GetType()
                    .GetTypeInfo()
                    .GetField(value.ToString())
                    .GetCustomAttributes(typeof(DescriptionAttribute), false);

                return name.Length > 0 ? name[0].Description : value.ToString();
            });

            return displayName;
        }
    }
}

Use a Switch statement

The simplest way to convert enum values into string representation is by using a switch statement.

Examples:

public string GetCountryName(Country country)
{
    switch(country)
    {
        case Country.UnitedStates:
            return "United States";
        case Country.Canada:
            return "Canada";
        default:
            throw new ArgumentOutOfRangeException();
    }
}
public string GetCountryContinent(Country country)
{
    switch(country)
    {
        case Country.UnitedStates:
        case Country.Canada:
            return "North America";
        default:
            throw new ArgumentOutOfRangeException();
    }
}

This approach is good if Country based logic is present only in a few places, but if we add more, the behavior gets scattered all around the application.

It gets hard to keep track of all places where we have different logic based on enum value.

Map Enum Values to Desired String Using a Dictionary

Using "if/else" or "switch" statements is useful if you have a couple of values to map, but code gets complicated if we end up having many values to convert. In that case, it's better to use a dictionary.

Another alternative is to create a dictionary that maps between enums to desired string values.

public static void Main()
{
    Console.WriteLine(GetOrderErrorDescription(OrderErrorCode.NotFound));
  // Output: "Order not found or does not exist."

    Console.WriteLine(GetOrderErrorDescription(OrderErrorCode.Locked));
  // Output: "Unknown"
}

public enum OrderErrorCode
{
    Unknown,
    NotFound,
    Locked
};

public static Dictionary<OrderErrorCode, string> OrderErrorDescriptions = new Dictionary<OrderErrorCode, string>()
{
        { OrderErrorCode.Unknown, "Unknown Order Error"},
        
        { OrderErrorCode.NotFound, "Order not found or does not exist."}
};

public static string GetOrderErrorDescription(OrderErrorCode errorCode)
{
    if (OrderErrorDescriptions.TryGetValue(errorCode, out string description))
    {
        return description;
    }

    // Could't find the mapping value in the dictionary.
    // Return a default string.
    return "Unknown";
}

The biggest advantage of the dictionary approach is that we can map a single enum to multiple dictionaries.

The downside of dictionary approach is that maintaining the conversion mapping might be tedious because each change in enum needs to be reflected in the dictionary.

Avoid this approach for anything other than descriptive messaging.

Code Patterns to avoid

Avoid Converting String Value to Enum By Name

Using Enum.Parse to get enum of a string is a practice that I frequently see in web applications.

The problem with this approach is that it becomes difficult to maintain if you have many values. After all, the promise of enum type is to provide compile-time safety by providing constant definitions.

Avoid Enum.TryParse and Enum.Parse because it tries to match (parse) arbitrary input values with the explicitly defined names in the enum.

But if you end up using these methods, catch exceptions.

For the example below, what's the output when we try to parse the drop-down text?

TryParse Enum Default Value in C#

The console prints out "UnitedStates" because TryParse outs the default value, which is 0 (not null!). And in our case, UnitedStates has a value of 0.

Enum.TryParse returns false, so be sure to check that too.

Avoid basing your logic on string representation of Enum Name

Avoid basing your logic on string representation of Enum Name.

if (Country.UnitedStates.ToString() == "UnitedStates") // do something

Logic that goes after the code that compares the string representation of enum name above would change if someone changed "UnitedStates" to "US" but didn't update the "UnitedStates" string.

Looking at the example, it might seem trivial, but it is easy to forget. This mistake happens often in big systems, so try to avoid it.

Enum.IsDefined

Enum.IsDefined method tells whether an integer value or its name exists in the enumeration.

The method returns true if it exists and false otherwise.

For example, we can use the Country enumeration and see if an enum member is defined with a certain associated value:

private enum Country
{
    UnitedStates,
    Canada
}

Enum.IsDefined(typeof(Country), "UnitedStates") // true

Enum.IsDefined(typeof(Country), Country.UnitedStates) // true

Enum.IsDefined(typeof(Country), 3) // false

Conversion of explicit enum names to strings based on Enum.IsDefined, or basing the further logic on it can lead to code smells.

Conclusion

In this blog post, we introduced the enum type and C# limitation of not being able to assign it a string value type.

There are many possibilities for replacing string enums with other C# features and methods that should be considered in order to reuse code more easily and create a better experience for developers.

Alternatives to String Enum

We looked at five most popular alternatives:

  • static readonly string

    • Quick solution to comparing, but not so safe when having to assign because it allows developers to assign strings that are not supported in the static class.
  • Attribute and Extension methods

    • Description attribute paired with extension methods is a quick way to add string to numeric values.
  • Custom Enumeration Class

    • Provides a brilliant solution for cases when we need to add a lot of logic around enums.
  • Switch or if/else statements

    • Not very good at scale. Only use it when the enumeration type is tiny.
  • Dictionary mapping

    • Useful when mapping one integral value to manny string representations.

When to use enum?

Enum are a type of data in C# that we should use with caution. When to use them?

  • If you need to store only a few values and they are all known at compile time.
  • When we won't be adding logic around the string representation of the enum.
  • When the string value is for error messages, descriptions, or basic front-end messages.
  • If you want the compiler to produce an error if someone assigns an undefined value or out of bounds index.
  • When the set of enum values is small. e.g. days of the week.
  • To make code more readable, especially when it is dealing with flags.

When not to use enum?

It might be worth to consider not using enums:

  • When you go through a lot of trouble to use an enum, it might be time to use a class.
  • When the application does not know the list of enum values at compile time (static).
  • When different enums correspond to different types with different behaviors but the same interface.
  • When we expect the list of enum value to grow a lot.
  • When having to store strings representation to a database, rather than integral value.

Will C# ever support for enums to be strings?

There are no signs that C# will have string enums soon.

There's an open GitHub proposal requesting string enums to be a C# language feature.

But until then, we'll have to keep using alternatives.

Published on