String Enums in C# — When, Why, and How?
Today, we'll talk about enum values: do they matter and if they do, are strings a better choice than numbers to represent them? We'll also see how to use string enums in C#.
Key Takeaways
- Values of enum members are generally not important. However, in certain cases, they can be useful.
- Strings often represent enums better than numbers. Unfortunately, C# does not natively support string enums.
- I've created a package called StrEnum that helps creating string enums in C#. StrEnum supports EF Core, ASP.NET Core, Dapper, and JSON.
1. Enum values are often secondary
When dealing with enums in C#, you generally only care about the names of their members and not about their numeric values. That is because the purpose of enums is to model a choice — and all you need for that is the names of the options to choose from:
public enum Season { Spring, Summer, Autumn, Winter } var season = Season.Winter; season == Season.Summer; // false
As long as you only refer to the options by name, you don't care about their underlying numeric values. Do you really need to know that Winter
's value is 3 to say that it's not equal to Summer
?
2. But sometimes they are useful
There are three cases where numeric values of enum members are important.
2.1. Enum values carry meaning
The most obvious case is using an enum to group a set of related constants. You give each number a name and put it under the same enum roof with the other similar members:
public enum PizzaSize { Personal = 10, Small = 12, Medium = 14, Large = 16 } public int NumberOfSlices(PizzaSize pizzaSize) { var diameterInInches = (int)pizzaSize; return (int)Math.Round(Math.PI * diameterInInches * diameterInInches / 80); } var slices = NumberOfSlices(PizzaSize.Large); // 10
In this case, the values of the members are important as they carry meaning so they have to be specified explicitly.
2.2. You are creating a multi-choice enum
An enum can also represent a combination of choices. Such enum must have the [Flags]
attribute applied to it and its members must act as bit fields, i.e. have the powers of two as their values:
[Flags] public enum BurgerSide { Fries = 0b0001, // 1 Coleslaw = 0b0010, // 2 Salad = 0b0100 // 4 }
var friesAndSalad = BurgerSide.Fries | BurgerSide.Salad; // 5 var everything = BurgerSide.Fries | BurgerSide.Coleslaw | BurgerSide.Salad; // 7
2.3. The value of an enum crosses the app boundary
C# apps rarely run isolated or produce no side effects. It's more common for an app to store its state in a database, interact with a third-party API, or provide data to a front-end.
If the data that your app exchanges with the third parties contain enums, it's a good practice to specify the values of their members:
public enum PostReaction { Like = 0, HeartEyes = 1, ClappingHands = 2, Fire = 3 }
Once PostReaction
travels outside of your app, all the systems that process it now need to know the meaning behind the numbers that represent the enum. The enum values become a part of the contract between your app and the third parties, so it is better to be clear and specify those values explicitly.
3. Strings as enum values
When enum values matter, using strings instead of numbers can often bring more value.
3.1. Strings carry more meaning than numbers
String values are self-descriptive. They often have meaning outside of the C# code.
Imagine that you need to track user reactions to blog posts and store them in a database. You create the following enum and assign the values to each of the items:
public enum PostReaction { Like = 0, HeartEyes = 1, ClappingHands = 2, Fire = 3 }
You complete the feature and everyone's happy: your app stores the reactions and displays the correct counters for the posts.
But then your data team decides to run analytics on users' reactions. They query the reactions table and see the following:
"What do all those Reaction numbers mean?" — they ask you. You open the definition of the PostReaction
enum and send them the name and the value pair of each reaction type.
Sometime later you are asked to add a new reaction, a SurprisedFace
. That's easy. You add a new enum member with the value of 4
.
In a few days, you receive another message from the data team, asking what that new 4
reaction is.
From now on you have an additional task to keep the data team aware of all the changes to the collection of reactions. And you rarely have just one such enum in your app. There can also be order statuses, account types, user preferences, and so on. You end up describing the values of each enum either in a reference table in your database or somewhere deep in your company's wiki. Whatever option you choose, you and your team now have to keep those tables in sync with the enums.
But what if you could do something like this:
public enum PostReaction: string { Like = "LIKE", HeartEyes = "HEART_EYES", ClappingHands = "CLAPPING_HANDS", Fire = "FIRE" }
By doing so, your C# code would still function the same but PostReactions
would be stored the following way:
The values of PostReaction
would be self-descriptive and unlike randomly-chosen numbers, they would have meaning on their own. Such values would have meaning even if the source code of your app is lost.
Unfortunately, such string enums are not supported by C#. However, there are ways to simulate them which I will cover further down in this post.
3.2. Strings are more human-friendly
What if both numbers and strings can meaningfully represent an enum? Countries are a good example. Rather than coming up with a random numeric ID per country, you can use the ISO 3166 Country Codes standard. For each country, it defines both a string and a numeric code. Ukraine, for example, has a numeric code of 840
and a three-letter code of "UKR"
and "UKR"
to me is much more meaningful than 840
.
When choosing between numbers and strings as an underlying enum type, prefer strings. As you've seen before, strings are self-descriptive and are more human-friendly than numbers. They make data easy to read and understand which will save your team time to debug, investigate, and analyze your systems.
4. When not to use strings
There are two cases where you should prefer using numbers over strings for enums.
4.1. Performance is critical
Databases are usually faster in sorting and indexing numbers than strings. Keep this in mind, especially if you are dealing with big volumes of data.
Strings also take up more space. That can increase the amount of data transferred in and out of your app as well as the size of your data files, indexes, and backups.
Having said that, I would always start with keeping your data as human-readable and self-descriptive as possible and only optimize for speed, bandwidth, and storage if they become an issue.
4.2. Numbers are more meaningful
Sometimes, numbers represent enum values better than strings. In such cases, stick with the standard numeric enums:
public enum PodiumPlace { First = 1, Second = 2, Third = 3 }
5. String enums in C#
As I mentioned before, C# does not natively support string enums. Luckily, they are easy to simulate.
5.1. Introducing StrEnum
I've created a NuGet package, StrEnum, that allows to create string-based enums which are type safe, work similarly to native C# enums, and can be used with EF Core, ASP.NET Core, and JSON.
PostReaction
, built with StrEnum, looks like this:
public class PostReaction: StringEnum<PostReaction> { public static readonly PostReaction Like = Define("LIKE"); public static readonly PostReaction HeartEyes = Define("HEART_EYES"); public static readonly PostReaction ClappingHands = Define("CLAPPING_HANDS"); public static readonly PostReaction Fire = Define("FIRE"); }
Now you can use it in the similar way you'd use a normal C# enum:
var clappingHands = PostReaction.ClappingHands; clappingHands.ToString(); // "ClappingHands" (string)clappingHands; // "CLAPPING_HANDS" var like = PostReaction.Parse("LIKE"); clappingHands == like; // false
5.2. String enum limitations
Since string enums are not natively supported by C#, they have a few minor limitations compared to regular enums.
5.2.1. No [Flags] support
String enum variable can only represent a single choice. That's because C# [Flags] enums use bitwise operations to combine multiple options, and these operations do not apply to strings.
5.2.2. Cannot be used in attributes
String enums cannot be used in attributes, since attributes only accept constant expressions as arguments, and string enum members are created at runtime. Assuming UserRole
is a string enum that describes user roles, the following wouldn't compile:
[Authorize(Roles = (string)UserRole.Admin)]
5.2.3. Cannot be used as default parameter values
String enum members cannot be used as default parameter values for optional parameters; such default values can only be null. The reason is the same as in the attributes case above: string enum members are not compile-time constants.
// error: default parameter for 'pizza' must be a compile-time constant public int NumberOfSlices(PizzaSize pizza = PizzaSize.Medium) { ... } // this works, but the method signature is less clear: public int NumberOfSlices(PizzaSize? pizza = null) { pizza = pizza ?? PizzaSize.Medium; ... }
5.2.4. Require bulkier switch
statements
You can't use string enums in regular, pre C# 7 switch
statements, since values for case
clauses have to be constants. In other words, the following won't compile:
switch (weather) { case Weather.Freezing: // Compile-time error: "A constant value is expected" PutOnACoat(); break; }
But there is a workaround. You can write slightly bulkier switch
statements with the help of C# 8 property patterns and when
case guards:
switch (weather) { case { } when weather == Weather.Freezing: PutOnACoat(); break; case { } when weather == Weather.Rainy: TakeAnUmbrella(); break; }
You can also write concise switch
expressions like this:
var description = weather switch { _ when weather == Weather.Freezing => "It's cold out there", _ when weather == Weather.Rainy => "It's raining cats and dogs", _ => "Looks nice" };
Just be careful with switch
statements: they can often be harmful, although sometimes their benefits are worth the cost.
6. Summary
Enum members are generally referenced by name and their numeric values are often secondary.
However, there are cases where numeric enum values are useful: when the enum groups constants with meaningful values, when it is a multi-choice enum, or when it travels in or out of your C# application. In all these cases it is better to specify enum values explicitly.
Strings often represent enum values better than numbers. That's because strings are self-descriptive and human-readable. I generally recommend using strings over numbers, although there are a few cases where numbers can be a better fit.
C# does not natively support string enums, that's why I came up with StrEnum — a NuGet package that simulates string enums that can be used with EF Core, ASP.NET Core, Dapper, and JSON.
And what is your take on string enums? Have you used them in production? Do you think they are worth the additional effort? Don't hesitate to share your stories and opinions in the comments!