ProudMonkey

ASP.NET Core and Web API: A Custom Wrapper for Managing Exceptions and Consistent Responses

Introduction

Building RESTFul API’s has been very popular nowadays and most projects that we build today heavily rely on APIs/Services to communicate with data. As you may know, creating Web API’s is an easy task, but designing a good API isn’t as easy as you may think especially if you are working with a lot of projects or microservices that exposes some public API end-points.

This article will talk about how to implement a custom wrapper for your ASP.NET Core and Web API applications for managing exceptions, providing meaningful and consistent responses to consumers.

Why?

Before we move down further, let’s talk about the “Why” thing first. Why we need to implement a custom wrapper and why it’s a good thing.

ASP.NET Core and the standard ASP.NET Web API allow us to create APIs in just a snap; however they do not provide a consistent response for successful requests and errors out of the box. To make it clearer, if you are taking a RESTful approach to your API then you will be utilising HTTP verbs such as GET, POST, PUT and DELETE. Each of this action may return different types depending on how your method/action is designed. Your POST, PUT and DELETE end-points may return a data or not at all. Your GET end-point may return a string, a List<T>, an IEnumerable, a custom class or 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.

When building APIs for “real” application projects, many developers does not care about the consumer. What they care mostly is they can return data to the consumer and that’s it. 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 your API.

Always remember that…

“A good API design is a UX for developers who consume it.”

As a developer who values the consumers, we want to give meaningful and consistent API responses to them. That why in this post, we will implement a custom wrapper that can be reused across applications that has the following features:

  • Handle unexpected errors
  • Handle ModelState validation errors
  • A configurable custom API exception
  • A consistent response object for Result and Errors
  • A detailed Result response
  • A detailed Error response
  • A configurable HTTP StatusCodes
  • Enable Swagger support

How?

When working with either ASP.NET Core or standard Web API, 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. By using a custom wrapper for your ASP.NET Core and Web API responses, you can ensure that all of your responses have a consistent structure, and all exceptions will be handled.

We will take a look at how we are going to implement a custom wrappers that handles all the features listed above for both ASP.NET Core and standard Web APIs.

VMD.RESTApiResponseWrapper Nuget Packages

If you want to skip the actual code implementation, there are two separate Nuget packages that you can integrate directly to your project:

  • VMD.RESTApiResponseWrapper.Core (For ASP.NET Core Apps)
  • VMD.RESTApiResponseWrapper.Net (For Standard Web API Apps)

Each of these libraries was created separately. The VMD.RESTApiResponseWrapper.Core was built using ASP.NET Core 2.0 with Visual Studio 2017. It uses a middleware to implement the wrapper and managing exceptions. The VMD.RESTApiResponseWrapper.Net on the other hand was built using full .NET Framework v4.6 with Visual Studio 2015. It uses a DelegatingHandler to implement the wrapper and uses an ExceptionFilterAttribute for handling exceptions.

Installation and Usage

First, you need to install the Newtonsoft.json package before installing the VMD.RESTApiResponseWrapper package.

ASP.NET Core Integration

For ASP.NET Core apps, you can install the package via NPM or using the following command:

PM> Install-Package VMD.RESTApiResponseWrapper.Core -Version 1.0.3  

After the installation, you can start integrating the wrapper to your ASP.NET Core project by following the steps below:

(1) Declare the namespace below within Startup.cs

using VMD.RESTApiResponseWrapper.Core.Extensions;  

(2) Register the middleware below within the Configure() method of Startup.cs

app.UseAPIResponseWrapperMiddleware();  

Note: Make sure to register it "before" the MVC middleware

ASP.NET Web API Integration

For standard ASP.NET Web API applications, you can do:

PM> Install-Package VMD.RESTApiResponseWrapper.Net -Version 1.0.3  

After the installation, you can start integrating the wrapper to your ASP.NET Web API project by following the steps below:

(1) Declare the following namespaces within WebApiConfig.cs

using VMD.RESTApiResponseWrapper.Net;  
using VMD.RESTApiResponseWrapper.Net.Filters;  

(2) Register the following within WebApiConfig.cs

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

Note: The latest versions of both packages as of this time of writing is v1.0.3

Sample Response Output

The following are examples of different response output:

Successful response format with data:

{
    "Version": "1.0.0.0",
    "StatusCode": 200,
    "Message": "Request successful.",
    "Result": [
        "value1",
        "value2"
    ]
}  

Successful response format without data:

{
    "Version": "1.0.0.0",
    "StatusCode": 201,
    "Message": "Student with ID 6 has been created."
}  

Response format for validation errors:

{
    "Version": "1.0.0.0",
    "StatusCode": 400,
    "Message": "Request responded with exceptions.",
    "ResponseException": {
        "IsError": true,
        "ExceptionMessage": "Validation Field Error.",
        "Details": null,
        "ReferenceErrorCode": null,
        "ReferenceDocumentLink": null,
        "ValidationErrors": [
            {
                "Field": "LastName",
                "Message": "'Last Name' should not be empty."
            },
            {
                "Field": "FirstName",
                "Message": "'First Name' should not be empty."
            }
    ]
    }
}

Response format for errors:

{
    "Version": "1.0.0.0",
    "StatusCode": 404,
    "Message": "Unable to process the request.",
    "ResponseException": {
        "IsError": true,
        "ExceptionMessage": "The specified URI does not exist. Please verify and try again.",
                  "Details": null,
        "ReferenceErrorCode": null,
        "ReferenceDocumentLink": null,
        "ValidationErrors": null
    }
} 

Defining a Custom Exception

This library isn't just a middleware or a wrapper; it also provides a method that you can use for defining your own exception. For example, if you want to throw your own exception message, you could simply do:

throw new ApiException("Your Message",401, ModelStateExtension.AllErrors(ModelState));  

The ApiException has the following parameters that you can set:

ApiException(string message,  
             int statusCode = 500,
             IEnumerable<ValidationError> errors = null,
             string errorCode = "",
             string refLink = "")

Defining Your Own Response Object

Aside from throwing your own custom exception, you could also return your own custom defined JSON response by using the ApiResponse object in your API controller. For example:

return new APIResponse(201,"Created");  

The APIResponse has the following parameters that you can set:

APIResponse(int statusCode,  
            string message = "",
            object result = null,
            ApiError apiError = null,
            string apiVersion = "1.0.0.0")

Package Source Code

The codes for these wrappers are open-source and available at github:

Feel free to check it out. Your feedback is much appreciated.

The Implementation

Let’s see how the custom wrapper is implemented for both ASP.NET Core and standard Web API. Let’s start with the common classes that were used for both projects.

The Wrapper Classes

Both ASP.NET Core and standard Web API projects used the following classes:

  • ValidationError
  • ApiError
  • ApiException
  • ApiResponse
  • ResponseMessageEnum

Each class above will be used to implement the custom wrapper for managing exceptions and response consistency. Keep in mind that the code demonstrated in this article are just the basic foundation of the library. You are obviously free to modify and add your own properties, or even customize the implementation based on your business needs.

Here are the actual codes for each class:

ValidationError Class

public class ValidationError  
{
    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string Field { get; }

    public string Message { get; }

    public ValidationError(string field, string message)
    {
        Field = field != string.Empty ? field : null;
        Message = message;
    }
}

The ValidationError class holds some properties for storing the field and it's corresponding message. Notice the [JsonProperty(NullValueHandling=NullValueHandling.Ignore)] attribute on the Field property. This is to ensure that the field will not be serialized in the case of a null value.

ApiError Class

public class ApiError  
{
    public bool IsError { get; set; }
    public string ExceptionMessage { get; set; }
    public string Details { get; set; }
    public string ReferenceErrorCode { get; set; }
    public string ReferenceDocumentLink { get; set; }
    public IEnumerable<ValidationError> ValidationErrors { get; set; }

    public ApiError(string message)
    {
        this.ExceptionMessage = message;
        this.IsError = true;
    }

    public ApiError(ModelStateDictionary modelState)
    {
        this.IsError = true;
        if (modelState != null && modelState.Any(m => m.Value.Errors.Count > 0))
        {
            this.ExceptionMessage = "Please correct the specified validation errors and try again.";
            this.ValidationErrors = modelState.Keys
            .SelectMany(key => modelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage)))
            .ToList();

        }
    }
}

The ApiError class is a custom serialization type used to return the error information via JSON to the consumer. This class holds a few important properties to provide meaningful information to consumers such as the ExceptionMessage, Details, ReferenceErrorCode, ReferenceDocumentLink and ValidationErrors. The class also has an overload constructor to pass in a ModelStateDictionary to return a list of validation errors to the consumers.

ApiException Class

public class ApiException : System.Exception  
{
    public int StatusCode { get; set; }

    public IEnumerable<ValidationError> Errors { get; set; }

    public string ReferenceErrorCode { get; set; }
    public string ReferenceDocumentLink { get; set; }

    public ApiException(string message,
                        int statusCode = 500,
                        IEnumerable<ValidationError> errors = null,
                        string errorCode = "",
                        string refLink = "") :
        base(message)
    {
        this.StatusCode = statusCode;
        this.Errors = errors;
        this.ReferenceErrorCode = errorCode;
        this.ReferenceDocumentLink = refLink;
    }

