Внедрение зависимостей (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, но может быть и ручная композиция в маленьких приложениях.
Как они внедряются?
- Через конструктор (Constructor Injection). Это самый популярный способ, зависимости передаются при создании объекта:
public class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo)
{
_repo = repo;
}
}
Разбор:
UserServiceполучаетIUserRepositoryв момент создания и сохраняет его в поле_repo.- Конструктор делает зависимость обязательной: без репозитория объект не инициализируется.
readonlyзащищает от случайной подмены репозитория после создания.- Это основной и рекомендуемый способ внедрения зависимостей в C#.
- Через свойства (Property Injection). Реже используется, но подходит для опциональных зависимостей:
public class ReportGenerator
{
public ILogger Logger { get; set; } // Можно не задавать
}
Разбор:
- Зависимость
Loggerзадаётся черезsetпосле создания объекта. - Подход удобен для опциональных расширений, например дополнительного логирования.
- Минус: объект может работать в неполной конфигурации, если свойство забыли установить.
- Поэтому property injection используют точечно и с дополнительной валидацией состояния.
- Через методы (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 как основа веб-интеграций.