DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Is the Data Warehouse Dead? 3 Patterns From Enterprise Architecture That Answer This Question
  • Why Your Test Automation Is Always Behind the Code And the Architecture That Fixes It
  • Multi-Scale Feature Learning in CNN and U-Net Architectures
  • How SaaS Architectures Break at Scale — and the Engineering Decisions That Prevent It

Trending

  • Feature Flag Debt: Performance Impact in Enterprise Applications
  • Compliance Automated Standard Solution (COMPASS), Part 10: How OSCAL Mapping Paves the Way for Continuous Compliance Scalability
  • A System Cannot Protect What It Does Not Understand
  • Multi-Scale Feature Learning in CNN and U-Net Architectures
  1. DZone
  2. Software Design and Architecture
  3. Microservices
  4. Error Handling and Validation Architecture in .NET Core

Error Handling and Validation Architecture in .NET Core

We cover ways to make a maintainable validation architecture. The main goal of this post is to clean up business, presentation, and data access logic.

By 
Boris Zaikin user avatar
Boris Zaikin
DZone Core CORE ·
Mar. 26, 19 · Tutorial
Likes (8)
Comment
Save
Tweet
Share
30.2K Views

Join the DZone community and get the full member experience.

Join For Free

In many projects, error handling and validation are distributed across business logic, API 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,” 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 an approach for sepearting validation and error handling logic from the other application layers.

Architecture Overview

For simplicity I use N-tire architecture, however, the approaches can be reused in CQRS, event-driven, microservices, SOA, and other architectures.

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

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

Application architecture

The validation and error handling architecture contain several components which I will describe in the next few sections.

API Validation Level

API controllers may contain a lot of validations, such as parameter checks, model state checks, etc., like in the example below. I will use declarative programming to move the validation logic out frthe om 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 a validation model attribute. 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 this attribute to the startup.cs file.

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

To validate the parameters of the API action methods, I will create an attribute and move the validation logic. The logic inside this attribute checks if the parameters contain validation attributes and validate the value.

Now the attribute can be added to the action method, if necessary (examples below):

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;

namespace DeviceManager.Api.ActionFilters
{
    /// <inheritdoc />
    public class ValidateActionParametersAttribute : ActionFilterAttribute
    {
        private const string RequiredAttributeKey = "RequiredAttribute";

        /// <inheritdoc />
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var descriptor = context.ActionDescriptor as ControllerActionDescriptor;

            if (descriptor != null)
            {
                var parameters = descriptor.MethodInfo.GetParameters();

                CheckParameterRequired(context, parameters);
            }

            base.OnActionExecuting(context);
        }

        private static void CheckParameterRequired(ActionExecutingContext context, IEnumerable<ParameterInfo> parameters)
        {
            foreach (var parameter in parameters)
            {
                if (parameter.CustomAttributes.Any() && parameter.CustomAttributes.Select(item => item.AttributeType
                        .ToString()
                        .Contains(RequiredAttributeKey)).Any())
                {
                    if (!context.ActionArguments.Keys.Contains(parameter.Name))
                    {
                        context.ModelState.AddModelError(parameter.Name, $"Parameter {parameter.Name} is required");
                    }
                }
            }

            if (context.ModelState.ErrorCount != 0)
            {
                context.Result = new BadRequestObjectResult(context.ModelState);
            }
        }
    }
}

Now the attribute can be added to the API method, like in the example 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 of two 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 is quite similar to using the Guard pattern. The below example shows this 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 the device's view model validation rules. The validation itself triggers inside the the validation service.

The validation rules based on the FluentValidation framework allow you to build rules in the 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 thing I will cover is errors/exception handling. I waited to address this topic at the end as all validation components generate exceptions and the centralized component that handles them and provides proper JSON error objects is required.

In the example below I’ve used .NET Core Middleware to catch all exceptions and created an HTTP error status according to Exception Type (in the ConfigurationExceptionType method) and build an error JSON object.

Also, the 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 options to create a 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 some advantages, they have several disadvantages.

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 two middlewares for different solution types.

Source Code

All examples can be found implemented in the ready-to-go framework here.

Architecture

Opinions expressed by DZone contributors are their own.

Related

  • Is the Data Warehouse Dead? 3 Patterns From Enterprise Architecture That Answer This Question
  • Why Your Test Automation Is Always Behind the Code And the Architecture That Fixes It
  • Multi-Scale Feature Learning in CNN and U-Net Architectures
  • How SaaS Architectures Break at Scale — and the Engineering Decisions That Prevent It

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook