Перейти к основному содержимому

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

Разработчику Архитектору

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

Dependency Injection

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

Зависимость (Dependency) — это любой внешний сервис, который использует ваш класс:

  • Базы данных (DbContext)
  • HTTP-клиенты (HttpClient)
  • Логгеры (ILogger)
  • Репозитории, внешние API и т. д.

Внедрение зависимостей — это передача этих сервисов извне (через конструктор, свойства или методы), а не создание их внутри класса. Без DI, зависимости создаются внутри класса, образуя "жесткую связь". Это сложно тестировать, менять и это нарушает один из принципов проектирования SOLID - принцип единственной ответственности (SRP).

public class OrderService
{
private readonly ILogger _logger = new FileLogger(); // Жёсткая привязка
private readonly IPaymentGateway _gateway = new PayPalGateway(); // Трудно тестировать

public void ProcessOrder(Order order)
{
_logger.Log("Processing order...");
_gateway.ProcessPayment(order.Total);
}
}

Разбор:

  • Поля _logger и _gateway создаются прямо внутри класса через new, поэтому сервис жёстко связан с конкретными реализациями.
  • private readonly делает ссылки неизменяемыми после создания, но не решает проблему подмены зависимостей.
  • ProcessOrder одновременно занимается бизнес-логикой и зависит от выбора инфраструктурных компонентов.
  • Такой код сложно тестировать изолированно, потому что нельзя легко подставить mock/stub.
  • Любая замена платёжного шлюза или логгера требует изменения класса и перекомпиляции.

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

Код ITЗагрузка примера кода…

Разбор:

  • Класс хранит зависимости через интерфейсы (ILogger, IPaymentGateway), а не через конкретные типы.
  • Конструктор принимает готовые сервисы извне — это constructor injection.
  • Бизнес-метод ProcessOrder становится проще: он использует контракты и не создаёт объекты сам.
  • Такой дизайн упрощает тестирование, расширение и соблюдает принцип инверсии зависимостей.
  • Источник зависимостей обычно контейнер DI, но может быть и ручная композиция в маленьких приложениях.

Как они внедряются?

  1. Через конструктор (Constructor Injection). Это самый популярный способ, зависимости передаются при создании объекта:
public class UserService
{
private readonly IUserRepository _repo;

public UserService(IUserRepository repo)
{
_repo = repo;
}
}

Разбор:

  • UserService получает IUserRepository в момент создания и сохраняет его в поле _repo.
  • Конструктор делает зависимость обязательной: без репозитория объект не инициализируется.
  • readonly защищает от случайной подмены репозитория после создания.
  • Это основной и рекомендуемый способ внедрения зависимостей в C#.
  1. Через свойства (Property Injection). Реже используется, но подходит для опциональных зависимостей:
public class ReportGenerator
{
public ILogger Logger { get; set; } // Можно не задавать
}

Разбор:

  • Зависимость Logger задаётся через set после создания объекта.
  • Подход удобен для опциональных расширений, например дополнительного логирования.
  • Минус: объект может работать в неполной конфигурации, если свойство забыли установить.
  • Поэтому property injection используют точечно и с дополнительной валидацией состояния.
  1. Через методы (Method Injection). Зависимость передаётся в конкретный метод:
public class DataExporter
{
public void Export(ISerializer serializer) { ... }
}

Разбор:

  • ISerializer передаётся только на время вызова Export, без хранения в состоянии объекта.
  • Этот подход полезен, когда стратегия сериализации зависит от сценария выполнения.
  • Класс остаётся более универсальным: один и тот же DataExporter может работать с разными сериализаторами.
  • Method injection снижает связанность, если зависимость не нужна постоянно.

.NET предоставляет встроенный IoC-контейнер (IServiceProvider), который управляет зависимостями.

Так работает регистрация сервисов в Program.cs или Startup.cs.

var builder = WebApplication.CreateBuilder(args);

// 1. Singleton (один экземпляр на всё приложение)
builder.Services.AddSingleton<ILogger, FileLogger>();

// 2. Scoped (один экземпляр на запрос в веб-приложении)
builder.Services.AddScoped<IUserRepository, UserRepository>();

// 3. Transient (новый экземпляр при каждом запросе)
builder.Services.AddTransient<IEmailService, EmailService>();

var app = builder.Build();

Разбор:

  • CreateBuilder(args) поднимает инфраструктуру приложения и коллекцию регистраций Services.

  • AddSingleton регистрирует один общий экземпляр на всё приложение.

  • AddScoped создаёт экземпляр на область (в веб-приложении — обычно на HTTP-запрос).

  • AddTransient выдаёт новый экземпляр при каждом разрешении зависимости.

  • builder.Build() фиксирует конфигурацию и создаёт готовое приложение с рабочим контейнером DI.

  • Выбор lifetime влияет на состояние сервисов, потокобезопасность и расход памяти.

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

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.