Diagnosing and Fixing MediatR Container Issues

An issue I see come up quite frequently, much to the chagrin of DI container maintainers, are problems of complex generics edge cases and how they come up in MediatR. In fact, more than one container author has demanded some kind of recompense for the questions received and issues opened from the kinds of complex cases folks attempt with MediatR. This post won't go into whether folks should try these complex scenarios (it depends), but rather how to diagnose and fix them.

I had one recently come up that on face value, looks like it should work. It starts with having a notification that's a base type for other events:

public interface IIntegrationEvent : INotification
{
}

Then we define some concrete notification:

public class MyEvent : IIntegrationEvent
{
}

Finally, we create an INotificationHandler for this base notification type:

public class IntegrationEventHandler 
    : INotificationHandler<IIntegrationEvent>
{
    public Task Handle(
        IIntegrationEvent integrationEvent, 
        CancellationToken cancellationToken)
    {
        //DO SOME WORK
        return Task.CompletedTask;
    }
}

When running this in our application using mediator.Publish(new MyEvent()), the code above never gets hit. So how can we diagnose and fix the issue?

Diagnosing Container Registration

These types are all resolved from a container at runtime as MediatR simply defers to an IServiceProvider to resolve handlers/behaviors etc. To isolate first, we can try to write a unit test that does more or less what our application code does:

[Fact]
public async Task Should_publish_to_handler()
{
    var services = new ServiceCollection();
    services.AddMediatR(typeof(HandlerResolutionTests));

    var serviceProvider = services.BuildServiceProvider();

    var mediator = serviceProvider.GetRequiredService<IMediator>();

    var notification = new MyEvent();

    await mediator.Publish(notification);

    // Assert what??
}

We create a ServiceCollection and use MediatR to register our handlers in the container. Next, we build our ServiceProvider to be able to get an IMediator instance. Finally, we send our concrete INotification instance to get published.

We have a problem here - how do we assert that our handler was actually called? Publish returns only Task, there's nothing to assert in the return value. We can assert the side effects of the handler but that's a much bigger scope of assertion than we would like. Ideally, we just want to make sure it gets called.

We can substitute a mock instance and do "Assert this method was called" but I find those mocking assertions brittle.

Instead, I like to reduce the number of moving parts here and remove MediatR from the equation entirely. Since MediatR defers to the ServiceProvider to resolve services, we can assert resolutions directly from that ServiceProvider itself in a unit test:

[Fact]
public void Should_resolve_handlers()
{
    var services = new ServiceCollection();
    services.AddMediatR(typeof(HandlerResolutionTests));

    var serviceProvider = services.BuildServiceProvider();

    var handlers = serviceProvider
        .GetServices<INotificationHandler<MyEvent>>()
        .ToList();

    handlers.Count.ShouldBe(1);
    handlers.Select(handler => handler.GetType())
        .ShouldContain(typeof(IntegrationEventHandler));
}

In this test, I'll resolve the expected services that MediatR would resolve underneath the covers. I've completely eliminated MediatR from the equation here, so I can focus only on the container registration itself. As expected (or not?) this test fails:

Shouldly.ShouldAssertException
handlers.Count
    should be
1
    but was
0

That's good! We can now focus on fixing this test without worrying about how to test if a handler is called altogether or worrying about MediatR at all.

Fixing Container Registration Issues

When it comes to fixing these kinds of container issues, it's usually one of a few culprits:

  • This dependency is not registered
  • This dependency is registered, but not resolved
  • This dependency is registered and resolved but there was some generics exception to close a generic type

One way to diagnose the first is just see what's registered in the first place:

// in our unit test
foreach (var service in services)
{
    _output.WriteLine($"{service.ServiceType.FullName},{service.ImplementationType?.FullName}");
}

