ASP.NET Core with AutoWrapper: Customizing the Default Response Output

Highlights the detail about what's new in AutoWrapper Version 2. AutoWrapper is a simple, yet customizable global exception handler and response wrapper for ASP.NET Core APIs.

Introduction

 
Last Month, I have 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 from the 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 own standards for your HTTP responses.
 

Installation

 
(1) Download and Install the latest AutoWrapper.Core from NuGet or via CLI: 
  1. PM> Install-Package AutoWrapper.Core -Version 2.0.1    
(2) Declare the following namespace within Startup.cs
  1. using AutoWrapper;    
(3) Register the middleware below within the Configure() method of Startup.cs "before" the UseRouting() middleware:
  1. app.UseApiResponseAndExceptionWrapper();   
Simple as that!
 

Version 1.x

 
The previous versions of AutoWrapper already provides 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 have 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:
  1. {  
  2.     "message""Request successful.",  
  3.     "isError"false,  
  4.     "result": [  
  5.       {  
  6.         "id": 7002,  
  7.         "firstName""Vianne",  
  8.         "lastName""Durano",  
  9.         "dateOfBirth""2018-11-01T00:00:00"  
  10.       }  
  11.     ]  
  12. }  
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 default "result" property to something else like "data", then you can simply define your own schema for mapping it like in the following:
  1. public class MapResponseObject    
  2. {  
  3.     [AutoWrapperPropertyMap(Prop.Result)]  
  4.     public object Data { getset; }  
  5. }  
You can then pass the MapResponseObject class to the AutoWrapper middleware like this:  
  1. app.UseApiResponseAndExceptionWrapper<MapResponseObject>();   
On successful requests, your response should now look something like this after mapping: 
  1. {  
  2.     "message""Request successful.",  
  3.     "isError"false,  
  4.     "data": {  
  5.         "id": 7002,  
  6.         "firstName""Vianne",  
  7.         "lastName""Durano",  
  8.         "dateOfBirth""2018-11-01T00:00:00"  
  9.     }  
  10. }  
Notice that the "result" attribute is now replaced with the "data" attribute.
By default, AutoWrapper will spit out the following response format when an exception has occurred:
  1. {  
  2.     "isError"true,  
  3.     "responseException": {  
  4.         "exceptionMessage""Unhandled Exception occurred. Unable to process the request."  
  5.     }  
  6. }  
And if you set IsDebug property in the AutoWrapperOptions, it will result to something like this with stacktrace information: 
  1. {  
  2.     "isError"true,  
  3.     "responseException": {  
  4.         "exceptionMessage"" Input string was not in a correct format.",  
  5.         "details""   at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n   at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)\r\n …"  
  6.     }  
  7. }  
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
  1. public class MapResponseObject    
  2. {  
  3.     [AutoWrapperPropertyMap(Prop.ResponseException)]  
  4.     public object Error { getset; }  
  5.   
  6.     [AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)]  
  7.     public string Message { getset; }  
  8.   
  9.     [AutoWrapperPropertyMap(Prop.ResponseException_Details)]  
  10.     public string StackTrace { getset; }  
  11. }  
To test the output, you can write the following code to simulate an error:
  1. int num = Convert.ToInt32("10s");    
