Joydip Kanjilal
Contributor

Avoid using enums in the domain layer in C#

how-to
Mar 21, 20245 mins
C#Development Libraries and FrameworksMicrosoft .NET

Understand the pitfalls of using enumeration types in the domain layer of your .NET applications and the advantages of using record types instead.

wrong way road sign neonbrand cc0 via unsplash 1200x800

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 and the advantages of using record types instead.

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.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Click Next.
  7. In the “Additional information” window shown next, choose “.NET 8.0 (Long Term Support)” as the framework version you would like to use.
  8. Click Create.

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 anemic 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.

record constants IDG

Figure 1: Displaying the name of the record constant at the console window.

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.

Joydip Kanjilal
Contributor

Joydip Kanjilal is a Microsoft Most Valuable Professional (MVP) in ASP.NET, as well as a speaker and the author of several books and articles. He received the prestigious MVP award for 2007, 2008, 2009, 2010, 2011, and 2012.

He has more than 20 years of experience in IT, with more than 16 years in Microsoft .Net and related technologies. He has been selected as MSDN Featured Developer of the Fortnight (MSDN) and as Community Credit Winner several times.

He is the author of eight books and more than 500 articles. Many of his articles have been featured at Microsoft’s Official Site on ASP.Net.

He was a speaker at the Spark IT 2010 event and at the Dr. Dobb’s Conference 2014 in Bangalore. He has also worked as a judge for the Jolt Awards at Dr. Dobb's Journal. He is a regular speaker at the SSWUG Virtual Conference, which is held twice each year.

More from this author