The last major release of MediatR brought a simplification in design. Instead of having several different IRequest types and IRequestHandler implementations with several flavors, MediatR would try at runtime to determine how a single IRequest should resolve to different IRequestHandler implementations (synchronous, async, async with a cancellation token). In practice, this proved problematic as not all containers support this sort of 'try-resolve' behavior. MediatR relied on try...catch to resolve, which can work, has unintended side effects.

MediatR 4.0 consolidates these design decisions. We'll still have a single IRequest type, but will now have only a single IRequestHandler interface. This simplifies the registrations quite a bit:

// Before (StructureMap)
cfg.Scan(scanner =>  
{
    scanner.AssemblyContainingType<Ping>();
    scanner.ConnectImplementationsToTypesClosing(typeof(IRequestHandler<,>));
    scanner.ConnectImplementationsToTypesClosing(typeof(IRequestHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(IAsyncRequestHandler<,>));
    scanner.ConnectImplementationsToTypesClosing(typeof(IAsyncRequestHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(ICancellableAsyncRequestHandler<,>));
    scanner.ConnectImplementationsToTypesClosing(typeof(ICancellableAsyncRequestHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(INotificationHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(IAsyncNotificationHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(ICancellableAsyncNotificationHandler<>));
});

// After
cfg.Scan(scanner =>  
{
    scanner.AssemblyContainingType<Ping>();
    scanner.ConnectImplementationsToTypesClosing(typeof(IRequestHandler<,>));
    scanner.ConnectImplementationsToTypesClosing(typeof(IRequestHandler<>));
    scanner.ConnectImplementationsToTypesClosing(typeof(INotificationHandler<>));
});

This new default interface is the previous ICancellableAsyncRequestHandler interface:

public interface IRequestHandler<in TRequest, TResponse>  
    where TRequest : IRequest<TResponse>
{
    Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}

public interface IRequestHandler<in TRequest>  
    where TRequest : IRequest
{
    Task Handle(TRequest message, CancellationToken cancellationToken);
}

Instead of multiple interfaces, MediatR 4.0 (re)introduces helper base classes:

  • RequestHandler - for synchronous actions
  • AsyncRequestHandler - for async actions that ignore the cancellation token

With the simplified interfaces, it's much less possible to "forget" a registration in your container.

Another small but breaking change is the behavior pipeline and pre-processor now have the cancellation token as part of the interface.

Both the MediatR and MediatR.Extensions.Microsoft.DependencyInjection packages are released with the simplified API.

Since these are breaking changes to the API (hence the major version bump), migration to the new API is manual. However, to ease the transition, you can follow the 3.0 to 4.0 migration guide. I've also updated the docs to walk through the new classes, and the samples include the updated registrations for the major containers out there:

  • Microsoft DI
  • Autofac
  • DryIoc
  • LightInject
  • Ninject
  • Simple Injector
  • StructureMap
  • Unity
  • Windsor

A big move for MediatR to break the API, but necessary to remove the complexity in multiple interfaces and registration. The only other option would be to make MediatR responsible for all registration and wiring, which is more than I want MediatR to do. The design goal of MediatR is to lean on the power of DI containers for registration, and MediatR merely resolves its interfaces.

Enjoy!