    public ApiException(System.Exception ex, int statusCode = 500) : base(ex.Message)
    {
        StatusCode = statusCode;
    }
}

The ApiException class is a custom exception that is used to throw explicit and application generated errors. These are typically used for validation errors or common operations that can have known negative responses such as a failed login attempt. The goal is to return a well-defined error message that is safe to be used for consumers.

ApiResponse Class

[DataContract]
public class APIResponse  
{
    [DataMember]
    public string Version { get; set; }

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

    [DataMember]
    public string Message { get; set; }

    [DataMember(EmitDefaultValue = false)]
    public ApiError ResponseException { get; set; }

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

    public APIResponse(int statusCode, string message = "", object result = null, ApiError apiError = null, string apiVersion = "1.0.0.0")
    {
        this.StatusCode = statusCode;
        this.Message = message;
        this.Result = result;
        this.ResponseException = apiError;
        this.Version = apiVersion;
    }
}

The APIResponse class is a custom wrapper response object that is used to provide a consistent data structure for all API responses. This contains some basic properties such as Version, StatusCode, Message, ResponseException and Result. We are using DataContract attributes to allow us define which properties to return, for example, The ResponseException and Result properties will not be returned if their values are null.

ResponseMessage Enum

public enum ResponseMessageEnum  
{
    [Description("Request successful.")]
    Success,
    [Description("Request responded with exceptions.")]
    Exception,
    [Description("Request denied.")]
    UnAuthorized,
    [Description("Request responded with validation error(s).")]
    ValidationError,
    [Description("Unable to process the request.")]
    Failure
}

The ResponseMessageEnum provides an enumeration for response description such as Success, Exception, UnAuthorize and ValidationError.

The ASP.NET Core Implementation

Now that we already have our wrapper classes’ ready, it’s time for us to do the actual implementation of them.

For ASP.NET Core implementation, we will use a Middleware to implement the custom wrapper features listed above. Middleware are the components that make up the pipeline that handles request and responses for the application. Each piece of middleware called has the option to do some processing on the request before calling next piece of middleware in line. After execution returns from the call to the next middleware, there is an opportunity to do processing on the response. For more details, see: ASP.NET Core Middleware

We need to do some work inside our middleware class since we want to spit out our own predefined Response object and we want to capture or filter out explicit and unhandled API exceptions.

Here’s the code of the custom ASP.NET Core middleware.

public class APIResponseMiddleware  
{
    private readonly RequestDelegate _next;

