Introduction
Last month, I released AutoWrapper
version 1.x and it’s incredible to see that it has hundreds of downloads now. It’s just so fulfilling to see such progress in just a month! I’m very glad that it somehow benefited many developers, so thank you all for the support and feedback. I truly appreciate them.
In my previous post, I have covered what AutoWrapper is and demonstrated how it can be used to beautify your ASP.NET Core API HTTP responses with consistent and meaningful information. If you haven’t gone through it, I would recommend you to check out my previous post first about: AutoWrapper: Prettify Your ASP.NET Core APIs with Meaningful Responses
Yesterday, AutoWrapper
version 2.0.1 was released with a few new features added based on community feedback.
What is AutoWrapper
Just to give you a quick recap, AutoWrapper
is a simple, yet customizable global exception handler and response wrapper for ASP.NET Core APIs. It uses an ASP.NET Core middleware
to intercept 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 code requirements and let the wrapper automatically handle the HTTP
response. This can speed up the development time when building your APIs while enforcing your own standards for your HTTP
responses.
Installation
- Download and install the latest
AutoWrapper.Core
from NuGet or via CLI:
PM> Install-Package AutoWrapper.Core -Version 2.0.1
- Declare the following namespace within Startup.cs:
using AutoWrapper;
- Register the
middleware
below within the Configure()
method of Startup.cs "before" the UseRouting()
middleware
:
app.UseApiResponseAndExceptionWrapper();
Simple as that!
Version 1.x
The previous versions of AutoWrapper
already provide the core features in it, and had a few properties that you can set to control how you would like the wrapper to produce an output. However, it doesn’t allow you to customize the response object itself. With similar feedback and requests that I got from developers, I decided to release a new version of AutoWrapper
to address most of them.
What’s New in Version 2?
The latest version of AutoWrapper
provides a better flexibility to use it based on your needs. Here are the newly features added:
- Enable property name mappings for the default
ApiResponse
properties - Added support to implement your own user-defined
Response
and Error
schema / object - Added
IgnoreNullValue
and UseCamelCaseNamingStrategy
options. Both properties are set to true
by default - Enable backward compatibility support for
netcoreapp2.1
and netcoreapp.2.2
.NET Core frameworks. - Exclude properties with
Null
values from the response output
Enable Property Mappings
This feature is the most requested of them all. By default, AutoWrapper
will spit out the following format on successful requests:
{
"message": "Request successful.",
"isError": false,
"result": [
{
"id": 7002,
"firstName": "Vianne",
"lastName": "Durano",
"dateOfBirth": "2018-11-01T00:00:00"
}
]
}
If you don’t like how the default properties are named, then you can now map whatever names you want for the property using the AutoWrapperPropertyMap
attribute. For example, let's say you want to change the name of the result
property to something else like data
, then you can simply define your own schema for mapping it like in the following:
public class MapResponseObject
{
[AutoWrapperPropertyMap(Prop.Result)]
public object Data { get; set; }
}
You can then pass the MapResponseObject
class to the AutoWrapper
middleware
like this:
app.UseApiResponseAndExceptionWrapper<MapResponseObject>();
On successful requests, your response should now look something like this after mapping:
{
"message": "Request successful.",
"isError": false,
"data": {
"id": 7002,
"firstName": "Vianne",
"lastName": "Durano",
"dateOfBirth": "2018-11-01T00:00:00"
}
}
Notice that the default result
attribute is now replaced with the data
attribute.
By default, AutoWrapper
will spit out the following response format when an exception has occurred:
{
"isError": true,
"responseException": {
"exceptionMessage": "Unhandled Exception occurred. Unable to process the request."
}
}
And if you set IsDebug
property in the AutoWrapperOptions
, it will result in something like this with stacktrace information:
{
"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 …"
}
}
If you want to change some of the names of the default ApiError
attributes to something else, you can simply add the following mapping in the MapResponseObject
:
public class MapResponseObject
{
[AutoWrapperPropertyMap(Prop.ResponseException)]
public object Error { get; set; }
[AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)]
public string Message { get; set; }
[AutoWrapperPropertyMap(Prop.ResponseException_Details)]
public string StackTrace { get; set; }
}
To test the output, you can write the following code to simulate an error:
int num = Convert.ToInt32("10s");
The output should now look something like this after the mapping:
{
"isError": true,
"error": {
"message": " Input string was not in a correct format.",
"stackTrace": " at System.Number.ThrowOverflowOrFormatException
(ParsingStatus status, TypeCode type)\r\n
at System.Number.ParseInt32(ReadOnlySpan`1 value,
NumberStyles styles, NumberFormatInfo info)\r\n …"
}
}
Notice that the default attributes for ApiError
model are now changed based on the properties defined in the MapResponseObject
class.
Keep in mind that you are free to choose whatever property you want to map. Here is the list of default properties that you can map:
[AutoWrapperPropertyMap(Prop.Version)]
[AutoWrapperPropertyMap(Prop.StatusCode)]
[AutoWrapperPropertyMap(Prop.Message)]
[AutoWrapperPropertyMap(Prop.IsError)]
[AutoWrapperPropertyMap(Prop.Result)]
[AutoWrapperPropertyMap(Prop.ResponseException)]
[AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)]
[AutoWrapperPropertyMap(Prop.ResponseException_Details)]
[AutoWrapperPropertyMap(Prop.ResponseException_ReferenceErrorCode)]
[AutoWrapperPropertyMap(Prop.ResponseException_ReferenceDocumentLink)]
[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors)]
[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Field)]
[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Message)]
Using Your Own Error Schema
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:
throw new ApiException("Error blah", 400, "511", "http://blah.com/error/511");
And the default output format is going to look like this:
{
"isError": true,
"responseException": {
"exceptionMessage": "Error blah",
"referenceErrorCode": "511",
"referenceDocumentLink": "http://blah.com/error/511"
}
}
If you don’t like how the default error format was structured, you can now define your own Error
object and pass it to the ApiException()
method. For example, if you have the following Error
model with mapping configured:
public class MapResponseObject
{
[AutoWrapperPropertyMap(Prop.ResponseException)]
public object Error { get; set; }
}
public class Error
{
public string Message { get; set; }
public string Code { get; set; }
public InnerError InnerError { get; set; }
public Error(string message, string code, InnerError inner)
{
this.Message = message;
this.Code = code;
this.InnerError = inner;
}
}
public class InnerError
{
public string RequestId { get; set; }
public string Date { get; set; }
public InnerError(string reqId, string reqDate)
{
this.RequestId = reqId;
this.Date = reqDate;
}
}
You can then throw an error like this:
throw new ApiException(
new Error("An error blah.", "InvalidRange",
new InnerError("12345678", DateTime.Now.ToShortDateString())
));
The format of the output will now look like this:
{
"isError": true,
"error": {
"message": "An error blah.",
"code": "InvalidRange",
"innerError": {
"requestId": "12345678",
"date": "10/16/2019"
}
}
}
Using Your Own API Response Schema
If mapping won't work for you and you need to add additional attributes to the default API
response schema, then you can now use your own custom schema/model to achieve that by setting the UseCustomSchema
to true
in AutoWrapperOptions
as shown in the following code below:
app.UseApiResponseAndExceptionWrapper(
new AutoWrapperOptions {
UseCustomSchema = true
});
Now let's say for example you wanted to have an attribute SentDate
and Pagination
object as part of your main API
response, you might want to define your API
response schema to something like this:
public class MyCustomApiResponse
{
public int Code { get; set; }
public string Message { get; set; }
public object Payload { get; set; }
public DateTime SentDate { get; set; }
public Pagination Pagination { get; set; }
public MyCustomApiResponse(DateTime sentDate, object payload = null,
string message = "", int statusCode = 200, Pagination pagination = null)
{
this.Code = statusCode;
this.Message = message == string.Empty ? "Success" : message;
this.Payload = payload;
this.SentDate = sentDate;
this.Pagination = pagination;
}
public MyCustomApiResponse(DateTime sentDate, object payload = null,
Pagination pagination = null)
{
this.Code = 200;
this.Message = "Success";
this.Payload = payload;
this.SentDate = sentDate;
this.Pagination = pagination;
}
public MyCustomApiResponse(object payload)
{
this.Code = 200;
this.Payload = payload;
}
}
public class Pagination
{
public int TotalItemsCount { get; set; }
public int PageSize { get; set; }
public int CurrentPage { get; set; }
public int TotalPages { get; set; }
}
To test the result, you can create a GET
method to something like this:
public async Task<MyCustomApiResponse> Get()
{
var data = await _personManager.GetAllAsync();
return new MyCustomApiResponse(DateTime.UtcNow, data,
new Pagination
{
CurrentPage = 1,
PageSize = 10,
TotalItemsCount = 200,
TotalPages = 20
});
}
Running the code should give you now the following response format:
{
"code": 200,
"message": "Success",
"payload": [
{
"id": 1,
"firstName": "Vianne",
"lastName": "Durano",
"dateOfBirth": "2018-11-01T00:00:00"
},
{
"id": 2,
"firstName": "Vynn",
"lastName": "Durano",
"dateOfBirth": "2018-11-01T00:00:00"
},
{
"id": 3,
"firstName": "Mitch",
"lastName": "Durano",
"dateOfBirth": "2018-11-01T00:00:00"
}
],
"sentDate": "2019-10-17T02:26:32.5242353Z",
"pagination": {
"totalItemsCount": 200,
"pageSize": 10,
"currentPage": 1,
"totalPages": 20
}
}
That’s it. One thing to note here is that once you use your own schema for your API
response, you have the full ability to control how you would want to format your data, but at the same time losing some of the option configurations for the default API
Response. The good thing is you can still take advantage of the ApiException()
method to throw a user-defined error message. For example, you can define your PUT
method like this:
[Route("{id:long}")]
[HttpPut]
public async Task<MyCustomApiResponse> Put(long id, [FromBody] PersonDTO dto)
{
if (ModelState.IsValid)
{
try
{
var person = _mapper.Map<Person>(dto);
person.ID = id;
if (await _personManager.UpdateAsync(person))
return new MyCustomApiResponse(DateTime.UtcNow, true, "Update successful.");
else
throw new ApiException($"Record with id: {id} does not exist.", 400);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, ex, "Error when trying to update with ID:{@ID}", id);
throw;
}
}
else
throw new ApiException(ModelState.AllErrors());
}
Now when a model validation occurs, you will be getting a default response format to something like this:
{
"isError": true,
"responseException": {
"exceptionMessage": "Request responded with validation error(s).
Please correct the specified validation errors and try again.",
"validationErrors": [
{
"field": "FirstName",
"message": "'First Name' must not be empty."
}
]
}
}
If you don’t like how the default error response is structured or named, then you can either pass a mapping object to the AutoWrapper
middleware or implement your own error schema as demonstrated in the previous section above.
Support for NetCoreApp2.1 and NetCoreApp2.2
AutoWrapper
version 2.x also now supports both .NET Core 2.1 and 2.2. You just need to install the Nuget package Newtonsoft.json first before AutoWrapper.Core
.
Summary
In this article, we’ve learned how to integrate and use the new features of AutoWrapper
version 2 in your ASP.NET Core application. The example above was based on ApiBoilerPlate project template.
Please drop your comments and suggestions so I can continue to work on future improvements for this project. You are also free to contribute as this is an open source project. :)
References
History
- 18th October, 2019: Initial post