I have posted before about How to use MediatR pipeline behaviours where I introduced and provided some pretty common examples where you could make use of Pipeline Behaviours. In this post, I wanted to dive a little deeper into this topic to illustrate how you can use MediatR to help implement clean code practices and elements of Aspect Oriented Programming (AOP) with MediatR.
What is Aspect Oriented Programming
Aspect-oriented programming (AOP) is a programming paradigm that isolates the supporting functions from the main program’s business logic.
AOP includes programming methods and tools supporting modularisation of concerns at the source code level, but it may also refer to the entire software engineering discipline.
The loss of modularity occurs at the intersection between concerns and AOP helps to return to modularity. This process of intersection, also known as weaving, occurs at build or runtime.
Weaving helps in a number of processes, such as:
- Replacing method bodies with new implementations
- Inserting code before and after method calls
- Implementing variable reads and writes
- Associating new states and behaviours with existing classes
AOP logic is implemented in an aspect class independent of later-augmented classes. Once implemented, it can be attached to any library class without aspect class awareness.
What are Cross Cutting concerns
In a typical layered architecture for software solution, but there will always a need for common functionality to be used within layers. such as:
- Validation
- Logging
- Exception handling
- Security
- Performance
- Auditing
- Caching
- Retry
These features are usually applicable across all layers, therefore will often have a common implementation. Typically objectives you would want to achieve in a cross cutting concern is to
We want to do a few computations by taking up each of these methods, by turn at the following stages:
- Do something before the method begins execution
- Do something after the method completes execution
- Track progress and execution during execution
I've discussed previously Implementing logging in .net core applications for logging, telemetry and your own sanity the dangers of weaving logging logic code amongst your business logic code and a few alternative practices you could use to overcome this problem, then in How to use MediatR Pipeline Behaviours I provided an example of how to go solve the problem making use of MediatR.
What are Pipeline behaviours
Pipeline behaviours are a type of middleware that get executed before and after a request. They are extremely useful for defining common cross cutting concern logic.
MediatR pipeline behaviours work in much the same way as you would expect from ASP.net core middleware. ,which is software that's assembled into an application pipeline to handle Requests and Responses. Each middle-ware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline.
Pipeline behaviours enable you to chain multiple behaviours together, so that each behaviour gets executed until it reaches the end of the chain and then the actual request handler is executed, then the response of which is then passed back up the chain.
Basic structure of Pipeline Behaviour
A Pipeline behaviour is essentially just a class that implements the IPipelineBehaviour
interface.
public class SampleBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { public ValidationBehaviour() { } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { return await next(); } }
This enables to us to implement almost any logic that that we want to execute in our MediatR pipelines. Making use of this simple pattern we are able to encapsulate any logic to transform the input and outputs in a single method.
Any complexities can be encapsulated and isolated from the rest of the application code, so if we need to apply any refactoring we only have to edit 1 method. As systems become more complex, isolating side-effects becomes critical for maintaining overall speed of delivery and minimise risk.
Simple Validation Pipeline
In order to go through this process lets implement a very basic Validation Pipeline for our REST API application.
In my blog tutorial repository I have Country application, which primarily gets Country data from the World Bank API and transforms it and returns via a REST API. I make use of my API Endpoint project template.
I won't discuss the full implementation of the application here, as that is the subject of other blog posts, but I will discuss how to implement the Query validation making use of the Validation Pipeline behaviour. For those unfamiliar with CQRS approach and the concepts of Commands and Queries check out What is CQRS
In our project we will create a Query Class which will have 1 property on it and we would like to implement some basic validation for the property. The following rules will need to be implemented.
- A 2 or 3 alpha characters are only allowed
- Country code cannot be empty
- country code cannot be null
- No numeric characters are allowed
Lets start by creating our simple Query class
using MediatR; using Microsoft.AspNetCore.Mvc; namespace Boleyn.Countries.Activities.Sample.Get { public class Query : IRequest<Response> { [FromRoute(Name = "isoCode")] public string CountryCode { get; set; } } }
We are now going to make use of FluentValidation to create a validator class for our Query
using FluentValidation; namespace Boleyn.Countries.Activities.Sample.Get { public class Validator : AbstractValidator<Query> { private const string IncorrectIsoCode = "Incorrect ISO country code value provided"; public Validator() { RuleFor(c => c.CountryCode).NotEmpty().Length(2,3).Matches(@"^[a-zA-Z ]+$").WithMessage(IncorrectIsoCode);; } } }
We will now create a MediatR pipeline behaviour to ensure that our Validator is executed. We'll implement some logging into our Pipeline too, so we can log the details of validation error out too.
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse> { private readonly IEnumerable<IValidator<TRequest>> _validators; private readonly ILogger _logger; public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators, ILogger logger) { _validators = validators; _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { // If no validators have been defined for the currently executing request exit if (!_validators.Any()) return await next(); // Check the currently executing context for any validation errors 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 now failures exit if (!failures.Any()) return await next(); // If we have failures iterate through them all get the details and then our Validation Error var sb = new StringBuilder(); failures.ForEach(f => { _logger.Information($"Validation Error: {f.PropertyName} {f.Severity} {f.AttemptedValue} "); sb.Append(f.ErrorMessage); }); throw new CountryValidationException(sb.ToString()); } }
With the code now complete we can now configure the dependency injection in our start up to enable MediatR to make use of our Pipeline. In our Startup.cs
in the ConfigureServices
we can set it up as follows
services.AddValidatorsFromAssembly(typeof(Startup).Assembly); services.AddMediatR(typeof(Startup)).AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
We can now run our application and execute a request that should fail validation and see what the response with validation message.
We can also check our log entries, in this instance we using Serilog and Seq for logging infrastructure, so we can go to seq and view our log entries
Improvements
The above approach will work, but there are a couple of issues with it. Even though we have taking the time and effort to isolate our cross cutting concerns, we have also inadvertently introduced some too.
We are making use of throwing exceptions on a validation exceptions and then handling the exceptions in our controller or handlers. This is not a great approach, because in the case of Validation, these should be expected and not exceptional circumstances. If we're testing for validation then we should not throw an exception if we find them, we should handle them more elegantly.
Secondly, throwing exceptions can incur a performance hit on our application which we should careful not to introduce as this is weaving technical debt into our application from the start. We also don't want to clutter our controllers or Mediatr handlers to with the unnecessary try{}catch{}
blocks.
I have devised a pattern, which I think is acceptable utilising both Pipeline Behaviours and Middleware to handle exceptions in my Web API. The full sample code can be found in my Blog Tutorials repository in the Countries project.
Basically the API calls the World Bank Countries API and gets some basic country if you provide a ISO code. There is some validation to check that the parameter provided is only between 2 & 3 Characters, Doesn't contain any Numerical Values. The application is based on the pattern I use in the Developing Api’s using Http Endpoints
Lets first take a look at the validation class for the query using FluentValidation, I only have the 1 property in my query, but as you'll see there is quite a bit of validation that I am putting on it.
using FluentValidation; namespace Boleyn.Countries.Activities.Country.Get { public class Validator : AbstractValidator<Query> { private const string IncorrectIsoCode = "Incorrect ISO country code value provided"; public Validator() { RuleFor(c => c.IsoCode).NotNull().NotEmpty() .Length(2,3).WithMessage("must be between {MinLength} and {MaxLength} characters. {TotalLength} were provided") .Matches(@"^[a-zA-Z ]+$").WithMessage(IncorrectIsoCode);; } } }
Next I want to implement my Validation PIpeline behaviour, the important thing to note here is that my Validation Pipeline behaviour will be generic and execute on all Mediatr request/response pipelines so the reality is if I do incur a validation error, I probably do want to exit out of the processing of the rest of the pipelines as early as possible, because there is no need to continue processing if we have errors.
I have started to develop my own custom API response library, which defines either a Single Result or List response which will typically be returned from an API. Both response types inherit from a BaseResponse object which defines a List of Errors
public abstract class BaseResponse { private readonly IList<KeyValuePair<string, string[]>> _errorMessages; protected BaseResponse( IList<KeyValuePair<string, string[]>> errors = null) { _errorMessages = errors ?? new List<KeyValuePair<string, string[]>>(); } public bool IsValid => !_errorMessages.Any(); public IList<KeyValuePair<string, string[]>> Errors => _errorMessages; }
public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TResponse : class { private readonly IEnumerable<IValidator<TRequest>> _validators; private readonly ILogger _logger; public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators, ILogger logger) { _validators = validators; _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { if (!typeof(TResponse).IsGenericType) return await next(); if (!_validators.Any()) return await next(); 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) .GroupBy( x => x.PropertyName, x => x.ErrorMessage, (propertyName, errorMessages) => new { Key = propertyName, Values = errorMessages.Distinct().ToArray() }) .ToDictionary(x => x.Key, x => x.Values); // IF no Validation Errors continue to next Behaviour if (!failures.Any()) return await next(); // Else exit to calling method error response with errors return Activator.CreateInstance(typeof(TResponse), null, failures.ToList()) as TResponse; } }
In our controller we simply query the IsValid of the response returned to return our response as desired
[HttpGet("{isoCode}")] [SwaggerOperation( Summary = "Retrieve a sample response by id ", Description = "Retrieves a sample response ", OperationId = "EF0A3653-153F-4E73-8D20-621C9F9FFDC9", Tags = new[] { "country" }) ] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Response))] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(BadRequestObjectResult))] [ProducesErrorResponseType(typeof(BadRequestResult))] [Produces("application/json")] public override async Task<ActionResult<Response>> HandleAsync([FromRoute] Query query, CancellationToken cancellationToken = new()) { var result = await _mediator.Send(query, cancellationToken); return result.IsValid ? new OkObjectResult(result.Item) : new BadRequestObjectResult(result.Errors); }
Exception Middleware
We don't want to handle exceptions in our controller, so what we'll also do is implement some additional middleware using dotnet core middleware to handle, reformat and report errors to our client, in this case API user.
internal sealed class ExceptionHandlingMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { try { await next(context); } catch (Exception e) { await HandleException(context, e); } } private static async Task HandleException(HttpContext httpContext, Exception exception) { var statusCode = GetStatusCode(exception); var response = new { title = GetTitle(exception), status = statusCode, detail = exception.Message, errors = GetErrors(exception) }; httpContext.Response.ContentType = "application/json"; httpContext.Response.StatusCode = statusCode; await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response)); } private static string GetTitle(Exception exception) => exception switch { ValidationException ve => ve.Title, NotFoundException nf => nf.Title, _ => "Server Error" }; private static int GetStatusCode(Exception exception) => exception switch { ValidationException => StatusCodes.Status400BadRequest, NotFoundException => StatusCodes.Status404NotFound, _ => StatusCodes.Status500InternalServerError }; private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception) { IReadOnlyDictionary<string, string[]> errors = null; if (exception is ValidationException validationException) { errors = validationException.Errors; } return errors; } }
We now simply want to wire up our dependencies in our Start up. In this case we are wiring up our mediatr pipelines in the ConfigureServices
services.AddValidatorsFromAssembly(typeof(Startup).Assembly); services.AddMediatR(typeof(Startup)) .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>)) .AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
Then we wire up our Middleware in our Configure
method
app.UseMiddleware<ExceptionHandlingMiddleware>();
There are some additional steps in the code i.e. Setting up resource files that I created to store text etc that I will use, I urge you to take a look at the sample code
With all the configuration complete if we execute our end point and supply an incorrect value we'll be returned a helpful response
Conclusion
In this example I've provided an example of how you can use both mediatr pipeline behaviours and dotnet core middleware to implement an effective Validation and Exception handling process for your application.
Over the next few weeks I will provide some more examples of some typical Pipeline Behaviours would probably want to implement to address Cross Cutting Concerns in your applicaitons.
- 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