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

«Можно ли в DI-контейнере ASP.NET Core зарегистрировать несколько реализаций одного интерфейса?» — вопрос из категории ASP.NET Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, можно. Стандартный контейнер 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), которые имеют встроенную поддержку именованных регистраций.