What are error handling and validation architecture in .NET Core?

Introduction

In many projects, error handling and validation are distributed across business logic, APIApplication Programming Interface controllers, and data access layers in the form of conditions (“if-else” sequences).

This leads to the violation of the Separation of Concerns Principle and results in “spaghetti code,” as shown 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));
}

Architecture overview

For this shot, we will use N-tire architectureclient-server architecture. However, the following approaches can be reused in CQRSCommand and Query Responsibility Segregation, Event-Driven, Micro Services, and SOAService-Oriented Architectures architectures.

Examples

Example architecture includes the following layers:

  • Presentation Layer — UI / API
  • Business Logic Layer — Services or Domain Services (in case you have DDD architecture)
  • Data Layer / Data Access Layer

Diagram of architecture

The diagram below shows the components and modules that belong to different layers and contain the presentation/API layer, business logic layer, and data access on the right side, and related validation and error handling logic on the left side:

Validation and error handling architecture contains several components that we will go over in the next few sections.

API validation level

API controllers may contain a lot of validation, such as parameters check or model state check, like in the example below.

We will use declarative programming to move validation logic out from the 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 easily be cleaned by creating validation model attributes. The example below contains a 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 the attribute below to startup.cs:

services.AddMvc(options => {
options.Filters.Add(typeof(ValidateModelStateAttribute));
});

How to validate parameters of API

To validate parameters of API action methods, we will create an attribute and move validation logic.

  • Logic inside the attribute checks if parameters contain validation attributes and validate the value.

  • Now, the attribute can be added to the action method if necessary.

[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 of 2 components: validation service and validation rules.

In the device validation services, we move all custom validation and rule-based validation logic from the service (device service in the example below).

This idea is similar to using the Guard pattern. An example of the validation service is given below:

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, we move 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 validation service.

The Validation rules are based on the FluentValidation framework, which allows you to 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

Let’s go over errors/exception handling. All validation components generate exceptions, and the centralized component that handles them and provides proper JSON error object is required.

In the example below, we use .NET core Middleware to catch all exceptions and create HTTPHyperText Transfer Protocol error status according to Exception Type (in the ConfigurationExceptionType method) and build error JSON object.

Middleware can also be used to log all of the exceptions 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 shot, we covered several options to create maintainable validation architecture.

The main goal of this shot is to clean up business, presentation, and data access logic.

It’s important to note that these approaches have several disadvantages as well.

For example:

  • Middleware — overrides existing response flow, which is a good option for the API and may be a disadvantage for web solutions. You may need to have 2 middlewares for different solution types.
Attributions:
  1. undefined by undefined
  2. undefined by undefined