Expose ASP.NET Core validation for SPAs like React, Vue or Angular

June 01, 2019

tl;dr: This article showcases two ways on how to expose ASP.NET Core built-in validation via HTTP for client applications. The accompanying source code is available at https://github.com/jannikbuschke/pragmatic-cqrs-with-asp-net-core-for-spas.


Pragmatic CQRS with ASP.NET Core and SPAs - Series overview

  1. Getting started with Odata: Expose a queryable api
  2. Enhance ASP.NET Core api controllers with Odata
  3. ️️️️️️️️▶️️️️ Expose ASP.NET Core validation for clients
  4. CQRS with MediatR (Commands) and Odata (Queries)

Motivation

ASP.NET Core has built-in model validation capabilities. In Razor Pages this can even be re-used on JavaScript client side, without duplicating the logic.

In Single Page Applications (SPAs) or in cases where back- and frontend cannot easily share code validation logic is usually duplicated on the client side. This optimizes for performance but increases the risk of bugs/inconsistencies between client and server validation. It also increases maintenance costs, as now these two parts need to stay in sync.

The applications I typically develop are totally fine with some introduced network costs (few users and usually low latency). In these scenarios it can be worth it to reduce maintenance costs and optimize for development speed at the expense of performance (and some network usage). I didn't yet see ~~a lot~~ any info about this on the internet, hence this post. We will look at one or two ways on how to expose ASP.NET Core validation to any http based client without duplicating or sharing code.


Validation with an additional route

In general there are two different techniques we can accomplish above outlined goal: a) create a second route that implements the same validation logic and returns any validation errors or b) extend an existing routes behavior to allow the client to indicate it's intent (validate or execute). Let's start with providing an additional route.

[ApiController]

One simple way is to reuse either the built-in ControllerBase model validation via ModelState or the ApiController attribute. Let's assume the following model and Controller that we will use as a starting point through-out this post:


