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

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

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

Начать курс

Введение: что такое Dependency Injection и зачем это нужно

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

В экосистеме C# и .NET DI стал стандартом де-факто. Начиная с .NET Core, в платформу встроен собственный контейнер внедрения зависимостей, который используется в ASP.NET Core, фоновых сервисах и даже в консольных приложениях. Без понимания DI невозможно представить современную разработку на C#.

В этой статье мы разберем DI на реальных примерах: от ручного внедрения до использования встроенного контейнера .NET. Вы увидите, как избавиться от жестких связей и сделать код пригодным для модульного тестирования.

Проблема без Dependency Injection

Представим типичный сценарий: класс OrderService обрабатывает заказы и отправляет email-уведомления. Без DI код выглядит так:

public class EmailSender
{
    public void Send(string to, string subject, string body)
    {
        Console.WriteLine($"Отправка email на {to}: {subject}");
    }
}

public class OrderService
{
    private EmailSender _emailSender;

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

    public void ProcessOrder(int orderId)
    {
        // Логика обработки заказа...
        _emailSender.Send("user@example.com", "Заказ оформлен", $"Ваш заказ #{orderId} принят.");
    }
}

В чем проблема? OrderService жестко привязан к конкретной реализации EmailSender. Если мы захотим заменить отправку email на SMS или логгирование, придется менять код OrderService. Тестирование тоже усложняется: невозможно подменить реальный EmailSender на mock-объект.

Решение: внедрение зависимостей через конструктор

Первый шаг к DI — выделить абстракцию (интерфейс) и передавать зависимость через конструктор. Это называется Constructor Injection.

Шаг 1: Создаем интерфейс

public interface INotificationService
{
    void Send(string to, string subject, string body);
}

Шаг 2: Реализуем интерфейс

public class EmailSender : INotificationService
{
    public void Send(string to, string subject, string body)
    {
        Console.WriteLine($"Email отправлен: {subject} -> {to}");
    }
}

public class SmsSender : INotificationService
{
    public void Send(string to, string subject, string body)
    {
        Console.WriteLine($"SMS отправлено: {subject} -> {to}");
    }
}

Шаг 3: Внедряем зависимость через конструктор

public class OrderService
{
    private readonly INotificationService _notificationService;

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

    public void ProcessOrder(int orderId)
    {
        // Логика обработки заказа...
        _notificationService.Send("user@example.com", "Заказ оформлен", $"Ваш заказ #{orderId} принят.");
    }
}

Теперь OrderService не знает, какая именно реализация используется. Мы можем подставить EmailSender, SmsSender или любой другой класс, реализующий INotificationService. Код стал гибким и тестируемым.

Использование встроенного IoC-контейнера в .NET

Вручную создавать все зависимости неудобно. В .NET есть встроенный контейнер (IoC-контейнер), который автоматически управляет жизненным циклом объектов. Рассмотрим пример для консольного приложения.

Шаг 1: Устанавливаем пакет (если нужно)

Для консольных приложений .NET 6+ добавьте ссылку на Microsoft.Extensions.DependencyInjection через NuGet.

Шаг 2: Настраиваем контейнер

using Microsoft.Extensions.DependencyInjection;

class Program
{
    static void Main(string[] args)
    {
        // Создаем коллекцию сервисов
        var services = new ServiceCollection();

        // Регистрируем зависимости
        services.AddTransient<INotificationService, EmailSender>();
        services.AddTransient<OrderService>();

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

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

Шаг 3: Разбираем жизненные циклы

  • Transient (AddTransient): новый экземпляр создается при каждом запросе. Идеально для легковесных сервисов без состояния.
  • Scoped (AddScoped): один экземпляр на область (например, на один HTTP-запрос в ASP.NET Core).
  • Singleton (AddSingleton): один экземпляр на всё приложение. Используется для кэшей, логгеров, конфигураций.

Выбор жизненного цикла важен: неправильный выбор может привести к утечкам памяти или неконсистентным данным. Для EmailSender обычно используют Transient или Singleton (если он потокобезопасен).

Dependency Injection в ASP.NET Core

В ASP.NET Core DI встроен прямо в каркас. Регистрация сервисов происходит в файле Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Регистрируем сервисы
builder.Services.AddScoped<INotificationService, EmailSender>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

// Контроллеры автоматически получают зависимости через конструктор
app.MapGet("/order/{id}", (int id, OrderService orderService) =>
{
    orderService.ProcessOrder(id);
    return Results.Ok("Заказ обработан");
});

app.Run();

Обратите внимание: в минимальных API и контроллерах MVC зависимости внедряются автоматически. Вам нужно только зарегистрировать их один раз.

Заключение

Dependency Injection — это не просто модный паттерн, а практический инструмент для создания качественного кода на C#. Мы разобрали:

  • Проблему жестких связей и как DI её ре

Блоги

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