In many projects error handling and validation is distributed across business logic, API controllers, data access layer in the form of conditions (“if-else” sequences). This leads to the violation of the Separation of Concerns Principle and results in “Spaghetti code”, like in the example below.
....
if (user != null)
{
if (subscription != null)
{
if (term == Term.Annually)
{
// code 1
}
else if (term == Term.Monthly)
{
// code 2
}
else
{
throw new InvalidArgumentException(nameof(term));
}
}
else
{
throw new ArgumentNullException(nameof(subscription));
}
}
else
{
throw new ArgumentNullException(nameof(user));
}
.....
In this article I describe the approach to splitting validation and error handling logic from the other application layers. The patterns and practices used below can be found in the Git Hub repository below.
Architecture overview
For simplicity I use N-tire architecture, however, explained approaches can be reused in CQRS, Event Driven, Micro Services, SOA, etc architectures.
Example architecture includes following layers:
- Presentation Layer — UI / API
- Business Logic Layer — Services or Domain Services (in case you have DDD architecture)
- Data Layer / Data Access Layer
In the diagram below shows the components and modules which belong to different layers and contains presentation/API layer, business logic layer, data access, in the right side and related validation and error handling logic in the left side.
The validation and error handling architecture contains several components which I will describe in next few sections.
API validation level
API controllers may contain a lot of validation such as parameters check, model state check etc like on example below. I will use declarative programming to move validation logic out from API controller.
[HttpGet]
[SwaggerOperation("GetDevices")]
[ValidateActionParameters]
public IActionResult Get([FromQuery][Required]int page, [FromQuery][Required]int pageSize)
{
if (!this.ModelState.IsValid)
{
return new BadRequestObjectResult(this.ModelState);
}
return new ObjectResult(deviceService.GetDevices(page, pageSize));
}
API controllers can be easily cleaned by creating validation model attribute. Example below contains simple model validation check.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace DeviceManager.Api.ActionFilters
{
/// <summary>
/// Intriduces Model state auto validation to reduce code duplication
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute" />
public class ValidateModelStateAttribute : ActionFilterAttribute
{
/// <summary>
/// Validates Model automaticaly
/// </summary>
/// <param name="context"></param>
/// <inheritdoc />
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
}
Just add this attribute to the startup.cs
services.AddMvc(options =>
{
options.Filters.Add(typeof(ValidateModelStateAttribute));
});
To validate parameters of API action methods I will create an attribute and move validation logic. Logic inside attribute checks if parameters contains validation attributes and validates the value.
Now attribute can be added to the action method, if necessary. (examples below)
[HttpGet]
[SwaggerOperation("GetDevices")] [ValidateActionParameters]
public IActionResult Get(
[FromQuery, Required]int page,
[FromQuery, Required]int pageSize)
{
return new ObjectResult(deviceService.GetDevices(page, pageSize)); }
Business layer validation
Business layer validation consists 2 components: validation service and validation rules.
In the device validation services I’ve moved all custom validation and rule based validation logic from the service (Device service in the example below). This idea quite similar to using Guard pattern. Below the example of the validation service.
using System;
using DeviceManager.Api.Model;
using DeviceManager.Api.Validation;
using FluentValidation;
namespace DeviceManager.Api.Services
{
/// <inheritdoc />
public class DeviceValidationService : IDeviceValidationService
{
private readonly IDeviceViewModelValidationRules deviceViewModelValidationRules;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceValidationService"/> class.
/// </summary>
/// <param name="deviceViewModelValidationRules">The device view model validation rules.</param>
public DeviceValidationService(
IDeviceViewModelValidationRules deviceViewModelValidationRules)
{
this.deviceViewModelValidationRules = deviceViewModelValidationRules;
}
/// <summary>
/// Validates the specified device view model.
/// </summary>
/// <param name="deviceViewModel">The device view model.</param>
/// <returns></returns>
/// <exception cref="ValidationException"></exception>
public IDeviceValidationService Validate(DeviceViewModel deviceViewModel)
{
var validationResult = deviceViewModelValidationRules.Validate(deviceViewModel);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
return this;
}
/// <summary>
/// Validates the device identifier.
/// </summary>
/// <param name="deviceId">The device identifier.</param>
/// <returns></returns>
/// <exception cref="ValidationException">Shuld not be empty</exception>
public IDeviceValidationService ValidateDeviceId(Guid deviceId)
{
if (deviceId == Guid.Empty)
{
throw new ValidationException("Should not be empty");
}
return this;
}
}
}
In the rules I’ve moved all possible validation checks related to the view or API models. In the example below you can see Device view model validation rules. The validation itself triggers inside the the validation service.
The Validation rules based on FluentValidation framework which allows you you build rules in fluent format.
using DeviceManager.Api.Model;
using FluentValidation;
namespace DeviceManager.Api.Validation
{
/// <summary>
/// Validation rules related to Device controller
/// </summary>
public class DeviceViewModelValidationRules : AbstractValidator<DeviceViewModel>, IDeviceViewModelValidationRules
{
/// <summary>
/// Initializes a new instance of the <see cref="DeviceViewModelValidationRules"/> class.
/// <example>
/// All validation rules can be found here: https://github.com/JeremySkinner/FluentValidation/wiki/a.-Index
/// </example>
/// </summary>
public DeviceViewModelValidationRules()
{
RuleFor(device => device.DeviceCode)
.NotEmpty()
.Length(5, 10);
RuleFor(device => device.DeviceCode)
.NotEmpty();
RuleFor(device => device.Title)
.NotEmpty();
}
}
}
Exception handling Middleware
The last turn I will cover errors/exception handling. I address this topic to the end as all validation components generate exceptions and the centralized component that handles them and provide proper JSON error object is required to have.
In the example below I’ve used .NET core Middleware to catch all exceptions and created HTTP error status, according to Exception Type (in ConfigurationExceptionType method) and build error JSON object.
Also Middleware can be used to log all exception in one place.
using System;
using System.Net;
using System.Threading.Tasks;
using DeviceManager.Api.Model;
using FluentValidation;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace DeviceManager.Api.Middlewares
{
/// <summary>
/// Central error/exception handler Middleware
/// </summary>
public class ExceptionHandlerMiddleware
{
private const string JsonContentType = "application/json";
private readonly RequestDelegate request;
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionHandlerMiddleware"/> class.
/// </summary>
/// <param name="next">The next.</param>
public ExceptionHandlerMiddleware(RequestDelegate next)
{
this.request = next;
}
/// <summary>
/// Invokes the specified context.
/// </summary>
/// <param name="context">The context.</param>
/// <returns></returns>
public Task Invoke(HttpContext context) => this.InvokeAsync(context);
async Task InvokeAsync(HttpContext context)
{
try
{
await this.request(context);
}
catch (Exception exception)
{
var httpStatusCode = ConfigurateExceptionTypes(exception);
// set http status code and content type
context.Response.StatusCode = httpStatusCode;
context.Response.ContentType = JsonContentType;
// writes / returns error model to the response
await context.Response.WriteAsync(
JsonConvert.SerializeObject(new ErrorModelViewModel
{
Message = exception.Message
}));
context.Response.Headers.Clear();
}
}
/// <summary>
/// Configurates/maps exception to the proper HTTP error Type
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns></returns>
private static int ConfigurateExceptionTypes(Exception exception)
{
int httpStatusCode;
// Exception type To Http Status configuration
switch (exception)
{
case var _ when exception is ValidationException:
httpStatusCode = (int) HttpStatusCode.BadRequest;
break;
default:
httpStatusCode = (int) HttpStatusCode.InternalServerError;
break;
}
return httpStatusCode;
}
}
}
Conclusion
In this article I covered several option to create maintainable validation architecture. The main goal of this article is to clean up business, presentation and data access logic. I would not recommend considering these approaches as “Silver bullets” as along with advantages they have several disadvantages.
For example:
- Middleware — overrides existing response flow which good option for the API and may be disadvantage for Web solutions. You may need to have 2 middlewares for different solution types.
Source code
All examples can be found implemented in the ready-to-go framework here
Top comments (5)
Isn't it a bad practice to throw validation exceptions? I mean, we would expect to receive some invalid user input data, it's not an exceptional situation.
Hey Marcell. Thank you for you comment. Exceptions allows you to gather all validation errors and intersect it in the meedle-ware. Moreover this concept used in many libraries, including .NET Core, Swagger etc. This is better than constantly repeat if-else (Crossing DRY principle) And why validation error can't be an exception ? And what is exception then ? ;)
Hi!
Validation exceptions are what Microsoft does not recommend to do.
docs.microsoft.com/en-us/visualstu...
Hey,
Well, this document is outdated (since 11/04/2016) moreover it is for .NET Framework. In the Dot Net Core things work slightly different ;)
Here is some example: docs.microsoft.com/en-us/aspnet/co...
Below on that page "Validation failure error response",
docs.microsoft.com/en-us/aspnet/co...
In addition "Model validation in ASP.NET Core MVC and Razor Pages",
docs.microsoft.com/en-us/aspnet/co...
"ASP.NET Core Performance Best Practices: Minimize exceptions"
docs.microsoft.com/ru-ru/aspnet/co...