In this post, we 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 learn 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.
Introduction
Building RESTFul APIs 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 APIs 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 expose 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 these actions 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 do not care about the consumer. What they care mostly is they can return data to the consumer and that’s it. APIs is not just about passing JSON
back and forth over HTTP
, but it’s also how you present meaningful responses to the developers who consume your API.
Always remember that…
Quote:
“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 is 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
- Support Swagger
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 wrapper 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:
- Declare the namespace below within Startup.cs:
using VMD.RESTApiResponseWrapper.Core.Extensions;
- Register the middleware below within the
Configure()
method of Startup.cs:
app.UseAPIResponseWrapperMiddleware();
Note: Make sure to register it "before" the MVC middleware.
- Done.
Here's an article demonstrating how to integrate this library into your ASP.NET Core REST API project: ASP.NET Core 2.1: Integrating VMD.RESTApiResponseWrapper.Core to Your REST API Application
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:
- Declare the following namespaces within WebApiConfig.cs:
using VMD.RESTApiResponseWrapper.Net;
using VMD.RESTApiResponseWrapper.Net.Filters;
- Register the following within WebApiConfig.cs:
config.Filters.Add(new ApiExceptionFilter());
config.MessageHandlers.Add(new WrappingHandler());
- Done.
Quote:
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 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.
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 its 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 the 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()
HandleSuccessRequestAsync()
.
The HandleExceptionAsync()
method handles the exceptions that have 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 methods above used the APIResponse
class as the final response object.
Now that we already implemented our custom middleware, 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 to display a meaningful response to the consumers.
Next are UnAuthorized
exceptions 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;
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 download the source code or check out the github repository. Thanks! :)
References