Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception-Handling Middleware and Page

I'm new to the concept of middleware, and am currently struggling with the proper way to handle exceptions in my MVC Core project.

What I want to happen is for the Exception to be caught, logged, and then send the user to a friendly error page with a message. At first, I was trying to manage all of these within the middleware, but realized I probably wasn't doing it correctly.

So if I want this flow to happen, should I use both my Exception-logging middleware AND the app.UseExceptionHandler("/Error") so that the middleware re-throws the exception to the page? And if so, how do I get the exception details in the Error page? And is it correct that the exception-handling mechanism I want to intercept first should be the last one in Configure?

All the examples I've found deal strictly with HTTP Status Code errors like 404; I'm looking to handle actual Exceptions (and their sub-classes). That way, in my View pages I can throw my own Exceptions where applicable (for example, if a View is provided a null for a mandatory field.)

Startup.cs snippet:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory, LoanDbContext context) {
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseStatusCodePages();
    if (env.IsDevelopment() || env.IsEnvironment("qa")) {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    } else {
        app.UseExceptionHandler("/Error");
    }
    app.UseMiddleware<MyExceptionHandler>(loggerFactory);
    // ... rest of Configure is irrelevant to this issue

MyExceptionHandler.cs

using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace MyCompany.MyProject.Helpers
{
    /// <summary>
    /// A custom Exception Handler Middleware which can be re-used in other projects.
    /// </summary>
    public sealed class MyExceptionHandler
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

        public MyExceptionHandler(RequestDelegate next, ILoggerFactory loggerFactory) {
            _next = next;
            _logger = loggerFactory.CreateLogger<MyExceptionHandler>();
        }

        public async Task Invoke(HttpContext context) {
            try {
                await _next(context);
            } catch (Exception ex) {
                HandleException(ex);
            }
        }

        // I realize this function looks pointless, but it will have more meat to it eventually.
        public void HandleException(Exception ex) {
            if (ex is ArgumentException argEx) {
                _logger.LogError(0, argEx, argEx.Message);
            } else if (ex is InvalidOperationException ioEx) {
                _logger.LogError(0, ioEx, "An Invalid Operation Exception occurred. This is usually caused by a database call that expects "
                    + "one result, but receives none or more than one.");
            } else if (ex is SqlException sqlEx) {
                _logger.LogError(0, sqlEx, $"A SQL database exception occurred. Error Number {sqlEx.Number}");
            } else if (ex is NullReferenceException nullEx) {
                _logger.LogError(0, nullEx, $"A Null Reference Exception occurred. Source: {nullEx.Source}.");
            } else if (ex is DbUpdateConcurrencyException dbEx) {
                _logger.LogError(0, dbEx, "A database error occurred while trying to update your item. This is usually due to someone else modifying the item since you loaded it.");
            } else {
                _logger.LogError(0, ex, "An unhandled exception has occurred.")
            }
        }
    }
}
like image 800
Andrew S Avatar asked Jun 27 '17 22:06

Andrew S


Video Answer


1 Answers

should I use both my Exception-logging middleware AND the app.UseExceptionHandler("/Error") so that the middleware re-throws the exception to the page?

Yes.

Using only a snippet of your example.

public async Task Invoke(HttpContext context) {
    try {
        await _next(context);
    } catch (Exception ex) {
        HandleException(ex);
         // re -throw the original exception
         // after logging the information
        throw;
    }
}

The above will re-throw the original error after logging it so that the other handler in the pipeline will catch it and do the out of the box handling.

Source Error Handling in ASP.NET Core

how do I get the exception details in the Error page?

Use IExceptionHandlerPathFeature to get the exception and the Path

public IActionResult Error()
{
    // Get the details of the exception that occurred
    var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

    if (exceptionFeature != null)
    {
        // Get which route the exception occurred at
        string routeWhereExceptionOccurred = exceptionFeature.Path;

        // Get the exception that occurred
        Exception exceptionThatOccurred = exceptionFeature.Error;

        // TODO: Do something with the exception
        // Log it with Serilog?
        // Send an e-mail, text, fax, or carrier pidgeon?  Maybe all of the above?
        // Whatever you do, be careful to catch any exceptions, otherwise you'll end up with a blank page and throwing a 500
    }

    return View();
}

Source Adding Global Error Handling and Logging in ASP.NET Core with IExceptionHandlerPathFeature

In the above example they mention doing the logging in the view but you would have already done that in your custom handler.

Pay particular attention to this little comment when rendering your error views.

Whatever you do, be careful to catch any exceptions, otherwise you'll end up with a blank page and throwing a 500

By plugging into the pipeline you avoid re-inventing features already provided out of the box by the framework and also manage cross-cutting concerns.

like image 67
Nkosi Avatar answered Oct 22 '22 06:10

Nkosi