    public APIResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (IsSwagger(context))
            await this._next(context);
        else
        {
            var originalBodyStream = context.Response.Body;

            using (var responseBody = new MemoryStream())
            {
                context.Response.Body = responseBody;

                try
                {
                    await _next.Invoke(context);

                    if (context.Response.StatusCode == (int)HttpStatusCode.OK)
                    {
                        var body = await FormatResponse(context.Response);
                        await HandleSuccessRequestAsync(context, body, context.Response.StatusCode);

                    }
                    else
                    {
                        await HandleNotSuccessRequestAsync(context, context.Response.StatusCode);
                    }
                }
                catch (System.Exception ex)
                {
                    await HandleExceptionAsync(context, ex);
                }
                finally
                {
                    responseBody.Seek(0, SeekOrigin.Begin);
                    await responseBody.CopyToAsync(originalBodyStream);
                }
            }
        }

    }

    private static Task HandleExceptionAsync(HttpContext context, System.Exception exception)
    {
        ApiError apiError = null;
        APIResponse apiResponse = null;
        int code = 0;

        if (exception is ApiException)
        {
            var ex = exception as ApiException;
            apiError = new ApiError(ex.Message);
            apiError.ValidationErrors = ex.Errors;
            apiError.ReferenceErrorCode = ex.ReferenceErrorCode;
            apiError.ReferenceDocumentLink = ex.ReferenceDocumentLink;
            code = ex.StatusCode;
            context.Response.StatusCode = code;

        }
        else if (exception is UnauthorizedAccessException)
        {
            apiError = new ApiError("Unauthorized Access");
            code = (int)HttpStatusCode.Unauthorized;
            context.Response.StatusCode = code;
        }
        else
        {
#if !DEBUG
            var msg = "An unhandled error occurred.";
            string stack = null;
#else
                var msg = exception.GetBaseException().Message;
                string stack = exception.StackTrace;
#endif

            apiError = new ApiError(msg);
            apiError.Details = stack;
            code = (int)HttpStatusCode.InternalServerError;
            context.Response.StatusCode = code;
        }

        context.Response.ContentType = "application/json";

        apiResponse = new APIResponse(code, ResponseMessageEnum.Exception.GetDescription(), null, apiError);

        var json = JsonConvert.SerializeObject(apiResponse);

        return context.Response.WriteAsync(json);
    }

    private static Task HandleNotSuccessRequestAsync(HttpContext context, int code)
    {
        context.Response.ContentType = "application/json";

        ApiError apiError = null;
        APIResponse apiResponse = null;

        if (code == (int)HttpStatusCode.NotFound)
            apiError = new ApiError("The specified URI does not exist. Please verify and try again.");
        else if (code == (int)HttpStatusCode.NoContent)
            apiError = new ApiError("The specified URI does not contain any content.");
        else
            apiError = new ApiError("Your request cannot be processed. Please contact a support.");

        apiResponse = new APIResponse(code, ResponseMessageEnum.Failure.GetDescription(), null, apiError);
        context.Response.StatusCode = code;

        var json = JsonConvert.SerializeObject(apiResponse);

        return context.Response.WriteAsync(json);
    }

    private static Task HandleSuccessRequestAsync(HttpContext context, object body, int code)
    {
        context.Response.ContentType = "application/json";
        string jsonString, bodyText = string.Empty;
        APIResponse apiResponse = null;


        if (!body.ToString().IsValidJson())
            bodyText = JsonConvert.SerializeObject(body);
        else
            bodyText = body.ToString();

        dynamic bodyContent = JsonConvert.DeserializeObject<dynamic>(bodyText);
        Type type;

        type = bodyContent?.GetType();

        if (type.Equals(typeof(Newtonsoft.Json.Linq.JObject)))
        {
            apiResponse = JsonConvert.DeserializeObject<APIResponse>(bodyText);
            if (apiResponse.StatusCode != code)
                jsonString = JsonConvert.SerializeObject(apiResponse);
            else if (apiResponse.Result != null)
                jsonString = JsonConvert.SerializeObject(apiResponse);
            else
            {
                apiResponse = new APIResponse(code, ResponseMessageEnum.Success.GetDescription(), bodyContent, null);
                jsonString = JsonConvert.SerializeObject(apiResponse);
            }
        }
        else
        {
            apiResponse = new APIResponse(code, ResponseMessageEnum.Success.GetDescription(), bodyContent, null);
            jsonString = JsonConvert.SerializeObject(apiResponse);
        }

        return context.Response.WriteAsync(jsonString);
    }

    private async Task<string> FormatResponse(HttpResponse response)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        var plainBodyText = await new StreamReader(response.Body).ReadToEndAsync();
        response.Body.Seek(0, SeekOrigin.Begin);

        return plainBodyText;
    }

    private bool IsSwagger(HttpContext context)
    {
        return context.Request.Path.StartsWithSegments("/swagger");

    }
}

The main method of our custom middleware is the Invoke(). This method accepts an HttpContext as parameter. The context holds the current Request and Response object from the pipeline. This allows us to intercept the context and do some custom processing, which in this case: (a) handle exceptions (b) return a standard custom response object.

The APIResponseMiddleware also contains three main private methods: HandleExceptionAsync(), HandleNotSuccessRequestAsync() and HandleSuccessRequestAsync().

The HandleExceptionAsync() method handles the exceptions that has been thrown and then construct a custom response object out from it and return it as a final response object. The HandleNotSuccessRequestAsync() method handles specific response based on status code. For this example, we've filtered out NotFound and NoContent StatusCodes and then construct a custom response. Finally, the HandleSuccessRequestAsync() method handles successful response and constructs a custom response object that will be returned to the consumers.

Note that all method above used the APIResponse class as the final response object.

Now that we already implemented our custom middlerware, we can then create a static class to simplify adding the middleware to the application’s pipeline:

public static class ApiResponseMiddlewareExtension  
{
    public static IApplicationBuilder UseAPIResponseWrapperMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<APIResponseMiddleware>();
    }
}

And the final step to use our custom middleware is to call the extension method that we created above within the Configure() method of Startup class:

app.UseAPIResponseMiddleware();  

The Standard ASP.NET Web API Implementation

Since middleware’s are designed for ASP.NET Core applications, in the standard Web API project, we will use the ExceptionFilterAttribute to handle and manage exceptions and use a DelegatingHandler to implement the custom response wrapper.

Exception Filter

Let’s start off with the exception filter implementation. Here’s the code for the filter implementation:

