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
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • It’s Not About Control — It’s About Collaboration Between Architecture and Security
  • Emerging Data Architectures: The Future of Data Management
  • Beyond Microservices: The Emerging Post-Monolith Architecture for 2025

Trending

  • How to Submit a Post to DZone
  • DZone's Article Submission Guidelines
  • Top Book Picks for Site Reliability Engineers
  • Event-Driven Architectures: Designing Scalable and Resilient Cloud Solutions
  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 (7)
Comment
Save
Tweet
Share
29.6K 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

  • Medallion Architecture: Why You Need It and How To Implement It With ClickHouse
  • It’s Not About Control — It’s About Collaboration Between Architecture and Security
  • Emerging Data Architectures: The Future of Data Management
  • Beyond Microservices: The Emerging Post-Monolith Architecture for 2025

Partner Resources

×

Comments
Oops! Something Went Wrong

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

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

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 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!