Внедрение зависимостей (Dependency Injection) в C#: полное руководство

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

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

Начать курс

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

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

Основная цель DI — реализация принципа Inversion of Control (IoC), когда управление созданием объектов передается внешнему контейнеру или фреймворку. Это кардинально меняет архитектуру приложения, делая её гибкой и готовой к изменениям.

Проблема жесткой связанности (Tight Coupling)

Рассмотрим типичный пример без DI:

public class OrderService
{
    private readonly EmailSender _emailSender;

    public OrderService()
    {
        // Жесткая зависимость — создаем экземпляр конкретного класса
        _emailSender = new EmailSender();
    }

    public void ProcessOrder(Order order)
    {
        // Логика обработки заказа
        _emailSender.Send("Заказ обработан");
    }
}

Здесь OrderService напрямую зависит от EmailSender. Если мы захотим заменить отправку email на SMS или логирование, придется изменять код OrderService. Это нарушает Open/Closed Principle и делает модульное тестирование практически невозможным.

Основные способы внедрения зависимостей в C#

В C# существует три основных способа реализации DI. Каждый из них имеет свои сценарии использования и особенности.

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

Это самый популярный и рекомендуемый способ в .NET. Зависимости передаются через параметры конструктора и обычно сохраняются в приватных полях, доступных только для чтения (readonly).

public interface INotificationService
{
    void Send(string message);
}

public class EmailService : INotificationService
{
    public void Send(string message)
    {
        Console.WriteLine($"Email отправлен: {message}");
    }
}

public class OrderService
{
    private readonly INotificationService _notificationService;

    // Зависимость внедряется через конструктор
    public OrderService(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void ProcessOrder(Order order)
    {
        // Логика...
        _notificationService.Send("Заказ обработан");
    }
}

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

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

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

public class OrderService
{
    public INotificationService NotificationService { get; set; }

    public OrderService() { }

    public void ProcessOrder(Order order)
    {
        if (NotificationService != null)
        {
            NotificationService.Send("Заказ обработан");
        }
    }
}

Недостаток: можно забыть установить зависимость, что приведет к NullReferenceException во время выполнения. Рекомендуется использовать только для опциональных зависимостей.

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

Зависимость передается непосредственно в метод, где она используется. Это удобно, когда зависимость требуется только для одного конкретного действия.

public class OrderService
{
    public void ProcessOrder(Order order, INotificationService notificationService)
    {
        // Логика...
        notificationService.Send("Заказ обработан");
    }
}

Этот подход часто применяется в аспектно-ориентированном программировании или когда зависимость может меняться для каждого вызова метода.

Встроенный IoC-контейнер в .NET Core / .NET 5+

Начиная с .NET Core, в платформу встроен мощный контейнер внедрения зависимостей (IServiceProvider). Он находится в пространстве имен Microsoft.Extensions.DependencyInjection. Для регистрации сервисов используется класс ServiceCollection.

Регистрация сервисов

В файле Program.cs или Startup.cs (для старых версий) вы регистрируете все зависимости:

using Microsoft.Extensions.DependencyInjection;

// Создаем коллекцию сервисов
var services = new ServiceCollection();

// Регистрируем зависимости с разным временем жизни
services.AddTransient<INotificationService, EmailService>();  // Новый экземпляр при каждом запросе
services.AddScoped<IOrderRepository, OrderRepository>();      // Один экземпляр на запрос
services.AddSingleton<ILogger, ConsoleLogger>();              // Один экземпляр на всё приложение

// Регистрируем основной сервис
services.AddTransient<OrderService>();

// Строим провайдер
var serviceProvider = services.BuildServiceProvider();

// Получаем экземпляр OrderService с автоматически внедренными зависимостями
var orderService = serviceProvider.GetRequiredService<OrderService>();

Времена жизни (Lifetimes) объектов

  • Transient — создается каждый раз, когда запрашивается. Идеально для легковесных сервисов без состояния.
  • Scoped — создается один раз на область (например, на один HTTP-запрос в ASP.NET).
  • Singleton — создается один раз и живет всё время жизни приложения. Подходит для кэшей, логгеров.

Пример использования DI в ASP.NET Core Web API

Рассмотрим реальный сценарий — контроллер Web API, который использует сервис для работы с базой данных.

// Интерфейс репозитория
public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync();
}

// Реализация репозитория
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _context.Products.ToListAsync();
    }
}

// Контроллер
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;

    // DI через конструктор — фреймворк сам внедрит репозиторий
    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        var products = await _repository.GetAllAsync();
        return Ok(products);
    }
}                

Блоги

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