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
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • How to Build a Scalable SaaS Product Architecture
  • Architecture Review Boards for Generative AI: A Framework for Governance, Risk Management, and Responsible Adoption
  • A Reference Architecture for Constrained AI-Assisted Design
  • GenAI Security Gateway: An End-to-End Architecture

Trending

  • AI-Driven Integration in Large-Scale Agile Environments
  • What Cloud Engineers Actually Need to Know About AI Infrastructure
  • AI-Era Cybersecurity, Part 1: Mapping Glasswing, Daybreak, Lightwell and the Tools Racing to Find and Fix Bugs
  • The Twelve-Factor Agents: Building Production-Ready LLM Applications

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  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
DZone Core CORE ·
Mar. 26, 19 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
30.5K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

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

  • How to Build a Scalable SaaS Product Architecture
  • Architecture Review Boards for Generative AI: A Framework for Governance, Risk Management, and Responsible Adoption
  • A Reference Architecture for Constrained AI-Assisted Design
  • GenAI Security Gateway: An End-to-End Architecture

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • 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