/// Create Person command DTO ///
public class CreatePerson
{
[Required]
public string Name { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
}
[Route("api/[controller]")]
[ApiController] /** <= ASP.NET Core built-in feature that will validate each incoming request **/
public class PersonController : Controller
{
/** Constructor and fields left out for brevity... **/
[HttpPost("create")]
public async Task<IActionResult> CreatePerson(CreatePerson request)
{
var person = mapper.ToPerson(request);
/**
detailed implementation and explanation left out for brevity...
ctx.Persons.Add(person);
await ctx.SaveChangesAsync();
**/
return Ok(person.Id);
}
}

We have a PersonCreate data transfer object (DTO) and a PersonController with a CreatePerson() method. We already applied the ApiController attribute, so before model binding/deserialization happens the incoming data is validated, and in case of an invalid request a HTTP 400 BAD REQUEST response will be returned to the client without invoking our action method. Currently the client does not have an option to only validate a potential request. All valid requests will be executed.

So lets implement a second action method to allow clients to validate the CreatePerson request without creating any side-effects:


[Route("api/[controller]")]
[ApiController] /** <= every incoming request will be validated **/
public class PersonController : Controller
{
[HttpPost("validate-create")]
public async Task<IActionResult> ValidateCreatePerson(CreatePerson request)
{
return Ok(); // no side-effects
}
[HttpPost("create")]
public async Task<IActionResult> CreatePerson(CreatePerson request)
{
/** ... side-effect **/
return Ok();
}
}

We simply added another route with the same request parameters (and validation) which returns HTTP 200 OK if the request is valid. Now clients can repeatedly validate their requests at /api/Person/validate-create!

Example

Client sends


POST /api/Persons/create/validate
{
"name": "Bob",
"email": "email.com"
}

Api returns


400 BadRequest
{
"errors":{
"email":[ "The email value is not a valid email" ]
}
}

Validator

Another alternative to the ApiController Attribute is to make use of the lower-level Validator class in the System.ComponentModel.DataAnnotations namespace. It allows to validate requests in an imperative way:


[HttpPost("validate-create")]
public async Task<IActionResult> ValidateCreatePerson(CreatePerson request)
{
ValidationContext context = new ValidationContext(request, serviceProvider);
List<ValidationResult> validationResults = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(obj, context, validationResults, true);
if(!isValid) {
return BadRequest(validationResults);
} else {
return Ok();
}
}


Extending an existing route

Instead of providing additional routes/paths, let us investigate how we can extend existing routes to allow clients to specify whether to execute or validate requests. Clients could send additional info via query parameters, headers or payloads to provide their intent. This approach is more sophisticated, as we will reach out into the default ASP.NET Core behavior. It is also a more elegant solution as it will allow us to declaratively add behavior without adding boilerplate action methods.

Custom Action Filter

ASP.NET Core provides two built-in middleware implementations developers can use to change the default behavior of incoming and outgoing requests. There is the main middle-ware which is configured in the Startup.cs Configure(IApplicationBuilder) and there are Filters that run at specific stages during the request pipeline after ASP.NET Core has selected the action to be executed.

We will not use the main middle-ware pipeline. Instead we will implement a custom ActionFilter. These special filters run right before and after an action method is executed.

Let's implement a custom ActionFilter that reads a header with name x-action-intent and value validate or execute. Depending on the intent it will either only validate and short-circuit (end) the current request pipeline or validate and if successful let the request pipeline execute, so that eventually our action method is invoked:


public class ValidatableAttribute : ActionFilterAttribute
{
private const string HeaderName = "x-action-intent";
public override void OnActionExecuting(ActionExecutingContext context)
{
StringValues header = context.HttpContext.Request.Headers[HeaderName];
string intent = header.FirstOrDefault();
if (intent == null)
{
// If there is no header set we will short-circuit the request pipeline by setting
// context.Result with a BadRequest.
context.Result = new BadRequestObjectResult($"Missing header '{HeaderName}'");
return;
}
if (!context.ModelState.IsValid)
{
// If the parameters that were send are not valid we will also
// in all cases short-circuit the pipeline.
context.Result = new BadRequestObjectResult(context.ModelState);
return;
}
switch (intent)
{
case "validate":
{
// If the clients intent is 'validate' we will short-circuit
// the request pipeline and return HTTP OK.
context.Result = new NoContentResult();
break;
}
case "execute":
{
// If the clients intent is 'execute' we will not do anything here.
// Default request pipeline continues and eventually our controller method is invoked.
break;
}
default:
{
context.Result = new BadRequestObjectResult($"Unknown _action parameter value: '{HeaderName}'");
break;
}
}
}
}
[Route("api/[controller]")]
[ApiController]
public class PersonController : Controller
{
[Validatable] /** Will validate and if necessary short-circuit the request **/
[HttpPost("create")]
public async Task<IActionResult> CreatePerson(CreatePerson request)
{
/** .
side-effect ...
**/
return Ok();
}
}
}

Now we can invoke the original /api/create route and indicate if we want to validate or execute (i.e. with Postman):

Example

Client sends


POST /api/Persons/create HTTP/1.1
Content-Type: application/json
x-action-intent: validate
{
"name": "123",
"email": "email@email.de"
}

Api responds with HTTP OK, meaning the request looks valid. Our action method was not invoked.

Sending a valid request with the execute intent as in the following example will not be short-circuited and our action method will be invoked:


POST /api/Persons/create HTTP/1.1
content-type: application/json
x-action-intent: execute
{
"name": "123",
"email": "email@email.de"
}

Summary

This post shows different ways to provide server-validation for http clients. The client can use the path, query-parameters or headers to indicate it's intent. It would also be possible to add data to the payload, however I feel that headers are the "correct"/"best" option.

To see the source code do


git clone https://github.com/jannikbuschke/pragmatic-cqrs-with-asp-net-core-for-spas

And look at example #3.