Dependency Injection в C#: Полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Dependency Injection в C#: что это и зачем нужно

Dependency Injection (DI) — это один из ключевых паттернов проектирования, который позволяет создавать слабосвязанные и легко тестируемые приложения на C#. Вместо того чтобы класс самостоятельно создавал свои зависимости (например, объекты для работы с базой данных или логирования), эти зависимости передаются (внедряются) извне. Это реализует принцип Inversion of Control (IoC) — управление передаётся внешнему контейнеру.

В экосистеме .NET DI стал стандартом де-факто, начиная с ASP.NET Core. Встроенный контейнер зависимостей (через Microsoft.Extensions.DependencyInjection) позволяет легко регистрировать и разрешать зависимости. В этой статье мы разберём теорию, практические примеры и лучшие практики использования DI в C#.

Основные принципы Dependency Injection

DI строится на трёх базовых способах внедрения зависимостей. Рассмотрим каждый из них.

1. Внедрение через конструктор

Самый распространённый и предпочтительный способ. Зависимости передаются через параметры конструктора класса.

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}

public class OrderService
{
    private readonly ILogger _logger;

    // Внедрение зависимости через конструктор
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessOrder(int orderId)
    {
        _logger.Log($"Обработка заказа {orderId}");
        // бизнес-логика
    }
}

2. Внедрение через свойство

Зависимость устанавливается через публичное свойство. Менее предпочтительно, так как делает зависимость необязательной и может привести к ошибкам во время выполнения.

public class OrderService
{
    public ILogger Logger { get; set; }

    public void ProcessOrder(int orderId)
    {
        if (Logger == null)
            throw new InvalidOperationException("Logger не установлен");
        Logger.Log($"Обработка заказа {orderId}");
    }
}

3. Внедрение через метод

Зависимость передаётся как параметр метода. Используется редко, обычно для временных зависимостей.

public class OrderService
{
    public void ProcessOrder(int orderId, ILogger logger)
    {
        logger.Log($"Обработка заказа {orderId}");
    }
}

Регистрация зависимостей в контейнере .NET

В современном .NET (ASP.NET Core, Console, Worker Services) используется встроенный DI-контейнер. Регистрация происходит в методе ConfigureServices или при создании ServiceCollection.

Существует три времени жизни (lifetime) для зарегистрированных сервисов:

  • Transient — новый экземпляр создаётся каждый раз при запросе. Подходит для лёгких, недолговечных сервисов.
  • Scoped — один экземпляр на один HTTP-запрос (или scope). Используется для Entity Framework DbContext.
  • Singleton — один экземпляр на всё приложение. Подходит для кэшей, настроек, логгеров.
// Пример для ASP.NET Core
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Регистрация с интерфейсом
        services.AddTransient<ILogger, ConsoleLogger>();
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddSingleton<IConfiguration, ConfigurationManager>();

        // Регистрация без интерфейса (конкретный класс)
        services.AddTransient<OrderService>();
    }
}

// Использование в контроллере
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrderController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpPost]
    public IActionResult CreateOrder(int id)
    {
        _orderService.ProcessOrder(id);
        return Ok();
    }
}

Продвинутые техники: фабрики и декораторы

Фабрика для динамического создания зависимостей

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

public interface IOrderProcessorFactory
{
    IOrderProcessor Create(string region);
}

public class OrderProcessorFactory : IOrderProcessorFactory
{
    private readonly IServiceProvider _serviceProvider;

    public OrderProcessorFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IOrderProcessor Create(string region)
    {
        return region switch
        {
            "EU" => _serviceProvider.GetRequiredService<EuOrderProcessor>(),
            "US" => _serviceProvider.GetRequiredService<UsOrderProcessor>(),
            _ => throw new ArgumentException("Неизвестный регион")
        };
    }
}

// Регистрация
services.AddTransient<EuOrderProcessor>();
services.AddTransient<UsOrderProcessor>();
services.AddSingleton<IOrderProcessorFactory, OrderProcessorFactory>();

Декоратор для расширения функциональности

Паттерн Декоратор позволяет обернуть существующий сервис, добавив дополнительное поведение (логирование, кэширование, проверка прав).

public class LoggingOrderServiceDecorator : IOrderService
{
    private readonly IOrderService _inner;
    private readonly ILogger _logger;

    public LoggingOrderServiceDecorator(IOrderService inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public void ProcessOrder(int orderId)
    {
        _logger.Log($"Начало обработки заказа {orderId}");
        _inner.ProcessOrder(orderId);
        _logger.Log($"Завершение обработки заказа {orderId}");
    }
}

// Регистрация декоратора (вручную или через библиотеку Scrutor)
services.AddTransient<IOrderService, OrderService>();
services.Decorate<IOrderService, LoggingOrderServiceDecorator>(); // требует Scrutor

Преимущества и недостатки Dependency Injection

Плюсы

  • Слабая связанность — классы зависят от абстракций, а не от конкретных реализаций.
  • Тестируемость — легко подменять зависимости на моки/стабы в юнит-тестах.
  • Упрощение поддержки — изменение реализации требует правки только в одном месте (регистрация контейнера).
  • Управление жизненным циклом — контейнер сам управляет временем жизни объектов.

Минусы

  • Сложность конфигурации — большое количество сервисов может сделать регистрацию запутанной.

Блоги

Рекомендаций по книгам