Please find below an explanation of the problem, and a simple but almost complete repro (.Net 8 / MediatR 12.4.1), usable in Linqpad or Console app.
I have a barely complex hierarchy of Notifications classes with MediatR :
public abstract class EntityNotification : INotification { }
public class EntityNotificationTyped<T> : EntityNotification { }
public class A { }
public class B { }
(INotification
-> EntityNotification
-> EntityNotificationTyped<T>
). I have also other children under EntityNotificationTyped
but like it appears above, it's already enough to exhibit the issue.
Please note that "EntityNotification" is not by itself a generic type.
Here are the handlers :
public class GenericNotificationHandler
: INotificationHandler<EntityNotification>
{
public async Task Handle(EntityNotification notification, CancellationToken cancellationToken)
{
Console.WriteLine("GenericNotificationHandler received:" + notification.GetType().FullName);
}
}
public class EntityNotificationForAHandler
: INotificationHandler<EntityNotificationTyped<A>>
{
public async Task Handle(EntityNotificationTyped<A> notification, CancellationToken cancellationToken)
{
Console.WriteLine("EntityNotificationForAHandler received:" + notification.GetType().FullName);
}
}
public class EntityNotificationForBHandler
: INotificationHandler<EntityNotificationTyped<B>>
{
public async Task Handle(EntityNotificationTyped<B> notification, CancellationToken cancellationToken)
{
Console.WriteLine("EntityNotificationForBHandler received:" + notification.GetType().FullName);
}
}
Here is the test program:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMediatR(config =>
{
config.RegisterServicesFromAssemblyContaining<Program>();
});
var app = builder.Build();
var publisher = app.Services.GetRequiredService<IPublisher>();
Console.WriteLine("Publishing A");
await publisher.Publish(new EntityNotificationTyped<A>());
Console.WriteLine(Environment.NewLine + "Publishing B");
await publisher.Publish(new EntityNotificationTyped<B>());
Console.WriteLine("End");
If I run it as-is, it works fine, all handlers are called as expected :
Publishing A
GenericNotificationHandler received:EntityNotificationTyped`1[[A, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
EntityNotificationForAHandler received:EntityNotificationTyped`1[[A, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
Publishing B
GenericNotificationHandler received:EntityNotificationTyped`1[[B, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
EntityNotificationForBHandler received:EntityNotificationTyped`1[[B, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
End
But now, if I completely remove "EntityNotificationForBHandler" class, no handler is invoked for "Publishing B" :
Publishing A
GenericNotificationHandler received:EntityNotificationTyped`1[[A, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
EntityNotificationForAHandler received:EntityNotificationTyped`1[[A, MediatRPublish, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
Publishing B
End
Why my "GenericNotificationHandler" that was previously working for any type of notification is not working when there's no specific handler for the "most nested inherited" notification type, besides it works when the last one is present ?
I have noticed that making the handler itself generic seems to help, I guess it has something to do with covariance/contravariance, but I don't get why sometimes it works, sometimes not. Here's a possible "generic handler" :
public class GenericNotificationHandler<TEntityNotification>(ILogger<GenericNotificationHandler<TEntityNotification>> logger)
: INotificationHandler<TEntityNotification>
where TEntityNotification : EntityNotification
{
public async Task Handle(TEntityNotification notification, CancellationToken cancellationToken)...
PS : I have also noticed that if I register explicitly the Generichandler for EntityNotificationTyped it also works: builder.Services.AddTransient<INotificationHandler<EntityNotificationTyped<B>>, GenericNotificationHandler>();
But the idea of a generic handler was to be able to capture all kind of notifications without to have to register them explicitly.
Thank you
I don't think there is a bug. If you inspect the service collection and analyze how MediatR registers possible notification handlers, you can get a sense of how it works. Below's the original scenario with a generic handler and both A
& B
handlers present in the assembly:
# | Service type | Implementation type | Generic |
---|---|---|---|
1 | INotificationHandler | GenericNotificationHandler | ✅ |
2 | INotificationHandler<EntityNotificationTyped<A>> | GenericNotificationHandler | ✅ |
3 | INotificationHandler<EntityNotificationTyped<A>> | EntityNotificationForAHandler | ❌ |
4 | INotificationHandler<EntityNotificationTyped<B>> | GenericNotificationHandler | ✅ |
5 | INotificationHandler<EntityNotificationTyped<B>> | EntityNotificationForBHandler | ❌ |
And here is the modified scenario with generic handler and only A
handler present in the assembly. Notice how services for B
are gone:
# | Service type | Implementation type | Generic |
---|---|---|---|
1 | INotificationHandler | GenericNotificationHandler | ✅ |
2 | INotificationHandler<EntityNotificationTyped<A>> | GenericNotificationHandler | ✅ |
3 | INotificationHandler<EntityNotificationTyped<A>> | EntityNotificationForAHandler | ❌ |
And this is because there is no handler present in assembly for B
in the second scenario. You get the registrations for handlers that can be found and compared to each other.
MediatR sees the handler, looks at the type it handles, analyzes whether it is open generic or not and registers it as service for each interface type the handler can be cast on. The collection of types is based on handler types MediatR obtained from scanning.
Notice that there is no connection between EntityNotificationTyped<T>
and A
and B
classes. Once you remove the B
handler, the link is gone. At this point MediatR knows nothing about this, so you cannot expect from it to register all possible permutations of notification handlers, because that would lead to registration of notification handlers for every type possible, not just A
and B
.
You can look at ServiceRegistrar to get a better understanding of the process. Or if you want a more practical approach, you can set a breakpoint inside MediatR's service collection extension method while disabling just my code option to see the execution live.
You can use open generic type, and it will handle all notifications that match, even those without explicit type-dedicated handlers:
public class GenericNotificationHandler<T>
: INotificationHandler<T>
where T : EntityNotification, new()
{
public async Task Handle(T notification, CancellationToken cancellationToken)
{
Console.WriteLine("EntityNotificationForBHandler received:" + notification.GetType().FullName);
}
}
This also looks much better in services:
# | Service type | Implementation type | Generic |
---|---|---|---|
1 | INotificationHandler<EntityNotificationTyped<A>> | EntityNotificationForAHandler | ❌ |
2 | INotificationHandler<EntityNotificationTyped<B>> | EntityNotificationForBHandler | ❌ |
3 | INotificationHandler<> | GenericNotificationHandler<> | ✅ |