Validation with Mediator Pipelines in ASP.NET Core Applications

Validate commands, events or queries with transversal behavior

When implementing web applications that manipulate information, there is always the need to validate data sent by the clients, ensuring business rules are properly implemented.

Previously I talked about the implementation of Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) using a mediator pattern and how to support transversal behavior via pipelines.

In this article I’m going to demonstrate how validation can be enforced into your commands and events before reaching the handlers, making sure that invalid data will be rejected by the API.

This approach not only reduces the amount of duplicated code, it also ensures validations won’t be forgotten, unless explicitly stated.


The project

To make it faster to implement the validation pipeline, I’m going to leverage this example on both of my previous articles, in which we implemented an endpoint to manage products and introduced both a logging and timeout pipelines.

The source code is available on GitHub.

The validations

To help us configure the rules we are going to use a very popular and one of my favorites libraries FluentValidation by creating classes implementing the interface IValidator<T>.

These classes will be added to the dependency injection container and used by the pipeline to validate both the commands and events before reaching the handler.

Lets start by installing the NuGet FluentValidation:

Create a Validations folder at the project root level, with a subfolder Products.

Inside the Products folder create both a validator for CreateProductCommand and CreatedProductEvent by extending the class AbstractValidator<T> (which itself implementes the interface IValidator<T>) and configure some rules in the constructors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(e => e.Code)
.NotEmpty()
.Length(8)
.Matches("^[0-9a-zA-Z]*$");

RuleFor(e => e.Name)
.NotEmpty()
.MaximumLength(128);

RuleFor(e => e.Price)
.GreaterThanOrEqualTo(0);
}
}

public class CreatedProductEventValidator : AbstractValidator<CreatedProductEvent>
{
public CreatedProductEventValidator()
{
RuleFor(e => e.ExternalId)
.NotEmpty();

RuleFor(e => e.Code)
.NotEmpty()
.Length(8)
.Matches("^[0-9a-zA-Z]*$");

RuleFor(e => e.Name)
.NotEmpty()
.MaximumLength(128);

RuleFor(e => e.Price)
.GreaterThanOrEqualTo(0);
}
}

Feel free to create validators for all other commands and events, I’m just focusing on these for simplicity. You can also check the documentation for supported rules and detailed usage instructions.

Open the Startup.cs file and register all classes implementing IValidator<T> into the container:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.AddSwaggerGen();

services.AddDbContext<ApiDbContext>(o =>
{
o.UseInMemoryDatabase("ApiDbContext");
});

services.AddMediator(o =>
{
o.AddPipeline<LoggingPipeline>();
o.AddPipeline<TimeoutPipeline>();
o.AddHandlersFromAssemblyOf<Startup>();
});

foreach (var implementationType in typeof(Startup)
.Assembly
.ExportedTypes
.Where(t => t.IsClass && !t.IsAbstract))
{
foreach (var serviceType in implementationType
.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>)))
{
services.Add(new ServiceDescriptor(serviceType, implementationType, ServiceLifetime.Transient));
}
}
}

// ...
}

The pipeline

Because for this example we are going to enforce validation only on commands and events, since they either mutate or represent the system state at a given point in time, the pipeline will be implemented as follows:

  1. Intercept any command or event;
  2. Resolve a required instance of IValidator<TCommand> or IValidator<TEvent> from the container;
  3. Invoke the method ValidateAndThrowAsync, failing with a ValidationException if something is invalid;

Inside the Pipelines folder create a ValidationPipeline class extending Pipeline (because we only need some of the methods):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ValidationPipeline : Pipeline
{
private readonly IServiceProvider _provider;

public ValidationPipeline(IServiceProvider provider)
{
_provider = provider;
}

public override async Task OnCommandAsync<TCommand>(Func<TCommand, CancellationToken, Task> next, TCommand cmd, CancellationToken ct)
{
await ValidateAndThrowAsync(cmd, ct);
await next(cmd, ct);
}

public override async Task<TResult> OnCommandAsync<TCommand, TResult>(Func<TCommand, CancellationToken, Task<TResult>> next, TCommand cmd, CancellationToken ct)
{
await ValidateAndThrowAsync(cmd, ct);
return await next(cmd, ct);
}

public override async Task OnEventAsync<TEvent>(Func<TEvent, CancellationToken, Task> next, TEvent evt, CancellationToken ct)
{
await ValidateAndThrowAsync(evt, ct);
await next(evt, ct);
}

private async Task ValidateAndThrowAsync<T>(T instance, CancellationToken ct)
{
var validator = _provider.GetRequiredService<IValidator<T>>();
await validator.ValidateAndThrowAsync(instance, ct);
}
}

