Click here to Skip to main content
14,355,062 members

AutoWrapper: Prettify Your ASP.NET Core APIs with Meaningful Responses

Rate this:
4.44 (13 votes)
Please Sign up or sign in to vote.
4.44 (13 votes)
4 Oct 2019CPOL
In this post, we will take a look at how we can beautify our ASP.NET Core API responses using AutoWrapper.

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 GETPOSTPUT and DELETE. Each of this action may return different types depending on how your endpoint is designed. Your POSTPUT 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 APIresponses 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:

//This is just an idiot example to trigger an error
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:

Image 1

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:

Image 2

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:

//This is just an idiot example to trigger an error
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:

Image 3

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)
{
    //Call a method to add a new record to the database
    try
    {
        var result = await SampleData.AddNew(band);
        return new ApiResponse("New record has been created to the database", result, 201);
    }
    catch (Exception ex)
    {
        //TO DO: Log 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)
    {
        //Call a method to add a new record to the database
        try
        {
            var result = await SampleData.AddNew(band);
            return new ApiResponse("New record has been created to the database", result, 201);
        }
        catch (Exception ex)
        {
            //TO DO: Log 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. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Vincent Maverick Durano
Architect
United States United States
A code monkey who loves to drink beer, play guitar and listen to music.

I currently work as a Solutions Architect and we build "cool things" to help people improve their health.

With over 13 years of professional experience working as a Sr. Software Engineer specializing mainly on Web and Mobile apps using Microsoft technologies. My exploration into programming began at the age of 15;Turbo PASCAL, C, C++, JAVA, VB6, Action Scripts and a variety of other equally obscure acronyms, mainly as a hobby. After several detours, I am here today on the VB.NET to C# channel. I have worked on Web Apps + Client-side technologies + Mobile Apps + Micro-services + REST APIs + Event Communication + Databases + Cloud + Containers , which go together like coffee crumble ice cream.

I have been awarded Microsoft MVP each year since 2009, awarded C# Corner MVP for 2015, 2016,2017 and 2018, CodeProject MVP, MVA, MVE, Microsoft Influencer, Dzone MVB, Microsoft ASP.NET Site Hall of Famer with All-Star level and a regular contributor at various technical community websites such as CSharpCorner, CodeProject, ASP.NET and TechNet.

Books written:
" Book: Understanding Game Application Development with Xamarin.Forms and ASP.NET
" Book (Technical Reviewer): ASP.NET Core and Angular 2
" EBook: Dockerizing ASP.NET Core and Blazor Applications on Mac
" EBook: ASP.NET MVC 5- A Beginner's Guide
" EBook: ASP.NET GridView Control Pocket Guide

Comments and Discussions

 
PraiseNice article Pin
asiwel9-Oct-19 19:28
professionalasiwel9-Oct-19 19:28 
GeneralRe: Nice article Pin
Vincent Maverick Durano21-Oct-19 14:09
mveVincent Maverick Durano21-Oct-19 14:09 
GeneralLooks redundant for me... Pin
Niemand2516-Sep-19 22:51
professionalNiemand2516-Sep-19 22:51 
I used similar wrapper in my first API project. However, I discarded it afterwards for more simple and HTTP consistent approach:
- if you get an expected result - data with response 200, 201 etc. - then there were no errors. No need to report obvious facts.
- if a request results in an exception, you get an appropriate response code and, if in debug mode, some exceptions wrapper (there are lots nice ones on net).

Version is redundant, by convention it is part of URL.
GeneralRe: Looks redundant for me... Pin
Vincent Maverick Durano17-Sep-19 11:34
mveVincent Maverick Durano17-Sep-19 11:34 
GeneralRe: Looks redundant for me... Pin
onefootswill17-Sep-19 20:56
memberonefootswill17-Sep-19 20:56 
GeneralRe: Looks redundant for me... Pin
Vincent Maverick Durano18-Sep-19 7:00
mveVincent Maverick Durano18-Sep-19 7:00 
GeneralRedundant for you maybe Pin
Lucas Vogel18-Sep-19 11:52
professionalLucas Vogel18-Sep-19 11:52 
GeneralRe: Redundant for you maybe Pin
Niemand2518-Sep-19 13:10
professionalNiemand2518-Sep-19 13:10 
GeneralRe: Redundant for you maybe Pin
Vincent Maverick Durano18-Sep-19 19:11
mveVincent Maverick Durano18-Sep-19 19:11 
GeneralRe: Redundant for you maybe Pin
Niemand2518-Sep-19 23:34
professionalNiemand2518-Sep-19 23:34 
GeneralRe: Redundant for you maybe Pin
Vincent Maverick Durano19-Sep-19 6:36
mveVincent Maverick Durano19-Sep-19 6:36 
GeneralRe: Redundant for you maybe Pin
Niemand2520-Sep-19 6:00
professionalNiemand2520-Sep-19 6:00 
GeneralRe: Looks redundant for me... Pin
Vincent Maverick Durano23-Sep-19 16:45
mveVincent Maverick Durano23-Sep-19 16:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 16 Sep 2019

Stats

11.5K views
116 downloads
24 bookmarked