Understand the pitfalls of using enumeration types in the domain layer of your .NET applications and the advantages of using record types instead. 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. Launch the Visual Studio IDE. Click on “Create new project.” In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed. Click Next. In the “Configure your new project” window, specify the name and location for the new project. Click Next. In the “Additional information” window shown next, choose “.NET 8.0 (Long Term Support)” as the framework version you would like to use. 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. 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. Related content news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks how-to How to use DispatchProxy for AOP in .NET Core Take advantage of the DispatchProxy class in C# to implement aspect-oriented programming by creating proxies that dynamically intercept method calls. By Joydip Kanjilal Nov 14, 2024 7 mins Microsoft .NET C# Development Libraries and Frameworks news Microsoft’s .NET 9 arrives, with performance, cloud, and AI boosts Cloud-native apps, AI-enabled apps, ASP.NET Core, Aspire, Blazor, MAUI, C#, and F# all get boosts with the latest major rev of the .NET platform. By Paul Krill Nov 12, 2024 4 mins C# Generative AI Microsoft .NET feature Can Wasm replace containers? WebAssembly revolutionized browser apps, and promises to upend the server stack. How will it impact containers and Kubernetes? Six experts weigh in. By Bill Doerrfeld Nov 11, 2024 12 mins Containers Kubernetes Cloud Native Resources Videos