Какие знаешь виды регистрации в IoC-контейнере?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Виды регистрации в IoC-контейнере в C#
В контексте внедрения зависимостей (Dependency Injection) и инверсии управления (IoC) в C#, регистрация сервисов в контейнере является фундаментальной операцией. Существует несколько основных видов регистрации, которые определяют жизненный цикл, область видимости и способ разрешения зависимостей. Рассмотрим ключевые виды, используя примеры на основе популярного контейнера Microsoft.Extensions.DependencyInjection.
1. Транзитная регистрация (Transient)
Транзитные зависимости создаются заново каждый раз, когда они запрашиваются контейнером. Это наиболее простой и распространённый вид, подходящий для лёгких, не имеющих состояния сервисов.
services.AddTransient<IMyService, MyService>();
// Или с фабричным методом
services.AddTransient<IMyService>(provider => new MyService("параметр"));
- Когда использовать: Сервисы без состояния, которые дешёвые в создании (например, валидаторы, мапперы, обработчики запросов).
- Особенность: Может привести к повышенному потреблению памяти и нагрузки на GC, если сервис тяжёлый и запрашивается часто.
2. Синглтон (Singleton)
Синглтоны создаются один раз за всё время жизни приложения (точнее, контейнера) и один экземпляр используется повторно для всех запросов.
services.AddSingleton<ICacheService, DistributedCacheService>();
// Регистрация существующего экземпляра как синглтона
var instance = new Logger();
services.AddSingleton<ILogger>(instance);
- Когда использовать: Сервисы, которые должны поддерживать общее состояние или являются ресурсоёмкими (кэш, конфигурация, логгер, соединение с базой данных в некоторых сценариях).
- Важно: Синглтоны должны быть потокобезопасными (thread-safe), так как к одному экземпляру могут обращаться множество потоков одновременно.
3. Скоупированная регистрация (Scoped)
Скоупированные зависимости создаются один раз в рамках определённой области видимости (scope). В контексте веб-приложения (ASP.NET Core) такая область — это один HTTP-запрос. Вне веб-контекста scope можно создать вручную.
services.AddScoped<IUserRepository, SqlUserRepository>();
services.AddScoped<IOrderService, OrderService>();
- Когда использовать: Сервисы, которые должны быть уникальными в рамках одной операции (запроса), но могут использоваться многократно в её пределах. Классический пример —
DbContextв Entity Framework Core. - Особенность: Попытка разрешить скоупированный сервис из корневого контейнера (вне scope) приведёт к исключению. В консольном приложении scope создаётся явно:
using (var scope = serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
// работа с scopedService
}
4. Регистрация по ключу (Keyed Services)
Начиная с .NET 8, в Microsoft.Extensions.DependencyInjection была добавлена возможность регистрации нескольких реализаций одного интерфейса с их последующим разрешением по строковому или числовому ключу.
// Регистрация с ключом
services.AddKeyedSingleton<INotificationService, EmailService>("email");
services.AddKeyedScoped<INotificationService, SmsService>("sms");
// Разрешение с ключом
public class MyController
{
public MyController([FromKeyedServices("email")] INotificationService service)
{
// Будет внедрён EmailService
}
}
- Когда использовать: Стратегический паттерн, когда выбор конкретной реализации зависит от контекста во время выполнения (например, выбор способа оплаты, нотификации).
5. Прочие важные виды и стратегии
-
Регистрация нескольких реализаций (Without Key): Несколько реализаций интерфейса можно зарегистрировать и затем разрешить как коллекцию
IEnumerable<TService>.services.AddSingleton<IPlugin, PluginA>(); services.AddSingleton<IPlugin, PluginB>(); // ... public class Processor { public Processor(IEnumerable<IPlugin> plugins) { /* plugins содержит и A, и B */ } } -
Открытые обобщённые типы (Open Generics): Позволяет зарегистрировать универсальный шаблон, что избавляет от необходимости регистрировать каждую закрытую версию.
services.AddSingleton(typeof(IRepository<>), typeof(GenericRepository<>)); // Контейнер сможет разрешить IRepository<User>, IRepository<Order> и т.д. -
Фабричные методы: Любой вид регистрации (
AddTransient,AddScoped,AddSingleton) поддерживает перегрузку с делегатомFunc<IServiceProvider, TService>. Это даёт полный контроль над процессом создания экземпляра, включая доступ к самому провайдеру услуг для разрешения дополнительных зависимостей.services.AddScoped<IService>(sp => { var options = sp.GetRequiredService<IOptions<MyOptions>>(); return new MyService(options.Value.ConnectionString); });
Критерии выбора вида регистрации:
- Время жизни объекта: Нужен ли новый экземпляр каждый раз, на один запрос или на всё приложение?
- Потокобезопасность: Будет ли экземпляр использоваться из нескольких потоков?
- Затраты на создание: Насколько дорога инициализация сервиса?
- Состояние: Хранит ли сервис изменяемое состояние, которое должно быть изолировано?
Правильный выбор вида регистрации критически важен для корректной работы приложения, предотвращения утечек памяти (memory leaks) и обеспечения ожидаемого поведения в многопоточных сценариях.