Search code examples
c#.netdependency-injection

Why doesn't .NET's DI system understand generic interface variance?


Consider this example:

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.Count()); // 1 (bummer...)

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)
    {

    }
}

When I try to resolve a list of IHook<SpecificEvent>s from the service provider, I was expecting to get the GeneralEventHook (which is a IHook<IEvent>) as well, since IHook's generic parameter is contravariant (in TEvent); and therefore, an IHook<IEvent>, is, in fact, assignable to an IHook<SpecificEvent>.

But it seems like the standard .NET dependency injection system does not support this. There's surprisingly very little information on the web about this type of scenario.

I'm curious why? Isn't there a way to customize the DI container to achieve this? If not, what would be a reasonable workaround for this type of requirement?


Solution

  • You did not register GeneralEventHook as IHook<SpecificEvent>, and dependency injection will only return services for types that were registered.

    Similairly, if you would have:

    public interface IService;
    public class Service : IService { }
    

    and you would register it as self:

    services.AddScoped<Service>();
    

    DI will fail to resolve IService.

    In order to achieve what you want, you need to regsiter GeneralEventHook as IHook<SpecificEvent>:

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

    This method will understand your contravariance allowing for such registration.