jbogard / MediatR

Simple, unambitious mediator implementation in .NET
Apache License 2.0
11k stars 1.16k forks source link

Handlers for interfaced notifications are not working as expected #1050

Open Kjelli opened 1 month ago

Kjelli commented 1 month ago

The issue at hand

When using interfaced notifications, and handlers for the interfaces - the handler does not run unless there exists a handler targetting a concrete implementation of the respective interface.

Steps to reproduce:

Bootstrap

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(Program).Assembly));
using IHost host = builder.Build();

var mediator = host.Services.GetRequiredService<IMediator>();

A) A simple notification handler that runs

// Simple case, nothing new

record SimpleGreeterNotification(string Message) : INotification;
class SimpleHandler : INotificationHandler<SimpleGreeterNotification>
{
    public Task Handle(SimpleGreeterNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine(notification.Message);
        return Task.CompletedTask;
    }
}

// ...

await mediator.Publish(new SimpleGreeterNotification("Hello")); 
// Outputs "Hello", no surprises here

B) An interfaced notification handler that runs

// Interfaced case

public interface IGreeterInterface : INotification;
record ImplementingGreeterNotification(string Message) : IGreeterInterface;

class InterfacedGreeterHandler : INotificationHandler<IGreeterInterface>
{
    public Task Handle(IGreeterInterface notification, CancellationToken cancellationToken)
    {
        Console.WriteLine("I only run when ImplementingGreeterHandler exists");
        return Task.CompletedTask;
    }
}

class ImplementingGreeterHandler : INotificationHandler<ImplementingGreeterNotification>
{
    public Task Handle(ImplementingGreeterNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine(notification.Message);
        return Task.CompletedTask;
    }
}

// ...

await mediator.Publish(new ImplementingGreeterNotification("Hello interface"));
// Outputs "I only run when ImplementingGreeterHandler exists"
// Outputs "Hello interface

C) Now for the interfaced notification handler that does not run

// Interfaced case

public interface IGreeterInterface : INotification;
record ImplementingGreeterNotification(string Message) : IGreeterInterface;

class InterfacedGreeterHandler : INotificationHandler<IGreeterInterface>
{
    public Task Handle(IGreeterInterface notification, CancellationToken cancellationToken)
    {
        Console.WriteLine("I only run when ImplementingGreeterHandler exists");
        return Task.CompletedTask;
    }
}

// ...

await mediator.Publish(new ImplementingGreeterNotification("Hello interface"));
// No output

Cases A and B are as expected, whereas case C is strange - I would expect InterfacedGreeterHandler to be run.

Further notes

I am not sure any of this is relevant, but I did some investigating.

I have a theory that this issue is caused near NotificationHandlerWrapper. Inspecting the service collection, I verify that it correctly contains INotificationHandler<IGreeterInterface>, but it does not contain INotificationHandler<ImplementingGreeterNotification>.

From these lines I see that IServiceProvider.GetServices<INotificationHandler<ImplementingGreeterNotification>> does not return anything, which I suspect subsequently fails to resolve a respective NotificationHandlerExecutor. https://github.com/jbogard/MediatR/blob/cac76bee59a0f61c99554d76d4c39d67810fb406/src/MediatR/Wrappers/NotificationHandlerWrapper.cs#L24-L26

It does make sense I suppose, but at face value this looks like odd behavior either way.

I tested a more basic, yet potentially related case; implementing a catch-all INotificationHandler<INotification> never runs if there are no other INotificationHandler-types registered.