Welcome to the navigation

Nulla ut sit commodo id ea eiusmod sunt amet, voluptate ipsum mollit sed irure nostrud enim velit anim ut ex labore nisi lorem in sint. Dolor esse irure pariatur, dolore exercitation duis incididunt commodo proident, in id ipsum est velit elit, do laborum, officia excepteur nulla dolore sed deserunt adipisicing

Yeah, this will be replaced... But please enjoy the search!

Global exception handling in ASP.NET Core

I had the need for a global exception handling in ASP.NET Core that separates Page controllers and API controllers.

There are a few good guides on how to implement general or global exception handling. One increasingly popular way of achieving this is to use the UseExceptionHandler extension method on the IApplicationBuilder. In example

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Simple and compact, the downside of this is that it is GLOBAL, meaning it doesn't really play well unless you are building API-only applications.

Using ExceptionFilterAttribute

Another more versatile way of managing global exceptions is to implement a custom ExceptionFilterAttribute. This is way more powerful and we do get access to the ActionContext since the ExceptionContext implements it via the FilterContext. The ActionContext property ActionDescriptor can be cast into a ControllerActionDescriptor which is what the ExceptionContext implement.

Why is this useful?

The ControlerActionDescriptor contain a ControllerTypeInfo property that allows us to see what controller implementation that caused the exception. This is done by checking if the controller implements ControllerBase and or Controller. The general rules are

  • Api's implements ControllerBase but not Controller
  • Pages implements both ControllerBase and Controller

The quick implementation of this would look like this

using System;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Project.Business.Filters
{
    public class ExceptionActionFilter : ExceptionFilterAttribute
    {
        private readonly IHostingEnvironment _hostingEnvironment;
        private readonly TelemetryClient _telemetryClient;

        public ExceptionActionFilter(
            IHostingEnvironment hostingEnvironment,
            TelemetryClient telemetryClient)
        {
            _hostingEnvironment = hostingEnvironment;
            _telemetryClient = telemetryClient;
        }

        #region Overrides of ExceptionFilterAttribute

        public override void OnException(ExceptionContext context)
        {
            var actionDescriptor = (Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor;
            Type controllerType = actionDescriptor.ControllerTypeInfo;

            var controllerBase = typeof(ControllerBase);
            var controller = typeof(Controller);

            // Api's implements ControllerBase but not Controller
            if (controllerType.IsSubclassOf(controllerBase) && !controllerType.IsSubclassOf(controller))
            {
                // Handle web api exception
            }

            // Pages implements ControllerBase and Controller
            if (controllerType.IsSubclassOf(controllerBase) && controllerType.IsSubclassOf(controller))
            {
                // Handle page exception
            }

            if (!_hostingEnvironment.IsDevelopment())
            {
                // Report exception to insights
                _telemetryClient.TrackException(context.Exception);
                _telemetryClient.Flush();
            }
            
            base.OnException(context);
        }

        #endregion
    }
}

Register in services

services.AddMvc(options =>
{
    options.Filters.Add<ExceptionActionFilter>();
});

Adjust accordingly, the TelemetryClient is optional, this can also be split into different filters depending on your needs.

A typical return of mine for the web api would look like this

// Api's implements ControllerBase but not Controller
if (controllerType.IsSubclassOf(controllerBase) && !controllerType.IsSubclassOf(controller))
{
    // Handle web api exception
    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    context.HttpContext.Response.ContentType = "application/json";
    context.Result = new JsonResult(context.Exception.Message);
}