The MediatR Library
Clean Architecture is an Interface design. Everything is connected via Interfaces and their hidden Implementations. A request comes from the Framework layer to the Business handler via a Business interface. A request from the Business circle to the database or other services is also activated using an Adapter interface.
Mediator pattern and MediatR library are just another way to write Interfaces and
Implementations
(via the TRequest
/TResponse
type). In fact, you can simply define your own interface and
attach the corresponding implementation through your IOC container. However, the main reason I use
MediatR is because of its excellent dynamic pipeline behavior, which helps me separate most of the
cross-cutting concerns out of the main handler, makes everything cleaner and produces a concise,
testable handler class.
A very simple handler in MediatR looks like this
public class InsertUser
{
/// <summary>
/// Insert a new User and return that User
/// </summary>
public class Request : IRequest<Response>
{
public int ClientId { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
public class Response
{
public User User { get; set; }
}
public class Handler : IRequestHandler<Request, Response>
{
public async Task<Response> Handle(Request request, CancellationToken cancellationToken)
{
// implement your logic here
await CheckExistence(request.UserName);
// implement your logic here
var user = await SomeFunction(cancellationToken);
return new Response
{
User = user
}
}
}
}
In this simplest form, it doesn’t look so different from the way we usually do with a normal Interface. However, let’s imagine what will happen when you want to add these requirements
- Log the related information to debug later.
- Track the process metrics to monitor and analyze performance.
- Lock the process to avoid race-condition.
- Transform the request/response format in a pre-defined way.
- Handle errors.
- Other cross-cutting concerns?…
- A more important question: How to group related requests and re-use these cross-cutting concern handlers?
MediatR Request Pipeline - A comprehensible solution
MediatR Request Pipeline is just another kind of Request Middleware. You simply need to
define the Behavior that you want for a type and make
sure your Request
object follow that type.
Here is an example about how to decouple logging and monitoring logic for the above Handler
public class LoggingBehavior : IPipelineBehavior<Request, Response>
{
private readonly ILogger _logger;
public LoggingBehavior(ILogger logger)
{
_logger = logger;
}
public async Task<Response> Handle(
Request request, CancellationToken cancellationToken, RequestHandlerDelegate<Response> next)
{
// log the input data
_logger.Log(LogLevel.Information, "Creating new user...");
_logger.Log(LogLevel.Information, new { request.Username }.ToString());
var res = await next();
// log the created data
var createdUser = res.User;
_logger.Log(LogLevel.Information, "New user created!");
_logger.Log(LogLevel.Information, new { createdUser.Id }.ToString());
return res;
}
}
and then register it with your DI Container (Autofac in this case)
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<LoggingBehavior>().As<IPipelineBehavior<Request, Response>>();
}
}
As you can see, the main Handler
class doesn’t change at all. We don’t need to update any test case
to adapt with this new change. You don’t need to prepare or mock anything related to your Logging
and Monitoring service. You can focus on testing only the main Business logic. All the
cross-cutting concerns could be easily abstracted away.
Group related Requests and Reuse Behaviors
In case you want to group related Requests and Reuse the existing Behaviors, all you need to do is to define an Interface for that group and add a generic Behavior for that type. Let’s say that you want to track the processing time for each different client to monitor which one causes performance problem for your system, here is how
public interface IWithMonitoring
{
int ClientId { get; }
}
public class MonitoringBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IWithMonitoring
{
private readonly ILogger _logger;
public MonitoringBehavior(ILogger logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
// measure the processing time
var stopWatch = new Stopwatch();
stopWatch.Start();
var response = await next();
stopWatch.Stop();
var data = $"Client {request.ClientId} - Request {nameof(TRequest)} - Time: {stopWatch.ElapsedMilliseconds}";
_logger.Log(LogLevel.Information, data);
return response;
}
}
// Make sure your Request class satisfy the type
public class Request : IRequest<Response>, IWithMonitoring
{
// ...
}
and of course, register the generic type with your IOC container
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterGeneric(typeof(MonitoringBehavior<,>)).As(typeof(IPipelineBehavior<,>));
}
}
Done, everything is applied automagically!
Limitations
The biggest disadvantage I have with MediatR is the lack of circular-dependencies protection. The easiest workaround is to structure your application following a multiple level design, where each Handler can only activate the Handlers in the lower level. This is achieved by coding convention and code reviewing.