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); }