Можно ли в DI-контейнере ASP.NET Core зарегистрировать несколько реализаций одного интерфейса?

Ответ

Да, можно. Стандартный контейнер Microsoft.Extensions.DependencyInjection поддерживает эту возможность.

Существует несколько основных сценариев:

1. Регистрация и разрешение последней зарегистрированной реализации (по умолчанию)

При прямом запросе IService будет возвращена последняя зарегистрированная реализация.

// Регистрация в Program.cs
builder.Services.AddTransient<INotifier, EmailNotifier>();
builder.Services.AddTransient<INotifier, SmsNotifier>(); // Этот будет использован по умолчанию

// Внедрение и использование (получим SmsNotifier)
public class OrderService
{
    private readonly INotifier _notifier;
    public OrderService(INotifier notifier) // Внедрится SmsNotifier
    {
        _notifier = notifier;
    }
}

2. Разрешение ВСЕХ зарегистрированных реализаций

Для реализации паттерна «Стратегия» или цепочки ответственности можно запросить IEnumerable<T>.

// Регистрация нескольких валидаторов
builder.Services.AddTransient<IOrderValidator, StockValidator>();
builder.Services.AddTransient<IOrderValidator, PaymentValidator>();
builder.Services.AddTransient<IOrderValidator, FraudValidator>();

// Внедрение коллекции всех валидаторов
public class OrderProcessor
{
    private readonly IEnumerable<IOrderValidator> _validators;
    public OrderProcessor(IEnumerable<IOrderValidator> validators)
    {
        _validators = validators; // Получим массив из 3-х валидаторов
    }

    public bool Validate(Order order)
    {
        // Последовательно выполняем все валидации
        return _validators.All(v => v.Validate(order));
    }
}

3. Именованное или типизированное разрешение (требует кастомного подхода)

Стандартный контейнер не поддерживает именованные регистрации «из коробки». Этого можно достичь, используя фабрику:

// Регистрация с ключом
builder.Services.AddTransient<EmailNotifier>();
builder.Services.AddTransient<SmsNotifier>();

builder.Services.AddTransient<Func<string, INotifier>>(serviceProvider => key =>
{
    return key switch
    {
        "email" => serviceProvider.GetRequiredService<EmailNotifier>(),
        "sms" => serviceProvider.GetRequiredService<SmsNotifier>(),
        _ => throw new KeyNotFoundException()
    };
});

// Использование
public class NotificationService
{
    private readonly Func<string, INotifier> _notifierFactory;
    public void Notify(string channel)
    {
        var notifier = _notifierFactory(channel); // Получаем нужную реализацию по ключу
        notifier.Send("Hello");
    }
}

Для сложных сценариев можно рассмотреть сторонние контейнеры (Autofac, Lamar), которые имеют встроенную поддержку именованных регистраций.

Ответ 18+ 🔞

Так, слушай сюда, про внедрение зависимостей в .NET. Ну, блядь, народ постоянно спрашивает, можно ли зарегить несколько сервисов под одним интерфейсом и как потом это чудо доставать. Отвечаю — да, конечно, можно, ёпта! Контейнер Microsoft.Extensions.DependencyInjection это умеет, хоть и не без своих, сука, приколов.

Вот смотри, основные сценарии, чтобы не ебал мозги:

1. Последний зарегистрированный — он же главный (де-факто)

По умолчанию, если ты запросишь просто интерфейс, тебе впендюрят последнюю зарегистрированную реализацию. Как в очереди за водкой — кто последний встал, того и талон.

// Регистрируем в Program.cs
builder.Services.AddTransient<INotifier, EmailNotifier>();
builder.Services.AddTransient<INotifier, SmsNotifier>(); // Вот этот красавец и будет главным!

// А потом в каком-нибудь сервисе...
public class OrderService
{
    private readonly INotifier _notifier;
    public OrderService(INotifier notifier) // Сюда придёт SmsNotifier, ясненько?
    {
        _notifier = notifier;
    }
}

2. Хочешь всех сразу? Получи IEnumerable<T>!

Бывает, надо не одного, а всех, кто реализует интерфейс. Например, для паттерна "Цепочка ответственности" или кучи валидаторов. Тогда просто запрашиваешь коллекцию — и контейнер, такой добрый, отдаёт тебе всех.

// Нарегали кучу валидаторов
builder.Services.AddTransient<IOrderValidator, StockValidator>();
builder.Services.AddTransient<IOrderValidator, PaymentValidator>();
builder.Services.AddTransient<IOrderValidator, FraudValidator>();

// А в процессоре заказа...
public class OrderProcessor
{
    private readonly IEnumerable<IOrderValidator> _validators; // Все трое тут как тут!
    public OrderProcessor(IEnumerable<IOrderValidator> validators)
    {
        _validators = validators;
    }

    public bool Validate(Order order)
    {
        // И каждого, сука, прогоняем
        return _validators.All(v => v.Validate(order));
    }
}

3. А если по имени? Тут уже сложнее...

Стандартный контейнер, блядь, из коробки не умеет в именованные регистрации. Типа, "дай мне не абы какой INotifier, а конкретно SmsNotifier". Но народ выкручивается через фабрику — и работает, чёрт возьми!

// Регистрируем каждый тип отдельно
builder.Services.AddTransient<EmailNotifier>();
builder.Services.AddTransient<SmsNotifier>();

// А потом добавляем фабрику, которая по строковому ключу выдает нужную хрень
builder.Services.AddTransient<Func<string, INotifier>>(serviceProvider => key =>
{
    return key switch
    {
        "email" => serviceProvider.GetRequiredService<EmailNotifier>(),
        "sms" => serviceProvider.GetRequiredService<SmsNotifier>(),
        _ => throw new KeyNotFoundException() // На случай, если прилетела хуйня
    };
});

// Используем как боги
public class NotificationService
{
    private readonly Func<string, INotifier> _notifierFactory;
    public void Notify(string channel)
    {
        var notifier = _notifierFactory(channel); // О, магия! Получили именно то, что просили.
        notifier.Send("Hello");
    }
}

Если тебе совсем овердохуища таких сценариев и хочется красоты — посмотри в сторону Autofac или Lamar. Там это из коробки есть. Но для большинства случаев и родного контейнера хватает, главное — понимать, как он работает, а не тыкать наугад, как мартышка в клавиатуру.