Avoid using enums in the domain layer in C#

By Joydip Kanjilal

When working on applications, you will often need to represent a group of constants in the business logic and even in the domain layers. However, you should avoid using enumeration types, or enums, in the domain layer and instead use alternatives such as record types.

Why? In this article, we’ll explain the downsides of using enumerations in the domain layer.

[ Also on InfoWorld: The best new features in C# 12 ]

Create a console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

We’ll use this .NET 8 console application project to work with the code examples shown in the subsequent sections of this article.

What’s wrong with enums?

While enumeration types can provide flexibility in maintaining a set of constant values in your application, they often introduce tight coupling between the domain model and the code that uses it, thus limiting your ability to evolve the domain model.

Consider the following enum used to define user and administrator roles.

public enum Roles{ User, Administrator, Reviewer, SuperAdmin}

You can also use an enum to define a group of constants as shown in the following code snippet.

public enum Roles{ User = 1, Administrator = 2, Reviewer = 3, SuperAdmin = 4}

Problem 1: Encapsulation smells

Now, suppose you need to know whether a particular role relates to an administrator role. The following code snippet illustrates an extension method named IsAdmin that checks if a particular role is an Administrator or a SuperAdmin.

public static class RolesExtensions{ public static bool IsAdmin(this Roles roles) => roles == Roles.Administrator || roles == Roles.SuperAdmin;}

This lands us on the first problem with using enums in the domain layer. Although you can operate on the enum using extension methods, your code breaks the encapsulation principle because the logic for querying the model and the model you created are separate. In other words, the logic that checks the model is not within the same class. This is an anti-pattern, and a model of this type is often known as an anaemic model.

Problem 2: Spaghetti code

Another problem with enums: You might often need to use explicit casts in your application’s code to retrieve a value from an enumeration. The following line of code illustrates this.

int role = (int)Roles.User;

Explicit casts are not a good approach. They are always costly in terms of performance, and they imply that you’ve used incompatible types in your application, or that the types have not been properly defined. Using enums in your domain layer could lead to using explicit casts throughout your application, cluttering up the code and making it harder to read and maintain.

Problem 3: Naming constraints

Remember, you cannot include space characters in the names of enumeration constants in C#. Hence, the following code is not valid in C#.

public enum Roles{ Admin, Super Admin}

You can take advantage of attributes to overcome this limitation.

using System.ComponentModel.DataAnnotations;public enum Roles{ Admin, [Display(Name = "Super Admin")] SuperAdmin}

However, you will run into problems when your application needs to provide support for different locales.

Use record types instead of enums

A better alternative is to use record types. You can take advantage of record types to create an immutable type as shown in the code snippet given below.

public record Roles(int Id){ public static Roles User { get; } = new(1); public static Roles Administrator { get; } = new(2); public static Roles Reviewer { get; } = new(3); public static Roles SuperAdmin { get; } = new(4);}

An immutable object is an object that, once instantiated, cannot be altered. Hence records possess intrinsic thread-safety and immunity to race conditions. Immutable objects also make your code more readable and easier to maintain.

A significant benefit of using record types is preserving encapsulation because any extension method you write can be a part of the model itself. Remember, you cannot include any methods inside an enum.

Further, records make it easy to provide meaningful names, as the following code illustrates.

public record Roles(int Id, string Name){ public static Roles User { get; } = new(1, "User"); public static Roles Administrator { get; } = new(2, "Administrator"); public static Roles Reviewer { get; } = new(3, "Reviewer"); public static Roles SuperAdmin { get; } = new(4, "Super Admin"); public override string ToString() => Name;}

You can now access the constants of the Roles record in much the same way you can access enumeration constants.

Roles admin = Roles.Administrator;Roles user = Roles.User;Roles reviewer = Roles.Reviewer;Roles superAdmin = Roles.SuperAdmin;

When you invoke the ToString() method, the name of the constant will be displayed at the console window as shown in Figure 1.

Alternatively, you could use a class instead of a record type and then define the constants you need. However, I would always prefer a record type for performance reasons. Record types are lightweight types due to which they are much faster than classes. A record is itself a reference type but it uses its own built-in equality check, which checks by value and not by reference.

© Info World