I have previously discussed the Mediator Design Pattern , which is one one of the Behavioral Patterns software design patterns defined by the Gang of Four in Design Patterns: Elements of Reusable Object-Oriented Software. I also introduced MediatR which is an implementation of the Mediator pattern that enables .net core developers to build simpler code by making all components communicate via a mediator object, instead of directly with each other. Helping code to remain highly decoupled and reducing complex dependencies between objects.
Design Patterns
Elements of Reusable Object-Oriented Software
23 patterns allow designers to create more flexible, elegant, and ultimately reusable designs without having to rediscover the design solutions themselves
In this post I will introduce one of my favourite features of MediatR that really makes it worthwhile using it to implement the Mediator pattern. This post will also start to tie up one of the loose ends which I started discussing in Implementing logging in .net core applications for logging, telemetry and your own sanity before veering off in what may at first seem like wild tangents in discussing Implementing logging with Serilog and Implement log monitoring with Seq and Serilog in .net Core . These were important aspects to discuss because as you'll discover in this article it suddenly all comes together, to help build software applications!
What are Pipelines
Requests/Responses travel back and forth through Pipelines in ASP.net core. When an Actor sends a request it passes the through a pipeline to the application, where an operation is performed using data contained in the request message. Once, the operation is completed a Response is sent back through the Pipeline containing the relevant data .
Pipelines are only aware of what the Request or Response are, and this is an important concept to understand when thinking about ASP.net Core Middleware.
Pipelines are also extremely handy when it comes to wanting implement common logic like Validation and Logging, primarily because we can write code that executes during the request enabling you to validate or perform logging duties etc.
MediatR Pipeline Behaviour
MediatR Pipeline behaviours were introduced in Version 3, enabling you execute validation or logging logic before and after your Command or Query Handlers execute, resulting in your handlers only having to deal with Valid requests in your CQRS implementation, and you don't have to clutter your Handler methods with repetitive logging or validation logic!
Example of MediatR Pipeline bahaviours
In order to really understand the benefits of MediatR, In my opinion it is really beneficial to understand CQRS (Command Query Response Segregation) and at a more high level the concepts of Domain Driven Design and familiarity of Software Design Patterns and SOLID Principles.
In this example we're going to build upon the foundations I discussed in Developing Api's using Http Endpoints and the Mediator Pattern. If you are unfamiliar with any of these concepts then I would urge you to go an read those articles. Along with gaining a broader understanding of the CQRS Architecture pattern
Add a validation pipeline behaviour
One of the most common things you'll need to do when using MediatR is validation. Most likely you'd like to validate your Request and Responses to ensure that they have all the data in the correct format. The important aspect here, is that you don't want to pollute or clutter your handler methods with repetitive validation logic. This is just inevitably increase the Cyclomatic complexity of your methods, and as software developers we define complexity as anything related to the structure of a software system that makes it hard to understand and modify the system.
Pipeline Behaviours enable us to implement Separation of Concerns software design principle , which is an important software architecture that aims to ensure that code is separated into layers and components that each have distinct functionality with as little overlap as possible.
A Philosophy of Software Design
addresses the topic of software design: how to decompose complex software systems into modules (such as classes and methods) that can be implemented relatively independently.
Fluent Validations
Fluent Validation is a free Open Source .NET validation library that enables the implementation of separation of concerns and develop clean, easy to create, and maintain validation logic.
We need to add the following Fluent Validation libraries to our project
dotnet add package FluentValidation dotnet add package FluentValidation.DependencyInjectionExtensions
We can now start to use the library. In our case we are going to add it to validate a command for Adding a new Salutation in our API, so in the Salutation --> Post directory we will another file which we will call the the CreateSalutationCommandValidator.cs
The code for this validator we will keep simple enough to understand at this point, and we will just ensure that no empty fields are supplied
using System.Data; using FluentValidation; namespace Cms.Endpoints.Salutations.Post { public class CreateSalutationCommandValidator : AbstractValidator<CreateSalutationCommand> { public CreateSalutationCommandValidator() { RuleFor(x => x.Abbreviation).NotEmpty(); RuleFor(x => x.Description).NotEmpty(); RuleFor(x => x.FullWord).NotEmpty(); } } }
We now need to complete a three more configuration steps before our API will actually make use of this Validator and the first of those is to wire up dependency injection and tell our Application to scan the assembly and wire up all the AbstractValidators that have been created in our application.
In our StartUp.cs
file in the ConfigureServices
method we need add the following line of code.
services.AddValidatorsFromAssembly(typeof(Startup).Assembly);
Then we need to create a class in the root of our application which we will call ValidationPipelineBehaviour.cs
Then we will add the following code to the file
using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentValidation; using MediatR; namespace Cms { public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators) { _validators = validators; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { if (_validators.Any()) { var context = new ValidationContext<TRequest>(request); var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); if (failures.Count != 0) throw new ValidationException(failures); } return await next(); } } }
At a high level what the code in this method does is act as a Middleware in the ASP .net core pipeline, and a list of the Validators that have been defined in the system are dependency injected. If any validators are defined then it determines if any are required for the currently executing request, and executes them and then displays the messages.
The last configuration step we now need to perform is again in the StartUp.cs
file in the ConfigureServices
method we need add the following line of code
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
All our configuration is complete and we can now see our validation working, if run the app and try execute our Post request and provide empty values
In follow up articles I will provide further details on how to tidy up those error messages and and leak so much application specific code and data, but as you can see this was really easy to accomplish and we did not have to clutter any of our Endpoint or Handler code with messy validation logic to implement our validation.
Logging Pipeline Behaviour
Logging is another area, where you don't really want to pollute your code with logging statements. I have previously explained this in Implementing logging in .net core applications for logging, telemetry and your own sanity, in that weaving logging code in amongst your business logic actually just adds to the complexity and ironically can often become the source of bugs when the primary reason to add logging to code is in order to help you troubleshoot and analyse the cause of bugs in your code.
Fortunately using MediatR we can use the exactly the same process as we just did for adding Validation Behaviour Pipelines to adding a Logging Behaviour Pipeline to log our Request and Response objects.
The first step is create a new LoggingBehaviour.cs class in the root of our application and add some similar code as before.
public class LoggingBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<LoggingBehaviour<TRequest, TResponse>> _logger; public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { //Request _logger.LogInformation($"Handling {typeof(TRequest).Name}"); Type myType = request.GetType(); IList<PropertyInfo> props = new List<PropertyInfo>(myType.GetProperties()); foreach (PropertyInfo prop in props) { object propValue = prop.GetValue(request, null); _logger.LogInformation("{Property} : {@Value}", prop.Name, propValue); } var response = await next(); //Response _logger.LogInformation($"Handled {typeof(TResponse).Name}"); return response; } }
Once we have done that we also as before need to add our Pipeline behaviour to our dependence injection pipeline by editing StartUp.cs
and adding the following line to our ConfigureServices
method.
builder.Services.AddMediatR(typeof(Program)) .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>)) .AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
That is it, this is all we have to do. Now If I run the application and fire off a few endpoints we should start to see our logging statements appear.
In Implement log monitoring with Seq and Serilog in .net Core we configured our sample application to make use of Serilog and Seq. We can now also use this functionality to review our logging details. If I open Seq by visiting http://localhost, assuming you are running the docker-compose script in the project as illustrated in How to run docker compose files in Rider , and inspect the logs I will now see the additional details appearing.
Conclusion
We have learned that MediatR offers a whole lot more than just a simple Mediator Pattern implementation but also that it enables us to develop code implementing more of CQRS pattern and it provides us powerful Pipeline Behaviours that we can use to develop powerful features in our code base.
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023