Не используйте перечисления в доменном слое C#
Вашему вниманию предлагается перевод статьи Joydip Kanjilal – Avoid using enums in the domain layer in C#.
В этой статье вы узнаете о подводных камнях использования перечислений (enum) в доменном слое ваших .NET-приложений и о преимуществах применения record-типов вместо них.

При разработке приложений часто требуется представить набор констант в бизнес-логике, включая доменный слой. Однако, лучше избегать перечислений (enum) в доменном слое, отдавая предпочтение альтернативам, таким как типы записей (record types).
Почему? В этой статье мы разберем недостатки применения перечислений в доменном слое и преимущества использования записей вместо них.
Создайте проект консольного приложения в Visual Studio
Прежде всего, давайте создадим проект консольного приложения .NET Core в Visual Studio. Если Visual Studio 2022 уже установлена, выполните следующие шаги, чтобы создать новый проект консольного приложения .NET Core:
- Запустите Visual Studio IDE.
- Нажмите “Create new project”.
- В окне “Create new project”, выберите “Console App (.NET Core)” из списка доступных шаблонов.
- Нажмите “Next”.
- В окне “Configure your new project”, укажите название и расположение нового проекта.
- Нажмите “Next”.
- В окне “Additional information”, выберите “.NET 8.0 (Long Term Support)” в качестве фреймворка, который вы хотите использовать.
- Нажмите “Create”.
Мы будем использовать проект консольного приложения на .NET 8 для работы с кодом примеров из этой статьи.
Что не так с перечислениями?
Хотя типы перечислений предоставляют гибкость в работе с наборами константных значений вашего приложения, они часто привносят тесную связь между доменными моделями и кодом, который их использует, поэтому ограничивается возможность развивать доменную модель.
Рассмотрим следующее перечисление, используемое для определения ролей.
public enum Roles
{
User,
Administrator,
Reviewer,
SuperAdmin
}
Вы также можете использовать перечисления для определения группы констант, как показано в следующем примере.
public enum Roles
{
User = 1,
Administrator = 2,
Reviewer = 3,
SuperAdmin = 4
}
Проблема 1: Запахи инкапсуляции
Теперь предположим, что вам нужно узнать, относится ли конкретная роль к административным ролям. В следующем примере приведен метод расширения IsAdmin, который проверяет что указанная роль – это Administrator или SuperAdmin.
public static class RolesExtensions
{
public static bool IsAdmin(this Roles roles)
=> roles == Roles.Administrator ||
roles == Roles.SuperAdmin;
}
Это приводит нас к первой проблеме использования перечислений в доменном слое. Хотя вы можете работать с перечислением с помощью методов расширения, ваш код нарушает принцип инкапсуляции, потому что логика обращения к модели и сама модель разделены. Другими словами, логика проверки модели находится не в том же классе. Это анти-паттерн и подобные модели называют анимичными.
Проблема 2: Спагетти-код
Другая проблема с перечислениями: в коде приложения может потребоваться явное приведения типа для получения значения из перечисления. Это иллюстрируется в следующей строке кода.
int role = (int)Roles.User;
Явные приведения – не лучший подход. Они сказываются на производительности и обычно указывают на проблему в системе типов – либо типы несовместимы, либо их некорректно определили. Использование перечислений в доменном слое может вести к использованию явных приведений во всем приложении. Это захламляет код и усложняет его читабельность и дальнейшую поддержку.
Проблема 3: Ограничения именования
В языке C# имена констант перечислений не могут содержать пробельные символы. Следовательно, следующий C#-код синтаксически неверен.
public enum Roles
{
Admin,
Super Admin
}
Вы можете воспользоваться атрибутами, чтобы обойти это ограничение.
using System.ComponentModel.DataAnnotations;
public enum Roles
{
Admin,
[Display(Name = "Super Admin")]
SuperAdmin
}
Однако проблемы возникнут, когда в вашем приложении потребуется поддержка нескольких локализаций.
Используйте типы записей вместо перечислений
Лучшей альтернативой будет использование типов записей (record types). С помощью записей вы можете создать неизменяемый тип, как показано в следующем примере.
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);
}
Неизменяемый объект – это объект, который после создания нельзя модифицировать. Таким образом, записи обладают врожденной потокобезопасностью и не подвержены состоянию гонки. Неизменяемые объекты также делают ваш код проще для чтения и поддержки.
Значительным преимуществом использования записей является сохранение инкапсуляции, поскольку любые написанные вами методы расширения можно перенести в саму модель. Внутрь перечислений вы не можете добавлять методы.
Кроме того, записи позволяют легко предоставлять осмысленные названия, как показано в следующем примере.
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;
}
Теперь вы можете получить доступ к константам записи Roles практически таким же способом, как к константам перечислений.
Roles admin = Roles.Administrator; Roles user = Roles.User; Roles reviewer = Roles.Reviewer; Roles superAdmin = Roles.SuperAdmin;
Когда вы вызовите метод ToString(), имя константы отобразится в окне консоли, как показано на Рисунке 1.

Рисунок 1: Отображение имени константы записи в окне консоли.
В качестве альтернативы, вы могли бы использовать класс вместо типа записи и в нем определить необходимые константы. Однако я бы отдавал предпочтение записям из соображений производительности. Типы записей являются легковесными типами, благодаря чему они намного быстрее классов. Запись сама по себе является ссылочным типом, но использует собственную встроенную проверку равенства, которая сравнивает объекты по значению, а не по ссылке.