I’ve recently started porting a REST API to .NET Core using the ASP.NET Core MVC framework, and one of the requirements is to keep the JSON response structure very similar to the one of the old project, that was written using another language and web framework.
The easiest part is returning response data when the request is successful. In this case, we can call the
Ok() method with the response data we want to send back, and the method will generate an
OkResult , taking care of content negotiation of the payload (which means choosing JSON, XML, etc.), serialization and writing of the result string to the response stream.
Now we need to take care of errors. Since returning an HTTP status code with an empty response body is almost always not a good idea with REST APIs, we need to define a new model (class) for errors.
In my case it looks like this (inspired by Boom):
The class has 3 properties, that are:
- the integer HTTP
- the string representation of the status code,
StatusDescription, which in practice looks like
Messagethat explains what actually happened.
You can also derive this class to define more specific predefined error types, like the following ones:
Now you can simply return an
ApiError instance when needed.
Note that instead of using the
Ok() method we now call the method that sets the correct HTTP status code. A long explanation of action results is available here, but I’ll summarize the main methods that are available, with the corresponding status codes:
Ok()for 200 OK;
BadRequest(object error)for 400 Bad Request;
NotFound(object value)for 404 Not Found;
Forbid()for 403 Forbidden;
Unauthorized()for 401 Unauthorized;
StatusCode(int statusCode, object value)for choosing a custom status code.
We’re not done yet. There are (global) errors that aren’t covered by the code above, and this is where the story starts to get interesting.
The first issue I noticed is that when there is no controller/action that can be mapped to the request URL, the framework will return an empty page with a response status code of 404. While that’s okay, we want to achieve consistent error responses.
The solution for this issue is a magic extension method available on the
UseStatusCodePagesWithReExecute. It allows a custom action to return a custom response for an error generated with no body.
It’s great that we can look at the source code of the framework, because it allows us to better understand what’s going on. Specifically, in this case we want to look at this file, where we can find that
UseStatusCodePagesWithReExecute actually uses the simpler
UseStatusCodePages , which in turn invokes a custom middleware that explicitly checks for the status code to be an error (400–500) and for the body to be empty.
So let’s modify our
Startup class and register the middleware that re-executes the request in case of errors with no body, using the
UseStatusCodePagesWithReExecute extension method.
Let’s now write a controller that will be responsible for generating the custom error response:
The controller has an action that takes the status code as the input, and generates a custom
ApiError instance, which is then returned as the payload of the response.
Note that in this case we call the method
ObjectResult() instead of the specific
NotFound(), etc., because we don’t need to set the status code. It is in fact already set when we reach the action.
The exception handling middleware will automatically log the exception through
ILogger, and then pass the control to the specified path, which maps to a controller action.
So let’s register the middleware in the
Startup class, and boom, we’re done!
The nice thing about the two extension methods is that it allows us to use a standard action for generating the response. In this way, we don’t have to worry about serialization and content negotiation, as we can just return an instance of the
ApiError class and the framework will take care of everything.
If you have more specific requirements you can of course write your own custom middleware. For example, it’s not difficult to understand the exception handling middleware, so if you don’t like something about it you can copy and modify it as you prefer.
That’s it for now, I hope it was helpful! Let me know if you have suggestions, need help or just want to say hello :)