Open the Startup.cs file and add the ValidationPipeline at least after the LoggingPipeline ensuring, in case invalid data is submitted, we can still see it in the logs:

1
2
3
4
5
6
7
services.AddMediator(o =>
{
o.AddPipeline<LoggingPipeline>();
o.AddPipeline<TimeoutPipeline>();
o.AddPipeline<ValidationPipeline();
o.AddHandlersFromAssemblyOf<Startup>();
});

If you now start the server and try to create a product with invalid data you will receive a ValidationException.

Because returning an HTTP 500 due to invalid data would be confusing to the client, lets just finish this example by creating an ASP.NET Core middleware converting this exception into a more appropriate code, like HTTP 422.

Once again, open the Startup.cs file and register the middleware immediately after the developer exception page, catching this exception and returning HTTP 422 with a more detailed JSON representation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Startup
{
// ...

public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();

app.Use(async (ctx, next) =>
{
try
{
await next();
}
catch (ValidationException e)
{
var response = ctx.Response;
if (response.HasStarted)
throw;
ctx.RequestServices
.GetRequiredService<ILogger<Startup>>()
.LogWarning(e, "Invalid data has been submitted");
response.Clear();
response.StatusCode = 422;
await response.WriteAsync(JsonSerializer.Serialize(new
{
Message = "Invalid data has been submitted",
ModelState = e.Errors.ToDictionary(error => error.ErrorCode, error => error.ErrorMessage)
}), Encoding.UTF8, ctx.RequestAborted);
}
});

app.UseSwagger();

app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Mediator ExampleApi V1");
});

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

Submit invalid data again and a more detailed message should be returned:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /products
{
"code": "-123456",
"name": "",
"price": -1
}

HTTP 422
{
"Message":"Invalid data has been submitted",
"ModelState":{
"ExactLengthValidator":"'Code' must be 8 characters in length. You entered 7 characters.",
"RegularExpressionValidator":"'Code' is not in the correct format.",
"NotEmptyValidator":"'Name' must not be empty.",
"GreaterThanOrEqualValidator":"'Price' must be greater than or equal to '0'."
}
}

Speeding things up!

If just like me, you can see yourself using a pipeline for validation in most of your projects, there is already a pipeline available via NuGet that should be configurable to most use cases while also providing a simpler way to register the validators into the container.

Install SimpleSoft.Mediator.Microsoft.Extensions.ValidationPipeline via NuGet:

Use the extension method AddPipelineForValidation and enforce both command and event validations and use the extension method AddValidatorsFromAssemblyOf to scan for all IValidator<T> classes and register them into the container.

For the current project, the ConfigureServices method would be similar to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();

services.AddSwaggerGen();

services.AddDbContext<ApiDbContext>(o =>
{
o.UseInMemoryDatabase("ApiDbContext");
});

services.AddMediator(o =>
{
o.AddPipeline<LoggingPipeline>();
o.AddPipeline<TimeoutPipeline>();
o.AddPipelineForValidation(options =>
{
options.ValidateCommand = true;
options.ValidateEvent = true;
});
o.AddValidatorsFromAssemblyOf<Startup>();
o.AddHandlersFromAssemblyOf<Startup>();
});
}

// ...
}

Conclusion

I hope this article gave you a good idea on how to use mediator pipelines to ensure that all your commands, events and even queries are initialized with proper data, either implementing your own pipeline or by using the existing ValidationPipeline NuGet.

I also made an article about transaction management with pipelines, you may also find it helpful.