Почему система внедрения зависимостей .NET не понимает варьируемость обобщённых интерфейсов?

Вопрос или проблема

Рассмотрим этот пример:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IHook<SpecificEvent>, SpecificEventHook>();
builder.Services.AddTransient<IHook<IEvent>, GeneralEventHook>();

var app = builder.Build();

var hooks = app.Services.GetServices<IHook<SpecificEvent>>();
Console.WriteLine(hooks.Length); // 1 (печально...)

public interface IEvent;

public interface IHook<in TEvent> where TEvent : IEvent
{
    void On(TEvent @event);
}

public record SpecificEvent(int Foo) : IEvent;

public class SpecificEventHook : IHook<SpecificEvent>
{
    public void On(SpecificEvent @event)
    {

    }
}
public class GeneralEventHook : IHook<IEvent>
{
    public void On(IEvent @event)
    {

    }
}

Когда я пытаюсь получить список IHook<SpecificEvent> из провайдера сервисов, я ожидал получить также GeneralEventHook (который является IHook<IEvent>), поскольку обобщенный параметр IHook является контравариантным (in TEvent); и, следовательно, IHook<IEvent> на самом деле можно присвоить IHook<SpecificEvent>.

Но похоже, что стандартная система внедрения зависимостей .NET не поддерживает это. На удивление, в Интернете очень мало информации об этом типе сценария.

Мне любопытно, почему? Разве нет способа настроить контейнер DI для достижения этого? Если нет, что было бы разумным обходным решением для такого требования?

Система внедрения зависимостей .NET не интегрируется с обобщенной вариацией интерфейсов, потому что она разработана для простоты и производительности, используя точное совпадение типов, а не учитывая вариационные отношения.
Чтобы обойти это, у вас есть несколько вариантов:

Создайте метод разрешения, который объединяет как специфические, так и общие хуки. Используйте шаблон адаптера, чтобы обернуть общие хуки как специфические, или вы можете реализовать агрегатор событий, который управляет всеми хуками и распределяет события.

Например

public static IEnumerable<IHook<TEvent>> GetAllHooks<TEvent>(this IServiceProvider services)
    where TEvent : IEvent
{
    var specificHooks = services.GetServices<IHook<TEvent>>();
    var generalHooks = services.GetServices<IHook<IEvent>>();
    return specificHooks.Concat(generalHooks);
}

// Использование:
var hooks = app.Services.GetAllHooks<SpecificEvent>();

Этот подход сохраняет существующую настройку DI, позволяя вам извлекать как специфические, так и общие хуки по мере необходимости.

Вы не зарегистрировали GeneralEventHook как IHook<SpecificEvent>, и внедрение зависимостей будет возвращать только типы, используемые во время регистрации.

Аналогично, если вы зарегистрируете Service (которая реализует IService) как Service (как саму себя), DI не сможет разрешить IService.

Чтобы добиться желаемого, вам нужно зарегистрировать GeneralEventHook как IHook<SpecificEvent>:

builder.Services.AddTransient<IHook<SpecificEvent>, GeneralEventHook>();

Этот метод поймет вашу контравариацию, позволяя такую регистрацию.

Ответ или решение

В .NET система внедрения зависимостей (DI) не учитывает вариацию обобщенных интерфейсов из-за своей простоты и быстродействия, использующей точное соответствие типов вместо учета отношений вариации. Это означает, что, когда вы регистрируете услуги, DI контейнер будет учитывать только строго определенные типы, а не их подтипы или суперклассы, даже если в коде указаны оговорки о контравариантности.

В вашем примере вы зарегистрировали SpecificEventHook как IHook<SpecificEvent> и GeneralEventHook как IHook<IEvent>. Поскольку IHook<IEvent> не зарегистрирован как IHook<SpecificEvent>, DI контейнер не сможет его разрешить при запросе GetServices<IHook<SpecificEvent>>(), и, следовательно, вернет только SpecificEventHook.

Чтобы добиться желаемого поведения, необходимо зарегистрировать GeneralEventHook также и как IHook<SpecificEvent>. Для этого добавьте следующую строку в вашу конфигурацию DI:

builder.Services.AddTransient<IHook<SpecificEvent>, GeneralEventHook>();

Таким образом, ваш DI контейнер будет понимать, что GeneralEventHook может быть использован как для работы с IEvent, так и с SpecificEvent, что соответствует принципу контравариантности.

Если вы хотите сделать решение более универсальным и не ограничиваться конкретными типами в DI, вы можете реализовать метод расширения, который объединяет все хуки, относящиеся к конкретному типу события. Вот как это может выглядеть:

public static IEnumerable<IHook<TEvent>> GetAllHooks<TEvent>(this IServiceProvider services)
    where TEvent : IEvent
{
    var specificHooks = services.GetServices<IHook<TEvent>>();
    var generalHooks = services.GetServices<IHook<IEvent>>();
    return specificHooks.Concat(generalHooks.Cast<IHook<TEvent>>());
}

// Использование:
var hooks = app.Services.GetAllHooks<SpecificEvent>();

Этот подход сохраняет существующую настройку DI и позволяет извлекать как конкретные, так и общие хуки, когда это необходимо.

Следовательно, вам не нужно модифицировать DI контейнер, чтобы он понимал контравариантность. Достаточно правильно зарегистрировать все зависимости и, при необходимости, создать метод расширения для выборки хуков.

Оцените материал
Добавить комментарий

Капча загружается...