Explaning IExceptionFilter in .NET Core

In this article, we will cover ASP.NET Core IExceptionFilter. Please refer to my previous article: IActionFilter in .Net Core

So, Let's get started.

Exception Filter in ASP.NET Core

Exception Filter allows us to handle unhandled exceptions that occur while processing an HTTP request within our application. Exception Filters in ASP.NET Core Applications are used for tasks such as Logging Exceptions, Returning Custom Error Responses, and Performing any other necessary action when an exception occurs.

Exception Filter also provides a way to centralize the exception-handling logic and keep it separate from our controller or from the business and data access logic, making our code more organized and maintainable.

IExceptionFilter

IExceptionFilter is an interface in ASP.NET Core that provides a mechanism for handling exceptions that occur during the processing of a request. By implementing IExceptionFilter, you can write custom logic to handle exceptions globally or per-controller.

Advantages of IExceptionFilter

  1. Centralized Exception Handling: You can manage all your exceptions in a single place, making it easier to maintain and modify your exception-handling strategy.
  2. Separation of Concerns: By handling exceptions separately, you keep the error-handling logic away from your business logic, improving code readability and maintainability.
  3. Consistent Error Responses: It allows you to standardize the way errors are reported back to the client, which can improve the API's usability. You can return consistent model formats, error codes, and messages.
  4. Access to HttpContext: Since filters have access to the `HttpContext`, you can easily log errors, modify responses, or perform any other operation based on the context of the request.
  5. Interception of All Exceptions: It can catch exceptions that aren't handled anywhere else, ensuring that your application can respond gracefully to unexpected errors.
  6. Custom Logic: You can implement any custom logic needed for exception handling, such as logging specific exceptions differently.

Disadvantages of IExceptionFilter

  1. Global Scope: When implemented globally, all exceptions will be handled by the same filter. This might not be desirable if different controllers or actions require different handling strategies.
  2. Complex Error Handling Logic: If you have complex error-handling needs, managing too many unique cases in a single filter could lead to convoluted code.
  3. Performance Concerns: Introducing additional logic in exception handling can potentially add overhead, especially if the handling involves extensive processing or logging.
  4. Limited to Web Context: Unlike middleware, exception filters are limited in scope to the MVC pipeline. They cannot handle exceptions that occur outside of the controller actions, such as in middleware.
  5. Difficulty in Testing: Since exception filters are tied to the ASP.NET injection system, they can introduce complexity when writing unit tests, particularly if they depend on the HttpContext.

Implementing IExceptionFilter

Implementing IExceptionFilter can greatly benefit your ASP.NET Core applications by providing structured and centralized exception handling. However, balance must be struck in how it's used to avoid complexity, ensure performance, and maintain flexibility. Choosing the right approach to exception handling may also involve combining it with other options like middleware, custom error pages, or even using logged service responses as needed.

public class HandleExceptionFilter : IExceptionFilter
    {
      private readonly ILogger<HandleExceptionFilter> _logger;
      public HandleExceptionFilter(ILogger<HandleExceptionFilter> logger)
        {
            _logger = logger;
        }
        
      public override void OnException(ExceptionContext filterContext)
        {
            bool isAjaxCall = filterContext.HttpContext.Request.Headers["x-requested-with"] == "XMLHttpRequest";
            filterContext.HttpContext.Session.Clear();

            if (isAjaxCall)
            {
                filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                var data = new
                {
                    filterContext.Exception.Message,
                    filterContext.Exception.StackTrace
                };
                filterContext.Result = new JsonResult(data);
                filterContext.ExceptionHandled = true;
            }

            if (!isAjaxCall)
            {
                filterContext.Result = new RedirectResult("/Error/Error");
            }

            _logger.LogError(GetExceptionDetails(filterContext.Exception));

            filterContext.ExceptionHandled = true;
            base.OnException(filterContext);

        }
        
       private string GetExceptionDetails(Exception exception)
        {
            var properties = exception.GetType()
                .GetProperties();
            var fields = properties
                .Select(property => new
                {
                    Name = property.Name,
                    Value = property.GetValue(exception, null)
                })
                .Select(x => $"{x.Name} = {(x.Value != null ? x.Value.ToString() : String.Empty)}");
            return String.Join("\n", fields);
        }  
    }
// Register the filter in Startup.cs

public void ConfigureServices(IServiceCollection services)
  {
	services.AddControllers(options =>
	 {
	     options.Filters.Add<HandleExceptionFilter>();
	 });
  }

// Above .net 6

builder.Services.AddScoped<HandleExceptionFilter>();

ExceptionFilter