Error handling in ASP.NET Core Web API
Generating the proper response for exceptions in ASP.NET Core WepApi
Both Web API in .NET 4.5 and later and Web API in ASP.NET CORE have great error handling support out of the box. You can specify you global handler and control your response to a client when exception happens.
Depending on the environment where your REST API is running you may want to disclose different level of exception details. Most likely you do not want to expose all the exception details to public on your production environment
As mentioned in the note above, you do not want the same response for your clients who are accessing development and production environment. I will focus a bit more on the development and qa environment because this is the place where you will give most of the details in your response. For production it mostly good enough to response with 400 BadRequest status code (not in all cases of course).
The response message
Before we switch to different ways of handling exceptions and responding to the client in that case, let's first see how should we respond to a client. We can come up with different messages to be returned in a response, or we can even try to serialize back our Exception instance which occurred.
The thing is that this detail rich response might be analyzed by automation test running on your qa instance of the service and some automated actions could be taken based on it. Test client might not be written in .NET at all, so there might be a problem understanding it from the automation suite.
Another thing is consistency in Exception response, so that both automated services and developers are comfortable using it.
For these cases Microsoft is suggesting using specific structure (https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#710-response-formats). Based on this article I created an implementation which makes it easier to generate it from the System.Exception inherited class instance which occured.
Since this document from GitHub is saying that our properties should be in lower case and in C# property names most commonly start with upper case, I added a small Newtonsoft.Json.Serialization.DefaultContractResolver class implementation which will make all properties lowercase during serialization.
internal class LowercaseContractResolver : DefaultContractResolver { protected override string ResolvePropertyName(string propertyName) { return propertyName.ToLower(); } }
Now to go to the actual response model implementation which in my case I placed in the Models folder of my WeApi Core project.
using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace DynamicModel.Core.Web.Api.Models { /* https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#710-response-formats */ internal class LowercaseContractResolver : DefaultContractResolver { protected override string ResolvePropertyName(string propertyName) { return propertyName.ToLower(); } } public class ErrorModel { public ErrorModel(System.Exception exception) { this.Error = new Exception(exception.GetType().Name, exception.Message); this.Error.Target = $"{exception.TargetSite.ReflectedType.FullName}.{exception.TargetSite.Name}"; if (exception.InnerException != null) { this.Error.InnerError = new InnerException(exception.InnerException); } } public Exception Error { get; set; } public String ToJson() { return JsonConvert.SerializeObject(this, Formatting.None, new JsonSerializerSettings() { ContractResolver = new LowercaseContractResolver() }); } } public class Exception { public Exception(String code, String message) { this.Code = code; this.Message = message; } [Required] public String Code { get; set; } [Required] public String Message { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public String Target { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public IEnumerable<Exception> Details { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public InnerException InnerError { get; set; } } public class InnerException { public InnerException(System.Exception exception) { this.Code = exception.GetType().Name; if (exception.InnerException != null) { this.InnerError = new InnerException(exception.InnerException); } } [Required] public String Code { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public InnerException InnerError { get; set; } } }
Exception handling
There are two ways of handling exceptions in ASP.NET Core WebApi. One way is to use your custom implementation of ExceptionFilterAttribute class.
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Result = new JsonResult(new ErrorModel(context.Exception)); } }
This option is great if you want to handle exceptions selectively, only on specific controllers or actions. You just decorate controller or action inside controller with the attribute and voila, you have exception handling on it.
Second way of handling exceptions is setting up global exception handling in Startup.cs class. This is done inside Configure method.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { #region Error handling app.UseExceptionHandler(x => { x.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.ContentType = "application/json"; var ex = context.Features.Get<IExceptionHandlerFeature>(); if (ex != null) { await context.Response.WriteAsync( new ErrorModel(ex.Error).ToJson() ).ConfigureAwait(false); } }); }); #endregion loggerFactory.AddDebug(LogLevel.Information); } else { #region Error handling app.UseExceptionHandler(x => { x.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.BadRequest; context.Response.ContentType = "application/json"; await context.Response.WriteAsync(string.Empty).ConfigureAwait(false); }); }); #endregion loggerFactory.AddDebug(LogLevel.Warning); } app.UseMvc(); }
You can see that depending on the environment we return different result. For development we return the previously explained model serialized to JSON while in other environments we are simply returning empty response with 400 bad request status code.
Now to test the exception handling, we're gonna add sample controller which will just throw an exception and we'll check the response using Postman
using System; using Microsoft.AspNetCore.Mvc; namespace Sample.Core.Web.Api.Controllers { [Produces("application/json")] [Route("api/Test")] public class TestController : Controller { public IActionResult Get() { throw new Exception("Test exception thrown", new InvalidOperationException("Innet thrown exception")); } } }
Now when we hit the get action on the TestController we'll get the JSON representation of our exception model class instance
{ "error": { "code": "Exception", "message": "Test exception thrown", "target": "Sample.Core.Web.Api.Controllers.TestController.Get", "innererror": { "code": "InvalidOperationException" } } }
This response model is a suggestion, you do not have to keep strict to it as you may need to expose more details. For example in some cases I also expose description for inner exception or the full stacktrace in the main Error model. Consider this as a guideline
References
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article