Первичные Конструкторы для Внедрения Зависимостей. Начало
Когда в C# 12 внедрили первичные конструкторы в классах, это было встречено скептически. Неявное изменяемое поле вместо явного поля только для чтения? Это казалось обменом безопасности на удобство. Но практика показала, что они значительно сокращают количество шаблонного кода в сервисных классах, а ловушка, которой все опасались, становится управляемой, если вы о ней знаете.
Классы сервисов
Вот как раньше выглядели классы сервисов:
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repo,
ILogger<OrderService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<Order?> GetAsync(Guid id)
{
_logger.LogInformation("Получаем заказ {OrderId}", id);
return await _repo.GetByIdAsync(id);
}
}
И как теперь:
public class OrderService(
IOrderRepository repo,
ILogger<OrderService> logger)
{
public async Task<Order?> GetAsync(Guid id)
{
logger.LogInformation("Получаем заказ {OrderId}", id);
return await repo.GetByIdAsync(id);
}
}
Объявления полей, тело конструктора, присваивания — всё исчезло. Параметры перехватываются и становятся доступны во всём теле класса.
Это наиболее распространённый вариант использования первичных конструкторов: внедрение зависимостей в классы сервисов. Вы объявляете то, что вам нужно, и используете это напрямую. Экономия на шаблонном коде быстро накапливается. Классам сервисов обычно не нужно проверять или преобразовывать свои зависимости. DI-контейнер предоставляет их, а вы их используете. Первичные конструкторы идеально подходят для этого.
Создание сущностей (с оговоркой)
Можно использовать первичные конструкторы для сущностей предметной области и объектов-значений, где необходимо обеспечить обязательность параметров во время создания:
public class Order(Guid customerId, Money total)
{
public Guid Id { get; } = Guid.NewGuid();
public Guid CustomerId { get; } = customerId;
public Money Total { get; } = total;
public OrderStatus Status { get; private set; }
= OrderStatus.Pending;
public DateTime CreatedAt { get; }
= DateTime.UtcNow;
public void Confirm()
{
if (Status != OrderStatus.Pending)
{
throw new InvalidOperationException(
$"Нельзя подтвердить заказ в статусе {Status}.");
}
Status = OrderStatus.Confirmed;
}
}
Создать заказ без customerId и total невозможно. Первичный конструктор делает это ограничение видимым на уровне объявления типа.
Обратите внимание на ключевое отличие от шаблона класса сервиса: параметры первичного конструктора присваиваются свойствам с инициализаторами (= customerId). Это важно, и это приводит к самой большой проблеме первичных конструкторов.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/why-i-switched-to-primary-constructors-for-di-in-csharp