How I handle Exceptions in ASP.NET
This is my preferred method of handling errors in ASP.NET projects. Perhaps, you’ll find it useful, or maybe you’ll find holes in it and poke at it in the comments, or improve it in your own projects.
I try to not have try-catch statements in controllers, preferring to use a GlobalExceptionFilter
or whichever construct is most appropriate for the version of .NET I am using.
I have annotated the GlobalExceptionFilter
code below, providing explanations for the choices I made.
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Foo.Filters;
public class GlobalExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order => int.MaxValue - 10; // Ensure it runs last in the Filter pipeline
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception is null) return;
var exception = context.Exception;
int statusCode = context.GetHttpStatusCode(
new Dictionary<Type, HttpStatusCode>() // Provide a mapping of Exception Type to HttpStatusCode
{
{ typeof(NotImplementedException), HttpStatusCode.NotImplemented },
{ typeof(TimeoutException), HttpStatusCode.RequestTimeout },
{ typeof(JwtException), HttpStatusCode.Unauthorized },
{ typeof(UnauthorizedAccessException), HttpStatusCode.Unauthorized },
{ typeof(System.ComponentModel.DataAnnotations.ValidationException), HttpStatusCode.BadRequest },
{ typeof(FluentValidation.ValidationException), HttpStatusCode.BadRequest },
}
) ?? HttpStatusCode.InternalServerError; // fallback to 500 internal server error
var telemetry = context.HttpContext.RequestServices.GetService(typeof(TelemetryClient)) as TelemetryClient;
var operationId = telemetry?.Context.Operation.Id ?? Guid.NewGuid().ToString();
var logger = context.HttpContext.RequestServices.GetService(typeof(ILogger)) as ILogger;
logger?.Error(exception, new
{
// Provides a predictable, searchable message in the Logs.
// The original Exception's Message will still be avaiilable as Exception.Message in the Logs.
Message = $"Error in {context.GetControllerName()}.{context.GetControllerActionName()}",
StatusCode = statusCode,
Request = new {
Controller = context.GetControllerName(),
Action = context.GetControllerActionName(),
context.HttpContext.Request.Path,
context.HttpContext.Request.Method
},
Application = Assembly.GetEntryAssembly().GetName().FullName,
HostedIP = System.Environment.MachineName,
Auth = new
{
Scheme = context.HttpContext.Items["AuthenticationScheme"],
},
// So we can search the logs by the OperationId to find out why the request failed.
OperationId = operationId
});
// Include the OperationId in the response Headers, so we can use it to lookup the exception in the logs
context.HttpContext.Response.Headers.Add("X-Operation-Id", operationId);
context.Result = new ObjectResult(
new ErrorResponse()
{
// Include actual error details in non-production environments
Errors = _configuration.ShouldIncludeExceptionDetails
? new List<string>
{
exception.Message,
exception.StackTrace
}
: new List<string> { "Exception Occured. Please contact administrator" }
})
{
StatusCode = Convert.ToInt32(statusCode)
};
context.ExceptionHandled = true;
}
}