← Назад к вопросам

Какие знаешь виды регистрации в IoC-контейнере?

1.7 Middle🔥 191 комментариев
#Dependency Injection и IoC

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Виды регистрации в 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);
    });
    

Критерии выбора вида регистрации:

  1. Время жизни объекта: Нужен ли новый экземпляр каждый раз, на один запрос или на всё приложение?
  2. Потокобезопасность: Будет ли экземпляр использоваться из нескольких потоков?
  3. Затраты на создание: Насколько дорога инициализация сервиса?
  4. Состояние: Хранит ли сервис изменяемое состояние, которое должно быть изолировано?

Правильный выбор вида регистрации критически важен для корректной работы приложения, предотвращения утечек памяти (memory leaks) и обеспечения ожидаемого поведения в многопоточных сценариях.