We recently started using MediatR in a project and its a great way to make your services simpler as requirements grow. I want to show a simple example to give an idea on why you would use it.
I’ve created a person controller that has a search endpoint that in theory will get a Person object from some store based on the Name.
[ApiController]
[Route("[controller]")]
public class PersonController : ControllerBase
{
private readonly ILogger<PersonController> _logger;
private readonly PersonService personService;
public PersonController(ILogger<PersonController> logger, PersonService personService)
{
_logger = logger;
this.personService = personService;
}
[HttpGet]
public async Task<Person> Get([FromQuery] SearchPerson searchPerson)
{
return await personService.Get(searchPerson);
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class SearchPerson
{
public string Name { get; set; }
}
public class PersonService
{
public async Task<Person> Get(SearchPerson search)
{
return new Person { Name = "Bob", Id = 1 };
}
}
This is working great but now we get a new requirement that we need to Audit all calls to our PersonService. Traditionally, you most probably would add this to your Get call in the service to ensure that each call is logged.
public interface IAudit<TRequest>
{
Task Create(string category, TRequest request);
}
public class Audit<TRequest> : IAudit<TRequest>
{
private readonly ILogger<Audit<TRequest>> logger;
public Audit(ILogger<Audit<TRequest>> logger)
{
this.logger = logger;
}
public async Task Create(string category, TRequest request)
{
logger.LogInformation("{category}: Request: {request}", request);
return;
}
}
public class PersonService
{
private readonly IAudit<SearchPerson> audit;
public PersonService(IAudit<SearchPerson> audit)
{
this.audit = audit;
}
public async Task<Person> Get(SearchPerson request, CancellationToken cancellationToken)
{
await audit.Create("Person", request);
return new Person { Name = "Bob", Id = 1 };
}
}
This definitely works but it breaks the single-responsibility principle. What happens if I then need to do another operation on each search. This class is going to get more complicated and its dependencies are going to grow.
Lets see how MediatR can help here. To use it I will need to make a few small changes to my classes and registrations.
My ConfigureServices will ask MediatR to look through the current assembly. If your solution is split then add the relevant assemblies.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<PersonService>();
services.AddScoped(typeof(IAudit<>), typeof(Audit<>));
services.AddMediatR(Assembly.GetExecutingAssembly());
}
I then make my search model implement the IRequest interface. This means that this model is used to return a Person object.
public class SearchPerson : IRequest<Person>
{
public string Name { get; set; }
}
My PersonService will implement the IRequestHandler interface, meaning that it can handle a SearchPerson and return a Person. My search method now also is called Handle and contains a CancellationToken.
public class PersonService : IRequestHandler<SearchPerson, Person>
{
private readonly IAudit<SearchPerson> audit;
public PersonService(IAudit<SearchPerson> audit)
{
this.audit = audit;
}
public async Task<Person> Handle(SearchPerson request, CancellationToken cancellationToken)
{
await audit.Create("Person", request);
return new Person { Name = "Bob", Id = 1 };
}
}
Then instead of injecting my PersonService into my controller I inject the IMediator and just Send the SearchPerson object to get a Person response.
[ApiController]
[Route("[controller]")]
public class PersonController : ControllerBase
{
private readonly ILogger<PersonController> _logger;
private readonly IMediator mediator;
public PersonController(ILogger<PersonController> logger, IMediator mediator)
{
_logger = logger;
this.mediator = mediator;
}
[HttpGet]
public async Task<Person> Get([FromQuery] SearchPerson searchPerson)
{
return await mediator.Send(searchPerson);
}
}
I will admit that stopping here most probably leaves the code more complex with little upside. The magic comes in when using a IPipelineBehavior. This is middleware for your MediatR requests which will sit between your IRequestHandler and the Send call.
Let me show you.
This is the QueryAudit PipelineBehavior which is a generic class that takes in a Request and category for the audit call.
public class QueryAudit<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly string category;
private readonly IAudit<TRequest> audit;
public QueryAuditor(string category, IAudit<TRequest> audit)
{
this.category = category;
this.audit = audit;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
await this.audit.Create(category, request);
return await next();
}
}
To use this, I created a ServiceExtensions class that can add it in the service registrations.
public static class ServiceExtensions
{
public static IServiceCollection AuditQuery<TRequest, TResponse>(this IServiceCollection services, string category)
where TRequest : IRequest<TResponse>
{
return services
.AddTransient<IPipelineBehavior<TRequest, TResponse>>
(
sc => new QueryAudit<TRequest, TResponse>(category, sc.GetRequiredService<IAudit<TRequest>>())
);
}
}
You can now just register it like this.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<PersonService>();
services.AddScoped(typeof(IAudit<>), typeof(Audit<>));
services.AddMediatR(Assembly.GetExecutingAssembly())
.AuditQuery<SearchPerson, Person>(category:"Person");
}
This will then simplify the PersonService to how it was before the audit requirement.
public class PersonService : IRequestHandler<SearchPerson, Person>
{
public async Task<Person> Handle(SearchPerson request, CancellationToken cancellationToken)
{
return new Person { Name = "Bob", Id = 1 };
}
}
You can image many different calls adding auditing by just registering the pipeline behaviour for them. Each class keeps to its own responsibilities without being polluted with unrelated requirements.