// output
MediatR.ServiceFactory,
MediatR.IMediator,MediatR.Mediator
MediatR.ISender,
MediatR.IPublisher,
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPreProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPostProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionProcessorBehavior`2
MediatR.INotificationHandler`1[[ClassLibrary1.IIntegrationEvent, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],ClassLibrary1.IntegrationEventHandler

So we can see at the bottom (in its weird generic type name way) that we have the service type of INotificationHandler<IIntegrationEvent> has a registered concrete type of IntegrationEventHandler. This means that our service is registered, but it might not be registered in with a type that the container understand how to put together. The container doesn't understand that if I ask for IEnumerable<INotificationHandler<MyEvent>> that it should also include this base type of INotificationHandler<IIntegrationEvent>, even though MyEvent : IIntegrationEvent.

So at this point, it's #2 above - the service is registered but not resolved. To fix, I can try to:

  • Extend the container registration to allow it to be resolved
  • Alter the handler type to allow it to be resolved
  • Change containers that know how to do this in the first place

Let's look at each of these in turn.

Extending Container Registration

For these one-off special cases, we can explicitly add a registration for the requested service/implementation types:

var services = new ServiceCollection();
services.AddMediatR(typeof(HandlerResolutionTests));
services.AddTransient<INotificationHandler<MyEvent>, IntegrationEventHandler>();

I'm filling in the connection so that when we ask for that concrete event/handler type, we also include the base handler type. With this additional registration, our test now passes. However, the list of registered events looks a bit odd:

MediatR.ServiceFactory,
MediatR.IMediator,MediatR.Mediator
MediatR.ISender,
MediatR.IPublisher,
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPreProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPostProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionProcessorBehavior`2
MediatR.INotificationHandler`1[[ClassLibrary1.IIntegrationEvent, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],ClassLibrary1.IntegrationEventHandler
MediatR.INotificationHandler`1[[ClassLibrary1.MyEvent, ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],ClassLibrary1.IntegrationEventHandler

As IntegrationEventHandler is registered twice. That means we leave ourselves open for that handler to get called twice. Not ideal! But explicit registration can still be useful in cases where the automatic registration is missed for whatever reason in services.AddMediatR.

Additionally, I had to close the INotificationHandler type to a concrete implementation of IIntegrationEvent. I'd have to do that registration for each and every implementation to make sure the handler gets called. To make that not break over time, I'd do some sort of assembly scanning to look for those derived types and register. Ugly code, but possible.

Alter the Handler Type

The definition of the notification handler type is contravariant for the TNotification parameter type, which means the compiler will allow me to successfully combine less derived types of the generic parameter but not necessarily the container. In order to "teach" the container that it should respect our variance rules, we can convert the handler to a constrained open generic:

public class IntegrationEventHandler<TIntegrationEvent>
    : INotificationHandler<TIntegrationEvent> 
    where TIntegrationEvent : IIntegrationEvent
{
    public Task Handle(
        TIntegrationEvent integrationEvent, 
        CancellationToken cancellationToken)
    {
        //DO SOME WORK
        return Task.CompletedTask;
    }
}

And alter our test accordingly to look for the closed generic type:

[Fact]
public void Should_resolve_handlers()
{
    var services = new ServiceCollection();
    services.AddMediatR(typeof(HandlerResolutionTests));

    foreach (var service in services)
    {
        _output.WriteLine($"{service.ServiceType.FullName},{service.ImplementationType?.FullName}");
    }

    var serviceProvider = services.BuildServiceProvider();

    var handlers = serviceProvider
        .GetServices<INotificationHandler<MyEvent>>()
        .ToList();

    handlers.Count.ShouldBe(1);
    handlers.Select(handler => handler.GetType())
        .ShouldContain(typeof(IntegrationEventHandler<MyEvent>));
}

And checking our registrations, we see the handler is only registered once:

MediatR.ServiceFactory,
MediatR.IMediator,MediatR.Mediator
MediatR.ISender,
MediatR.IPublisher,
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPreProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestPostProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionActionProcessorBehavior`2
MediatR.IPipelineBehavior`2,MediatR.Pipeline.RequestExceptionProcessorBehavior`2
MediatR.INotificationHandler`1,ClassLibrary1.IntegrationEventHandler`1

There's a catch here though - only the 5.0 release and later versions of Microsoft.Extensions.DependencyInjection support this constrained generics behavior. For a lot of folks this won't be an issue, but for some it may. Also, while this registration worked, other situations may not. For those cases, we may not want to alter our types at all and instead opt for a completely separate container altogether.

Change Containers

Sometimes we don't have to change anything about our types like we did in the previous example when using a 3rd-party container. For example, if we use our original types, and switch to Lamar:

[Fact]
public void Should_resolve_handlers_with_Lamar()
{
    var container = new Container(services =>
    {
        services.AddMediatR(typeof(HandlerResolutionTests));
    });

    _output.WriteLine(container.WhatDoIHave());

    var serviceProvider = (IServiceProvider)container;

    var handlers = serviceProvider
        .GetServices<INotificationHandler<MyEvent>>()
        .ToList();

    handlers.Count.ShouldBe(1);
    handlers.Select(handler => handler.GetType())
        .ShouldContain(typeof(IntegrationEventHandler));
}

There's nothing extra or special to register here, my test just passes. Lamar is just that much more powerful in its features that it can handle this situation out-of-the-box.

Which is right?

It depends. Some folks don't like to reference other containers, some don't mind, some already do. Some folks don't mind the open generics with constraints, some do. But inevitably, when you try to do more interesting/complex scenarios with the stock Microsoft DI container, you'll hit its limitations. When you do, it's best to remove MediatR from the equation and focus on what the container provides, and go from there.

There also might be cases where nothing will work because your generics use case is too complex. If you're spending a week trying to get your container(s) to make your situation work - take a step back and try to simplify your solution. Your future teammates will thank you!