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.Length); // 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?
You did not register GeneralEventHook
as IHook<SpecificEvent>
, and dependency injection will only return for types that were used during registration.
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.
The .NET Dependency Injection system doesn’t integrate with generic interface variance because it’s designed for simplicity and performance, using exact type matching rather than considering variance relationships.
To work around this, you have a few options:
Create a custom resolution method that combines both specific and general hooks.
Use the Adapter pattern to wrap general hooks as specific ones or you can
Implement an event aggregator that manages all hooks and dispatches events.
For example
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);
}
// Usage:
var hooks = app.Services.GetAllHooks<SpecificEvent>();
This approach maintains the existing DI setup while allowing you to retrieve both specific and general hooks when needed.
1