Introduction
Quote:
UPDATE:
- 10/17/2019: AutoWrapper version
2.0.1
- added new features. - 10/06/2019: AutoWrapper version
1.2.0
- refactor, cleanup and bugfixes for SPA support. - 10/04/2019: AutoWrapper version
1.1.0
with newly added options. - 09/23/2019: AutoWrapper version
1.0.0
is now officially released with some added option configurations to it.
When building APIs for “real” application projects, most developers forgot about the importance of providing meaningful response to their consumers. There are a few reasons why this can happen; could be that their development time is limited, they don't have standard format for HTTP
response or simply they just don't care about the response for as long as the API
will return the needed data to the consumer. Well, API’s is not just about passing JSON
back and forth over HTTP
, but it’s also how you present meaningful responses to the developers who consumes it.
As someone once told…
“A good API design is a UX for developers who consume it.”
As an API
developer who value consumers, we want to give meaningful and consistent API
responses to them.
ASP.NET Core gives us the ability to create REST APIs in just a snap; however, they do not provide a consistent response for successful requests and errors out of the box. If you are taking a RESTful approach to your API’s, then you will be utilizing HTTP
verbs such as GET
, POST
, PUT
and DELETE
. Each of this action may return different types depending on how your endpoint is designed. Your POST
, PUT
and DELETE
endpoints may return a data or not at all. Your GET
endpoint may return a string
, a List<T>
, an IEnumerable
of some type or even an object
. On the other hand, if your API
throws an error, it will return an object
or worst an HTML
string stating the cause of the error. The differences among all of these responses make it difficult to consume the API
, because the consumer needs to know the type and structure of the data that is being returned in each case. Both the client code and the service code become difficult to manage.
Last year, I created a couple of Nuget packages for managing exceptions and response consistency using a custom object
wrapper for restful APIs.
I was amazed that both packages had hundreds of downloads now, and was used by other open-source project such as the Blazor Boilerplate.
While I consider both packages a success, still there are a few glitches with them and so I have decided to create a new package to refactor the code base, apply bug fixes and add new features to it.
In this post, we will take a look at how we can beautify our ASP.NET Core API
responses using AutoWrapper
.
The Default ASP.NET Core API Response
When you create a new ASP.NET Core API template, Visual Studio will scaffold all the necessary files and dependencies to help you get started building RESTful APIs. The generated template includes a “WeatherForecastController
” to simulate a simple GET
request using a static data as shown in the code below:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
When you run the application, you will be presented with the following output in JSON
format:
[
{
"date": "2019-09-16T13:08:39.5994786-05:00",
"temperatureC": -8,
"temperatureF": 18,
"summary": "Bracing"
},
{
"date": "2019-09-17T13:08:39.5995153-05:00",
"temperatureC": 54,
"temperatureF": 129,
"summary": "Cool"
},
{
"date": "2019-09-18T13:08:39.5995162-05:00",
"temperatureC": 33,
"temperatureF": 91,
"summary": "Bracing"
},
{
"date": "2019-09-19T13:08:39.5995166-05:00",
"temperatureC": 38,
"temperatureF": 100,
"summary": "Balmy"
},
{
"date": "2019-09-20T13:08:39.599517-05:00",
"temperatureC": -3,
"temperatureF": 27,
"summary": "Sweltering"
}
]
That’s great and the API
works just like what we expect, except that it doesn’t give us a meaningful response. We understand that the data is the very important part of the response however, spitting out just the data as the JSON
response isn’t really that helpful at all, especially when there’s an unexpected behavior that happens between each request.
For example, the following code simulates an unexpected error that could happen in your code:
int num = Convert.ToInt32("a");
The code above tries to convert a string
that contains non-numeric values into an integer
type, which will cause an error at runtime. The response output is going to look like something this:
Figure 1: Unhandled Exception
Yuck!
Can you imagine the disappointment of the developers who will consume your API seeing this format from the response? Well at least the stack trace information is somewhat useful because it gives you the idea about the cause of the error, but that should be handled and never let your API consumers see that information for security risks. Stack trace information are definitely helpful during the development stage and debugging. In production, detailed errors like this should be handled, log them somewhere for analysis and return a meaningful response back to the consumer.
Another scenario is when you are trying to access an API
endpoint that doesn’t exist gives you nothing in return other than the famous 404 (Not Found)
Http Status Code.
When working with REST APIs, it is important to handle exceptions and return consistent responses for all the requests that are processed by your API regardless of success or failure. This makes it a lot easier to consume the API, without requiring complex code on the client.
AutoWrapper.Core to the Rescue
AutoWrapper takes care of the incoming HTTP
requests and automatically wraps the responses for you by providing a consistent response format for both successful and error results. The goal is to let you focus on your business specific requirements and let the wrapper handles the HTTP
response. Imagine the time you save from developing your APIs while enforcing standards for your HTTP
response.
AutoWrapper
is a project fork based from VMD.RESTApiResponseWrapper.Corewhich is designed to support .NET Core 3.x and above. The implementation of this package was refactored to provide a more convenient way to use the middleware with added flexibility.
Main Features:
- Exception handling
- ModelState validation error handling (support both Data Annotation and FluentValidation)
- A configurable API exception
- A consistent response format for Result and Errors
- A detailed Result response
- A detailed Error response
- A configurable HTTP StatusCodes and messages
- Add support for Swagger
- Add Logging support for Request, Response and Exceptions
- Add options in the middleware to set ApiVersion and IsDebug properties
TL, DR. Show Me the Code
With just a few steps, you can turn your API Controller
to return something meaningful response without doing much development effort on your part. All you have to do is:
1. Download and Install the latest AutoWrapper.Core
from NuGet or via CLI:
PM> Install-Package AutoWrapper.Core -Version 1.0.1-rc
Quote:
Note: This is a prerelease version at the moment and will be released officially once .NET Core 3 is out.
2. Declare the following namespace within Startup.cs
using AutoWrapper;
3. Register the middleware below within the Configure()
method of Startup.cs
"before" the UseRouting()
middleware:
app.UseApiResponseAndExceptionWrapper();
The default API version format is set to "1.0.0.0
". If you wish to specify a different version format for your API, then you can do:
app.UseApiResponseAndExceptionWrapper(new ApiResponseOptions { ApiVersion = "2.0" });
That's simple! Now try to build and run your ASP.NET Core API default application again. Based on our example, here’s how the response is going to look like for the “WeatherForecastController
” API:
{
"message": "Request successful.",
"isError": false,
"result": [
{
"date": "2019-09-16T23:37:51.5544349-05:00",
"temperatureC": 21,
"temperatureF": 69,
"summary": "Mild"
},
{
"date": "2019-09-17T23:37:51.554466-05:00",
"temperatureC": 28,
"temperatureF": 82,
"summary": "Cool"
},
{
"date": "2019-09-18T23:37:51.554467-05:00",
"temperatureC": 21,
"temperatureF": 69,
"summary": "Sweltering"
},
{
"date": "2019-09-19T23:37:51.5544676-05:00",
"temperatureC": 53,
"temperatureF": 127,
"summary": "Chilly"
},
{
"date": "2019-09-20T23:37:51.5544681-05:00",
"temperatureC": 22,
"temperatureF": 71,
"summary": "Bracing"
}
]
}
Sweet!
If you noticed, the output now contains a few properties in the response such as the , message
, isError
and the actual data being contained in the result
property.
Another good thing about AutoWrapper
is that logging is already preconfigured. .NET Core apps has built-in logging mechanism by default and any requests and responses that has been intercepted by the wrapper will be automatically logged. For this example, it will show something like this in your Visual Studio console window:
Figure 2: Visual Studio Console logs
.NET Core supports a logging API that works with a variety of built-in and third-party logging providers. Depending on what supported .NET Core logging provider you use and how you configure the location to log the data (e.g text file, Cloud , etc. ), AutoWrapper will automatically write the logs there for you.
Here’s another example of an output when you try to point to a URL that doesn’t exist:
{
"isError": true,
"responseException": {
"exceptionMessage": "Request not found. The specified uri does not exist.",
"details": null,
"referenceErrorCode": null,
"referenceDocumentLink": null,
"validationErrors": null
}
}
Now noticed how the response object was changed. The statusCode
was automatically set to 404
. The result
property was automatically omitted when any unexpected error or exception has occurred, and display the responseException
property instead for showing the error messages and extra information.
Keep in mind that any errors or exceptions will also be logged. For example, if we run the following code again:
int num = Convert.ToInt32("a");
It will now give you the following response:
{
"isError": true,
"responseException": {
"exceptionMessage": "Unhandled Exception occured. Unable to process the request.",
"details": null,
"referenceErrorCode": null,
"referenceDocumentLink": null,
"validationErrors": null
}
}
And the console window will display something like this:
Figure 3: Visual Studio Console logs
By default, AutoWrapper
suppresses stack trace information. If you want to see the actual details of the error from the response during the development stage, then simply set the AutoWrapperOptions
IsDebug
to true
:
app.UseApiResponseAndExceptionWrapper( new AutoWrapperOptions { IsDebug = true });
Now when you run the application again to trigger an exception, it will now show something like this:
{
"isError": true,
"responseException": {
"exceptionMessage": " Input string was not in a correct format.",
"details": " at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)\r\n at System.Convert.ToInt32(String value)\r\n at AutoWrapperDemo.Controllers.WeatherForecastController.Get() in . . . . . . .,
"referenceErrorCode": null,
"referenceDocumentLink": null,
"validationErrors": null
}
}
Noticed that the real exception message and its details are now shown.
Defining Your Own Custom Message
To display a custom message in your response, use the ApiResponse
object from AutoWrapper.Wrappers
namespace. For example, if you want to display a message when a successful POST
has been made, then you can do something like this:
[HttpPost]
public async Task<ApiResponse> Post([FromBody]CreateBandDTO band)
{
try
{
var result = await SampleData.AddNew(band);
return new ApiResponse("New record has been created to the database", result, 201);
}
catch (Exception ex)
{
throw;
}
}
Running the code will give you the following result when successful:
{
"message": "New record has been created to the database",
"isError": false,
"result": 100
}
The ApiResponse
object has the following parameters that you can set:
ApiResponse(string message, object result = null, int statusCode = 200, string apiVersion = "1.0.0.0")
Defining Your Own Api Exception
AutoWrapper
also provides an ApiException
object that you can use to define your own exception. For example, if you want to throw your own exception message, you could simply do:
For capturing ModelState
validation errors
throw new ApiException(ModelState.AllErrors());
For throwing your own exception message
throw new ApiException($"Record with id: {id} does not exist.", 400);
For example, let’s modify the POST method with ModelState validation:
[HttpPost]
public async Task<ApiResponse> Post([FromBody]CreateBandDTO band)
{
if (ModelState.IsValid)
{
try
{
var result = await SampleData.AddNew(band);
return new ApiResponse("New record has been created to the database", result, 201);
}
catch (Exception ex)
{
throw;
}
}
else
throw new ApiException(ModelState.AllErrors());
}
Running the code will result to something like this when validation fails:
{
"isError": true,
"responseException": {
"exceptionMessage": "Request responded with validation error(s). Please correct the specified validation errors and try again.",
"details": null,
"referenceErrorCode": null,
"referenceDocumentLink": null,
"validationErrors": [
{
"field": "Name",
"message": "The Name field is required."
}
]
}
}
See how the validationErrors
property is automatically populated with the violated fields from your model.
The ApiException
object contains the following three overload constructors
that you can use to define an exception:
ApiException(string message, int statusCode = 500, string errorCode = "", string refLink = "")
ApiException(IEnumerable<ValidationError> errors, int statusCode = 400)
ApiException(System.Exception ex, int statusCode = 500)
Options
The following properties are the options that you can set:
Version 1.0.0
ApiVersion
ShowApiVersion
ShowStatusCode
IsDebug
Version 1.1.0 Additions
IsApiOnly
WrapWhenApiPathStartsWith
ShowApiVersion
if you want to show the API
version in the response, then you can do:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowApiVersion = true });
The default API
version format is set to "1.0.0.0
"
ApiVersion
If you wish to specify a different version format, then you can do:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions {
ShowApiVersion = true,
ApiVersion = "2.0"
});
ShowStatusCode
if you want to show the StatusCode
in the response, then you can do:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowStatusCode = true });
IsDebug
By default, AutoWrapper
suppresses stack trace information. If you want to see the actual details of the error from the response during the development stage, then simply set the AutoWrapperOptions
IsDebug
to true
:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsDebug = true });
IsApiOnly
AutoWrapper
is meant to be used for ASP.NET Core API project templates only. If you are combining API Controllers
within your front-end projects like Angular, MVC, React, Blazor and other SPA frameworks that supports .NET Core, then use this property to enable it.
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsApiOnly = false} );
WrapWhenApiPathStartsWith
If you set the IsApiOnly
option to false
, you can also specify the segment of your API
path for validation. By default it was set to "/api"
. If you want to set it to something else, then you can do:
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions {
IsApiOnly = false,
WrapWhenApiPathStartsWith = "/myapi"
});
This will activate the AutoWrapper
to intercept HTTP
responses when a request path contains the WrapWhenApiPathStartsWith
value.
Quote:
Note that I would still recommend you to implement your API Controllers
in a seperate project to value the separation of concerns and to avoid mixing route configurations for your SPAs
and APIs.
Support for Logging
Another good thing about AutoWrapper
is that logging is already pre-configured. .NET Core apps has built-in logging mechanism by default, and any requests and responses that has been intercepted by the wrapper will be automatically logged (thanks to Dependency Injecton!). .NET Core supports a logging API
that works with a variety of built-in and third-party logging providers. Depending on what supported .NET Core logging provider you use and how you configure the location to log the data (e.g text file, Cloud , etc. ), AutoWrapper
will automatically write the logs there for you.
Support for Swagger
Swagger provides an advance documentation for your APIs where it allows developers to reference the details of your API
endpoints and test them when necessary. This is very helpful especially when your API
is public and you expect many developers to use it.
AutoWrapper
omit any request with “/swagger
” in the URL so you can still be able to navigate to the Swagger UI for your API documentation.
Summary
In this article, we’ve learned how to integrate and use the core features of AutoWrapper
in your ASP.NET Core application.
I’m pretty sure there are still lots of things to improve in this project, so feel free to try it out and let me know your thoughts. Comments and suggestions are welcome, please drop a message and I’d be happy to answer any queries as I can.
Check out new features of version 2 here: ASP.NET Core with AutoWrapper: Customizing the Default Response Output
GitHub Repo
https://github.com/proudmonkey/AutoWrapper
References
Release History
- 10/17/2019: AutoWrapper version
2.0.1
- added new features. - 10/06/2019: AutoWrapper version
1.2.0
- refactor, cleanup and bugfixes for SPA support. - 10/04/2019: AutoWrapper version
1.1.0
with newly added options. - 09/23/2019: AutoWrapper version
1.0.0
offcial release. - 09/14/2019: AutoWrapper version
1.0.0-rc
prerelease.