How to implement cross cutting concerns with MediatR Pipeline Behaviours

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.

Conclusion

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.

Latest posts by Gary Woodfine (see all)