ASP.NET REST - Input Data Validation

Development
  • Marcin Golonka
  • 07-06-2022

One of the fundamental principles of a good REST API is input data validation and clear communication of validation errors to the consuming service. In ASP, we have dedicated attributes and the ProblemDetails class that complies with RFC 7807.

RFC 7807 Standard

Almost always in an API, we return errors that we can signal through HTTP status codes. However, often this information is only a basic error indicator, not allowing us to determine the exact cause. Therefore, in March 2016, a standard was defined to unify error responses in API interfaces.

A definition compliant with RFC 7807 should look like this, for example:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-52f8eaa452b5b1ab506c5abb29e7e6e7-5aa2a02d9a445576-00",
  "errors": {
    "Title": [
      "The Title field is required.",
      "The field Title must be a string with a minimum length of 5 and a maximum length of 15."
    ]
  }
}

The most interesting section for us is the Errors array. The Title property should be a short message that is human-readable according to the specification. The standard also allows for error title localization.

Of course, just returning the appropriate JSON doesn’t solve the problem; we still need to remember to return the correct HTTP response code. In the example above, 400 - BadRequest is represented by the Status property.

Validation Attributes in ASP

Developers from Redmond, starting with .NET Core 2.1, by default return a class implementing the above standard for API projects when using validation attributes. Therefore, for most cases, it is sufficient to use dedicated validation attributes in the model class accepted in the API method body.

For Example

Model Class

public class TodoItem  
{  
 [Required]  
 [StringLength(15, MinimumLength = 5)]  
  public string Title { get; set; }  
 [Required] 
 public DateTime ValidTo { get; set; }  
 [MinLength(5)]
 public string SubTitle { get; set; }  
 [EmailAddress]
 public string NotifyEmail { get; set; }  
}

Controller

//...
public class TodoController : ControllerBase  
{  
  //... 
  [HttpPost]  
  public void Post([FromBody] TodoItem value)  
  { }  
 //...
}

After calling the above method with incorrect input data, we will receive an error:

Request

{
 "title": "Test",
 "validTo": "2022-06-07T18:45:49.742Z",
 "subTitle": "string",
 "notifyEmail": "userexample"
}

Response

{
 "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
 "title": "One or more validation errors occurred.",
 "status": 400,
 "traceId": "00-a70409b3919da21ece51e7c524db7684-80db0ffd6d864f8e-00",
 "errors": {
   "NotifyEmail": [
     "The NotifyEmail field is not a valid e-mail address."
   ]
 }
}

Built-in Attributes

Examples of attributes available in the System.ComponentModel.DataAnnotations namespace System.ComponentModel.DataAnnotations:

  • [Required] - Required field
  • [EmailAddress] - Email field
  • [StringLength] - Character length constraint
  • [RegularExpression] - Regular expression

Custom Validation Attribute

Not always built-in attributes cover our cases, so the possibility of implementing custom attributes comes in handy. For validation attributes, we need to implement the ValidationAttribute abstract class.

Example

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]  
public class ContainsA: ValidationAttribute  
{  
	  internal static string ErrorMessageMockup =>  "String don't contain A letter";  
	  
	  public ContainsA() : base(() => ErrorMessageMockup)   { }
	  
	  public override bool IsValid(object? value)  
	  {  
		 if (!(value is string) || value == null)  
			  return false;  
	  
		  return ((string)value).Contains("A");  
	  }
 }

This attribute checks if strings contain at least one letter “A.”

Validation in the Controller

We are not always able to validate input data with attributes, such as when checking with an external service or database. In such cases, we can return an instance of the ProblemDetails object directly in the controller. However, in this case, remember that the responsibility for returning the correct HTTP error code and completing the OPEN API documentation lies with us.

Example

// POST: api/Todo  
[HttpPost]  
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ValidationProblemDetails))]  
public ActionResult Post([FromBody] TodoItem value)  
{  
	  if (!value.Title.Contains("A"))  {
	     ModelState.AddModelError("Title", "String don't contain A letter");  
	     return ValidationProblem();  
	  }  
	  return Ok();  
}

Summary

As seen above, error handling in ASP has been simplified to a minimum. In most cases, developers only need to remember to add the appropriate attributes to input classes. Often, when inheriting model classes from database models with Entity Framework, most attributes are already added during the database design stage.

Things start to get complicated when specific validation is expected, but even for such cases, appropriate abstract classes have been prepared for implementing custom logic.