Reporting Model Validation Errors in ASP.NET Core 2.0

Couple of days ago, we had an outage in one of our production APIs that’s built with ASP.NET Core 2.0 and all we got to see in all our logging was something similar to this which doesn’t really help reveal the root cause:

500 Internal Server Error caused by using a null Employee object

After much debugging hassle for several hours we found that it was caused by a type mismatch between the DTO on the server (which expected an int) and client sent JSON payload to the POST endpoint (which sent a decimal), in one of the fields. This resulted in the following exception:

Newtonsoft.Json.JsonReaderException: Input string ‘20.4’ is not a valid integer. Path ‘Age’, line 3, position 12.

…actually, it will be nice if you could tell me what are you?!

It turns out that ASP.NET Core 2.0 by default doesn’t report back on model validation errors but continues processing the invalid request resulting in the 500 ISE error.

It also turns out that ASP.NET Core 2.2 has already solved this problem by sending back a 400 Bad Request when model validation fails thereby short circuiting the rest of the request and reporting back the real issue:

Rather nice addition out of the box in ASP.NET Core 2.2

The obvious solution therefore might seem like upgrading to ASP.NET Core 2.2 but sometimes due to organisational constraints its not as easy. So let’s assume that is not on the table for now.

Then how do I add similar model validation error reporting to the 2.0 solution? First, lets look at how 2.2 does it.

To deal with model validation error reporting, in ASP.NET Core 2.2, 2 new types were added:

  1. ProblemDetails
  2. ValidationProblemDetails which inherits from ProblemDetails and deals specifically with model validation errors. 

During a model validation failure event, an instance of ValidationProblemDetails is filled in and sent out with Content-Type header set to application/problem+json. This header value is an RFC standard for HTTP APIs that want to send out human/machine readable responses when request processing fails, giving plenty of context and information for easier diagnostics.

Back in my ASP.NET Core 2.0 API, where these types aren’t available, I will create them manually and customise them slightly for my purposes. First up, ProblemDetails:

public class ProblemDetails
{
public ProblemDetails()
{
}
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "type")]
public string Type { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "title")]
public string Title { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "status")]
public int? Status { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "detail")]
public string Detail { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "instance")]
public string Instance { get; set; }
[JsonExtensionData]
public IDictionary<string, object> Extensions { get; }
}

Then the ValidationProblemDetails class:

public class ValidationProblemDetails : ProblemDetails
{
public ValidationProblemDetails() : base()
{
}
public ValidationProblemDetails(
ModelStateDictionary modelStateDictionary)
: base()
{
this.Errors = modelStateDictionary
.ToDictionary(x => x.Key,
y => y.Value.Errors.Select(
z => z.Exception.Details()).ToArray());
}
[JsonProperty(PropertyName = "errors")]
public IDictionary<string, string[]> Errors { get; }
}

In my version of this class, I am returning all the inner exceptions as well as the stack trace and I’ve created an extension method on the Exception class to do so:

public static class ExceptionExtensions
{
public static string Details(this Exception exception)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine(exception.Message);
while (exception.InnerException != null)
{
builder.AppendLine(exception.InnerException.Message);
}
builder.AppendLine(exception.StackTrace);
return builder.ToString();
}
}

Once this is in place, I will create an IActionFilter that can allow me to short circuit the request pipeline if my model state is not valid and send an appropriate response with model validation errors:

public class ModelBindingFailureFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context
.HttpContext
.Response
.ContentType = "application/problem+json";
var validationProblems = new ValidationProblemDetails(
context.ModelState)
{
Title = "One or more validation errors occurred",
Status = (int)HttpStatusCode.BadRequest,
Instance = context.HttpContext.TraceIdentifier
};
context.Result = new BadRequestObjectResult(validationProblems);
}
}
}

Using action filters is a great way to take care of cross cutting concerns like logging, model validation error reporting, setting up of any ambient contexts etc in a central place instead of duplicating this code in each controller action which can lead to divergence in behaviour over time.

Because RFC recommends so, I will also set the Content-Type header to application/problem+json and this way the client knows that this response contains an error report.

NB: I can also log the model errors in the filter for later diagnosis (for this post though I haven’t shown it). The mere act of setting the context.Result will short circuit the rest of the pipeline and will prevent the generic and misleading 500 Internal Server Error. I say misleading because ultimately, its the request that’s not valid and that makes it a 400 Bad Request, the fact that it causes a null object and using that null object subsequently causes a request failure, is a symptom of this bad request and not the cause.

Finally, I will simply add this filter to my MVC pipeline:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(x => x.Filters.Add(new ModelBindingFailureFilter()));
}
view raw Startup.cs hosted with ❤ by GitHub

Now when I make the request again with my dodgy JSON payload, I get this response:

Model validation errors trapped and reported by a custom IActionFilter

Now this is what I would have expected to see in every ASP.NET Core version out of the box, would have saved us several hours of debugging if we’d seen this in our logs! Why it only appeared after 2.0, I have no idea!

I can also create a simpler version of the action filter, one that doesn’t use the ProblemDetails class hierarchy at all but simply queries the model state and sends back the errors. Something like this:

public class ModelBindingFailureFilterSimple : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(
context
.ModelState
.ToDictionary(
x=>x.Key,
y=>y.Value.Errors.Select(
z=>z.Exception.Message).ToList()));
}
}
}

Resulting in a response that looks like this:

Simpler response

If you want a quick and cheap approach then this simpler variant might be just fine but if you want to follow the whole RFC-esque problem details style pattern because it will make the transition to ASP.NET Core 2.2 easier, then go with the first version.

Anyway, the PoC code for this post is up on my GitHub! Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.