ProudMonkey

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

This space is for rent. Contact me at vmsdurano at gmail dot com.

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 speedup 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:

PM> Install-Package AutoWrapper.Core -Version 2.0.1  

(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();  

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:

{
    "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 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 to 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 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 that 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 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:

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 opensource project. :)

References

Buy Me A Coffee