Какие есть подходы внедрения зависимостей в ASP.NET Core?

Ответ

В ASP.NET Core существует три основных подхода внедрения зависимостей (DI), каждый из которых имеет свои сценарии применения.

1. Внедрение через конструктор (Constructor Injection)

Наиболее предпочтительный и рекомендуемый подход. Зависимости объявляются как параметры конструктора класса, обычно через интерфейсы. Контейнер DI автоматически предоставляет реализацию при создании экземпляра.

public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    private readonly IEmailService _emailService;

    // Зависимости внедряются через конструктор
    public OrderService(ILogger<OrderService> logger, IEmailService emailService)
    {
        _logger = logger;
        _emailService = emailService;
    }

    public void ProcessOrder(Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
        // ... логика
        _emailService.SendConfirmation(order);
    }
}

Преимущества: Явные зависимости, класс легко тестировать (можно передавать mock-объекты), состояние объекта валидно сразу после создания.

2. Внедрение через метод (Method Injection)

Используется, когда зависимость требуется только для выполнения одного конкретного метода, а не для всего жизненного цикла объекта.

public class ReportGenerator
{
    // Зависимость передается как параметр метода
    public string GenerateReport(ReportData data, IFormatter formatter)
    {
        return formatter.Format(data);
    }
}

Сценарий использования: Полезен для стратегий или сервисов, которые могут меняться в зависимости от контекста вызова.

3. Внедрение через свойство (Property Injection)

Наименее предпочтительный подход. Зависимости устанавливаются через публичные свойства после создания объекта. Контейнеры, такие как IServiceProvider, могут это делать автоматически, если настроены соответствующим образом.

public class NotificationService
{
    // Зависимость устанавливается через свойство
    public ILogger<NotificationService> Logger { get; set; }
}

Недостатки: Делает зависимости неявными, объект может находиться в невалидном состоянии до установки свойства, усложняет тестирование. Используется в основном для совместимости с legacy-кодом или в некоторых фреймворках (например, в Razor Pages для [Inject]).

Регистрация в DI-контейнере

Независимо от подхода, зависимости должны быть зарегистрированы в Program.cs или Startup.cs:

builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddSingleton<ICacheService, DistributedCacheService>();
builder.Services.AddTransient<IReportFormatter, PdfReportFormatter>();

Лучшие практики:

  • Всегда предпочитайте внедрение через конструктор.
  • Программируйте на основе интерфейсов, а не конкретных классов.
  • Избегайте Service Locator (например, вызов GetService из IServiceProvider) как антипаттерна, так как это скрывает зависимости и усложняет код.

Ответ 18+ 🔞

Давай разберём эту дичь про внедрение зависимостей в ASP.NET Core, а то народ часто путается, как будто в трёх соснах.

Смотри, есть три основных способа запихнуть твоему сервису в жопу нужные ему штуки. И один из них — просто царь и бог, а остальные — так, на подхвате.

1. Внедрение через конструктор (Constructor Injection) — наш бро, наш герой

Это, блядь, святое. Самый правильный, тестируемый и человеческий способ. Ты просто объявляешь всё, что нужно классу, в параметрах конструктора. Контейнер DI, такой добрый дядя, сам всё подвезёт и скормит.

public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    private readonly IEmailService _emailService;

    // Смотри сюда: всё нужное прилетает прямо в конструктор. Красота!
    public OrderService(ILogger<OrderService> logger, IEmailService emailService)
    {
        _logger = logger;
        _emailService = emailService;
    }

    public void ProcessOrder(Order order)
    {
        _logger.LogInformation("Processing order {OrderId}", order.Id);
        // ... какая-то магия
        _emailService.SendConfirmation(order);
    }
}

Чем хорош: Всё на виду, как на ладони. Хочешь протестировать? Да пожалуйста, подсовываешь в конструктор заглушки (mocks) и не паришься. Объект сразу после создания — готов к бою, а не в полуобморочном состоянии.

2. Внедрение через метод (Method Injection) — ситуативный пацан

Бывает, что зависимость нужна не всему классу на всю жизнь, а только для одного конкретного дела. Вот тогда ты тащишь её параметром в метод.

public class ReportGenerator
{
    // Зависимость приходит только когда нужно сгенерить отчёт
    public string GenerateReport(ReportData data, IFormatter formatter)
    {
        return formatter.Format(data);
    }
}

Когда применять: Ну, например, если логика форматирования меняется в зависимости от того, что за клиент приперся. Один раз нужен PDF, другой раз — JSON, и нехуй держать всё в полях класса.

3. Внедрение через свойство (Property Injection) — старый, кривой костыль

А вот это, прости господи, говно. Зависимости выставляются через публичные свойства. Объект после создания — как пустая консервная банка, пока в него что-то не насыплешь.

public class NotificationService
{
    // Логгер? А хз, будет он или нет. Ждём-с.
    public ILogger<NotificationService> Logger { get; set; }
}

Почему это пиздец: Объект может спокойно существовать в нерабочем состоянии. Тестировать — боль. Понимать, что ему нужно — ещё больнее. Используется только в каких-то древних легаси-системах или в специфичных местах вроде Razor Pages (там атрибут [Inject]), но это не повод тащить эту хуйню в свой нормальный код.

Главное — не забудь зарегистрировать это добро!

Какой бы способ ты ни выбрал (а ты выбери конструктор, ёпта), зависимости надо прописать в контейнере. Обычно это делается в Program.cs.

builder.Services.AddScoped<IEmailService, SmtpEmailService>(); // Один на запрос
builder.Services.AddSingleton<ICacheService, DistributedCacheService>(); // Один на всех
builder.Services.AddTransient<IReportFormatter, PdfReportFormatter>(); // Новый каждый раз

Итоговая памятка, чтобы не быть мудаком:

  • Внедрение через конструктор — твой лучший друг, используй его везде, где можно.
  • Работай с интерфейсами, а не с конкретными классами. Это даёт гибкость, а не геморрой.
  • Беги как ошпаренный от Service Locator (это когда ты из IServiceProvider руками GetService вызываешь). Это антипаттерн, который прячет зависимости так, что потом сам чёрт ногу сломит. Не наступай на эти грабли.