Wrapping ASP.NET Web API Responses for consistency and to provide additional information

Most well known public facing API's return a consistent response with a similar structure returned for all requests regardless of success or failure. This makes consumption of the API far easier and more intuitive. It also allows custom meta data to be added to every response. Out of the box, ASP.NET Web API does not use a common structure but it is very easy to change. This post explains how you can shape your responses by utilising a simple DelegatingHandler.

The problem

The standard ASP.NET Web API responses are rather inconsistent.

If you are taking a RESTful approach to your API then you will be utilising HTTP verbs such as GET, PUT and POST. When retrieving data with a GET, your response body will contain data. This may be an array for multiple records (e.g. GET /products) or a single object (e.g. GET /products/42). A POST may not return a response body at all and instead use an HTTP Header to point the user to the newly created resource. If your service needs to return an error then the in-built error handling will return an object with a message property exposing the error message. In each of these cases, we are returning very different data to the client. Depending on your client, this can make it very difficult to consume your service. You need to check each call and see what format to expect. If you are expecting a JSON array and get a JSON object error or an empty body plus a 404 status code then your client code can quickly become messy.

In addition, returning common meta data with every response is difficult.

It is often useful to return certain data with every request. Examples include errors and warnings, API call counts and paging information. It doesn't make sense to clutter up your controller actions with calls to populate this information and even if you do, you are still left with the problem of requests that fail for some reason and do not end up calling some or all of your action code.

The solution

Delegating handlers are extremely useful for cross cutting concerns. They hook into the very early and very late stages of the request-response pipeline making them ideal for manipulating the response right before it is sent back to the client. The code below shows one example of a simple DelegatingHandler:

public class WrappingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);

        return BuildApiResponse(request, response);
    }

    private static HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
    {
        object content;
        string errorMessage = null;

        if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
        {
            HttpError error = content as HttpError;

            if (error != null)
            {
                content = null;
                errorMessage = error.Message;

#if DEBUG
                errorMessage = string.Concat(errorMessage, error.ExceptionMessage, error.StackTrace);
#endif
            }
        }

        var newResponse = request.CreateResponse(response.StatusCode, new ApiResponse(response.StatusCode, content, errorMessage));

        foreach (var header in response.Headers)
        {
            newResponse.Headers.Add(header.Key, header.Value);
        }

        return newResponse;
    }
}

As you can see, there is very little to it. If there is an error, then we extract the message (and the stacktrace in debug mode). You will probably want to add some logic to sanitise the error messages to ensure that you do not expose more than you intended.

We use request.CreateResponse which creates a new response with the appropriate formatter. This means that we need to copy over any headers from the old unwrapped response. As an alternative, we could keep the existing response and just update the content. As far as I can tell, this requires that you hard code the formatter but this is not necessarily such a bad thing if like many people, you solely use JSON.

The handler uses a custom wrapper class:

[DataContract]
public class ApiResponse
{
    [DataMember]
    public string Version { get { return "1.2.3"; } }

    [DataMember]
    public int StatusCode { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public string ErrorMessage { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public object Result { get; set; }

    public ApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
    {
        StatusCode = (int)statusCode;
        Result = result;
        ErrorMessage = errorMessage;
    }
}

Obviously, you can adapt the code for your own purposes and add your own meta data as required.

We are using data contract attributes to allow us to not return certain properties such as ErrorMessage if they are null. Elsewhere, we have also removed the XmlFormatter which means that we do not need to worry about KnownTypes configuration.

Hook up your new handler by adding the following line to your start up code (typically WebApiConfig.cs in App_Start):

config.MessageHandlers.Add(new WrappingHandler());

You should now find that your responses are all wrapped nicely:

{
	"Version":"1.2.3",
	"StatusCode":200,
	"Result":
	{
		"Id":42,
		"QuestionText":"This is a test question",
		"QuestionImageUrl": "http://www.blah.com/test.png"
	}
}

Even your error responses:

{
	"Version":"1.2.3",
	"StatusCode":400,
	"ErrorMessage":"Invalid question Id"
}

This example is very basic but it is trivial to extend it. If you have a public API, you might very well want to query a data store and return quota information such a number of requests still available. The important thing is to make your responses consistent.

In a future post, we'll talk about extending our example to return paging information based on IQueryable data sources.

Comments

Avatar for Michel Michel wrote on 03 Dec 2014

5 days trying to figure out why my ios app cannot process the JSON return by asp.net web Api. Thank you for this great post.

Avatar for Kevin Park Kevin Park wrote on 13 Mar 2024

This is exactly what i was looking for. Thanks