public class ApiExceptionFilter : ExceptionFilterAttribute  
{
    public override void OnException(HttpActionExecutedContext context)
    {
        ApiError apiError = null;
        APIResponse apiResponse = null;
        int code = 0;

        if (context.Exception is ApiException)
        {
            var ex = context.Exception as ApiException;
            apiError = new ApiError(ex.Message);
            apiError.ValidationErrors = ex.Errors;
            apiError.ReferenceErrorCode = ex.ReferenceErrorCode;
            apiError.ReferenceDocumentLink = ex.ReferenceDocumentLink;
            code = ex.StatusCode;

        }
        else if (context.Exception is UnauthorizedAccessException)
        {
            apiError = new ApiError("Unauthorized Access");
            code = (int)HttpStatusCode.Unauthorized;
        }
        else
        {
#if !DEBUG
            var msg = "An unhandled error occurred.";
            string stack = null;
#else
                var msg = context.Exception.GetBaseException().Message;
                string stack = context.Exception.StackTrace;
#endif

            apiError = new ApiError(msg);
            apiError.Details = stack;
            code = (int)HttpStatusCode.InternalServerError;

        }

        apiResponse = new APIResponse(code, ResponseMessageEnum.Exception.GetDescription(), null, apiError);

        HttpStatusCode c = (HttpStatusCode)code;

        context.Response = context.Request.CreateResponse(c, apiResponse);
    }
}

Just like in the ASP.NET Core's HandleExceptionAsync() method, the custom exception filter method handles the exception that is thrown from the application. The implementation of the filter is pretty much identical to the ASP.NET Core implementation of HandleExceptionAsync() method.

Let’s elaborate a bit of what the filter actually doing. The exception filter differentiates between several different exception types. First it looks at a custom ApiException type, which is a special application generated Exception that can be used display a meaningful response to the consumers.

Next are UnAuthorized execptions which are handled specially by returning a forced 401 exception which can be used on the client to force authentication.

Finally there are Unhandled exceptions - these are unexpected failures that the application doesn't explicitly know about. This could be a hardware failure, a null reference exception, an unexpected parsing error. Basically anything that's - unhandled. These errors generate a generic error message in production so that no sensitive data is returned.

Delegating Handler

DelegatingHandlers 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. Here’s the code for the Delegating Handler.

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

    private static HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
    {

        dynamic content = null;
        object data = null;
        string errorMessage = null;
        ApiError apiError = null;

        var code = (int)response.StatusCode;

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

            //handle exception
            if (error != null)
            {
                content = null;

                if (response.StatusCode == HttpStatusCode.NotFound)
                    apiError = new ApiError("The specified URI does not exist. Please verify and try again.");
                else if (response.StatusCode == HttpStatusCode.NoContent)
                    apiError = new ApiError("The specified URI does not contain any content.");
                else
                {
                    errorMessage = error.Message;

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

                    apiError = new ApiError(errorMessage);
                }

                data = new APIResponse((int)code, ResponseMessageEnum.Failure.GetDescription(), null, apiError);

            }
            else
                data = content;
        }
        else
        {
            if (response.TryGetContentValue(out content))
            {
                Type type;
                type = content?.GetType();

                if (type.Name.Equals("APIResponse"))
                {
                    response.StatusCode = Enum.Parse(typeof(HttpStatusCode), content.StatusCode.ToString());
                    data = content;
                }
                else if (type.Name.Equals("SwaggerDocument"))
                    data = content;
                else
                    data = new APIResponse(code, ResponseMessageEnum.Success.GetDescription(), content);
            }
            else
            {
                if (response.IsSuccessStatusCode)
                    data = new APIResponse((int)response.StatusCode, ResponseMessageEnum.Success.GetDescription());
            }
        }

        var newResponse = request.CreateResponse(response.StatusCode, data);

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

        return newResponse;
    }

    private bool IsSwagger(HttpRequestMessage request)
    {
        return request.RequestUri.PathAndQuery.StartsWith("/swagger");
    }
}

The code above is implemented differently but it accomplishes the same things as what is implemented in ASP.NET Core middleware. For this case, we are using a delegating handler to intercept the current context and to construct a custom response object to consumers. We used the Request.CreateResponse() method to create a new response with the appropriate formatter and then copy over any headers from the old unwrapped response before returning the final response object.

To use the filter and wrapper, you just need to register them within your WebApiConfig.cs file:

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

Summary

In this post, we’ve learned how to create a simple custom wrapper for managing API exceptions and consistent responses for both ASP.NET Core and standard Web API projects. We also learned how to easily integrate the VMS.RESTApiResponseWrapper libraries to your ASP.NET Core and standard Web API projects without doing all the code implementation demonstrated in this article.

Feel free to check out the github repository for the source code. Thanks! :)

References