The output should now look something like this after the mapping:
  1. {  
  2.     "isError"true,  
  3.     "error": {  
  4.         "message"" Input string was not in a correct format.",  
  5.         "stackTrace""   at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n   at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)\r\n …"  
  6.     }  
  7. }  
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 that you want to map. Here is the list of default properties that you can map:
  1. [AutoWrapperPropertyMap(Prop.Version)]  
  2. [AutoWrapperPropertyMap(Prop.StatusCode)]  
  3. [AutoWrapperPropertyMap(Prop.Message)]  
  4. [AutoWrapperPropertyMap(Prop.IsError)]  
  5. [AutoWrapperPropertyMap(Prop.Result)]  
  6. [AutoWrapperPropertyMap(Prop.ResponseException)]  
  7. [AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)]  
  8. [AutoWrapperPropertyMap(Prop.ResponseException_Details)]  
  9. [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceErrorCode)]  
  10. [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceDocumentLink)]  
  11. [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors)]  
  12. [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Field)]  
  13. [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:
  1. throw new ApiException("Error blah", 400, "511""http://blah.com/error/511");    
And the default output format is going to look like this:
  1. {  
  2.     "isError"true,  
  3.     "responseException": {  
  4.         "exceptionMessage""Error blah",  
  5.         "referenceErrorCode""511",  
  6.         "referenceDocumentLink""http://blah.com/error/511"  
  7.     }  
  8. }  
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: 
  1. public class MapResponseObject    
  2. {  
  3.     [AutoWrapperPropertyMap(Prop.ResponseException)]  
  4.     public object Error { getset; }  
  5. }  
  6.   
  7. public class Error    
  8. {  
  9.     public string Message { getset; }  
  10.   
  11.     public string Code { getset; }  
  12.     public InnerError InnerError { getset; }  
  13.   
  14.     public Error(string message, string code, InnerError inner)  
  15.     {  
  16.         this.Message = message;  
  17.         this.Code = code;  
  18.         this.InnerError = inner;  
  19.     }  
  20.   
  21. }  
  22.   
  23. public class InnerError    
  24. {  
  25.     public string RequestId { getset; }  
  26.     public string Date { getset; }  
  27.   
  28.     public InnerError(string reqId, string reqDate)  
  29.     {  
  30.         this.RequestId = reqId;  
  31.         this.Date = reqDate;  
  32.     }  
  33. }  
You can then throw an error like this:
  1. throw new ApiException(    
  2.       new Error("An error blah.""InvalidRange",  
  3.       new InnerError("12345678", DateTime.Now.ToShortDateString())  
  4. ));  
The format of the output will now look like this:
  1. {  
  2.     "isError"true,  
  3.     "error": {  
  4.         "message""An error blah.",  
  5.         "code""InvalidRange",  
  6.         "innerError": {  
  7.             "requestId""12345678",  
  8.             "date""10/16/2019"  
  9.         }  
  10.     }  
  11. }   
 

Using Your Own API Response Schema

 
If mapping wont 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:
  1. 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: 
  1. public class MyCustomApiResponse    
  2. {  
  3.     public int Code { getset; }  
  4.     public string Message { getset; }  
  5.     public object Payload { getset; }  
  6.     public DateTime SentDate { getset; }  
  7.     public Pagination Pagination { getset; }  
  8.   
  9.     public MyCustomApiResponse(DateTime sentDate, object payload = nullstring message = ""int statusCode = 200, Pagination pagination = null)  
  10.     {  
  11.         this.Code = statusCode;  
  12.         this.Message = message == string.Empty ? "Success" : message;  
  13.         this.Payload = payload;  
  14.         this.SentDate = sentDate;  
  15.         this.Pagination = pagination;  
  16.     }  
  17.   
  18.     public MyCustomApiResponse(DateTime sentDate, object payload = null, Pagination pagination = null)  
  19.     {  
  20.         this.Code = 200;  
  21.         this.Message = "Success";  
  22.         this.Payload = payload;  
  23.         this.SentDate = sentDate;  
  24.         this.Pagination = pagination;  
  25.     }  
  26.   
  27.     public MyCustomApiResponse(object payload)  
  28.     {  
  29.         this.Code = 200;  
  30.         this.Payload = payload;  
  31.     }  
  32.   
  33. }  
  34.   
  35. public class Pagination    
  36. {  
  37.     public int TotalItemsCount { getset; }  
  38.     public int PageSize { getset; }  
  39.     public int CurrentPage { getset; }  
  40.     public int TotalPages { getset; }  
  41. }  
To test the result, you can create a GET method to something like this: 
  1. public async Task<MyCustomApiResponse> Get()    
  2. {  
  3.     var data = await _personManager.GetAllAsync();  
  4.   
  5.     return new MyCustomApiResponse(DateTime.UtcNow, data,  
  6.         new Pagination  
  7.         {  
  8.             CurrentPage = 1,  
  9.             PageSize = 10,  
  10.             TotalItemsCount = 200,  
  11.             TotalPages = 20  
  12.         });  
  13.   
  14. }  
Running the code should give you now the following response format:
  1. {  
  2.     "code": 200,  
  3.     "message""Success",  
  4.     "payload": [  
  5.         {  
  6.             "id": 1,  
  7.             "firstName""Vianne",  
  8.             "lastName""Durano",  
  9.             "dateOfBirth""2018-11-01T00:00:00"  
  10.         },  
  11.         {  
  12.             "id": 2,  
  13.             "firstName""Vynn",  
  14.             "lastName""Durano",  
  15.             "dateOfBirth""2018-11-01T00:00:00"  
  16.         },  
  17.         {  
  18.             "id": 3,  
  19.             "firstName""Mitch",  
  20.             "lastName""Durano",  
  21.             "dateOfBirth""2018-11-01T00:00:00"  
  22.         }  
  23.     ],  
  24.     "sentDate""2019-10-17T02:26:32.5242353Z",  
  25.     "pagination": {  
  26.         "totalItemsCount": 200,  
  27.         "pageSize": 10,  
  28.         "currentPage": 1,  
  29.         "totalPages": 20  
  30.     }  
  31. }  
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:
  1. [Route("{id:long}")]  
  2. [HttpPut]  
  3. public async Task<MyCustomApiResponse> Put(long id, [FromBody] PersonDTO dto)    
  4. {  
  5.     if (ModelState.IsValid)  
  6.     {  
  7.         try  
  8.         {  
  9.             var person = _mapper.Map<Person>(dto);  
  10.             person.ID = id;  
  11.   
  12.             if (await _personManager.UpdateAsync(person))  
  13.                 return new MyCustomApiResponse(DateTime.UtcNow, true"Update successful.");  
  14.             else  
  15.                 throw new ApiException($"Record with id: {id} does not exist.", 400);  
  16.         }  
  17.         catch (Exception ex)  
  18.         {  
  19.             _logger.Log(LogLevel.Error, ex, "Error when trying to update with ID:{@ID}", id);  
  20.             throw;  
  21.         }  
  22.     }  
  23.     else  
  24.         throw new ApiException(ModelState.AllErrors());  
  25. }  
Now when a model validation occurs, you will be getting a default response format to something like this:
  1. {  
  2.     "isError"true,  
  3.     "responseException": {  
  4.         "exceptionMessage""Request responded with validation error(s). Please correct the specified validation errors and try again.",  
  5.         "validationErrors": [  
  6.             {  
  7.                 "field""FirstName",  
  8.                 "message""'First Name' must not be empty."  
  9.             }  
  10.         ]  
  11.     }  
  12. }  
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 opensource project. :)
 

References