Source code
.NET .NET Core C# Diagnostics Visual Studio

Centralized Logging and Exception Handling in a .NET Core Application

Welcome to today’s post.

In today’s post I will be discussing how to use error handling and diagnostics logging middleware to trap and process unhandled errors from a .NET Core API.

One useful tactic for handling unhandled errors within an application is to create a centralized error handling method or delegate that can trap the exception and perform a useful set of operations. One of these operations can include logging. The mechanism of central control within an ASP.NET Core application is to utilize the middleware extensions. Later in this post, I will show how to use one of these middleware extension methods, UseExceptionHandler() to intercept and process unhandled exception errors. The use of a centralized method helps us avoid having to implement separate exception blocks to handle general exceptions in every one of our application service methods and keep the code cleaner.

Explaining the Logging Middleware Extension

In a previous post I discussed how to use extension methods within .NET Core applications.

In the application pipeline IAppBuilder, we will be using the middleware extension methods for logging. The extension methods for logging allow us to configure and enable the following event logging providers:

Console logging.

Debugging logging.

Event logging.

Enabling the logging extensions for console and debugging is done as follows:

using Microsoft.Extensions.Logging;
…

public void ConfigureServices(IServiceCollection services)
{
  services.AddLogging(config =>
  {
    config.AddConsole(opts =>
    {
      opts.IncludeScopes = true;
    });
    config.AddDebug();
  });
  …
}

The Console logging provider option, IncludeScopes allows us to wrap a set of logs within a disposable transaction block. The logging within the scope ends once the block is disposed.   

To be able to intercept unhandled errors in our Web API .NET Core application, we can use the UseExceptionHandler() extension method to delegate our application error, filter the error, then output the error response is a more user-friendly way to the caller.

A general exception handler is used in the production environment where we wish to sanitize the error instead of displaying the full error and stack trace, which might contain sensitive details.

The general exception handler (as a lambda function) can be used within the Configure() method of startup.cs. In the handler we output the error in the HTTP response and log the error as shown:

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";

            var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
            if (contextFeature != null)
            {
                var errorDetails = new ErrorDetail()
                {
                    StatusCode = context.Response.StatusCode,
                    Message = "Internal Server Error."
                };
                await context.Response.WriteAsync(  
  		            JsonConvert.SerializeObject(errorDetails));

                logger.LogError(String.Format("Stacktrace of error: {0}",   		
                    contextFeature.Error.StackTrace.ToString()));
            }
        });
    });
    app.UseHsts();
}

To make the code cleaner, we move the error handler into its own custom extension method, which is the error handler. 

Custom Exception Handling Middleware

We implement a custom.NET Core middleware extension class as follows:

using GeneralErrorHandling.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Net;

namespace GeneralErrorHandler.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILogger logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = 500;
                    context.Response.ContentType = "application/json";

                    var contextFeature = context.Features
                        .Get<IExceptionHandlerFeature>();
                    
                    if (contextFeature != null)
                    { 
                        var errorDetails = new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error."
                        };
                        await context.Response
                            .WriteAsync(JsonConvert.SerializeObject(errorDetails));
                    }
                    logger.LogError(String.Format(
 		                "Stacktrace of error: {0}", 
                        contextFeature.Error.StackTrace.ToString()));
                });
            });
        }
    }
}

We also include a model to encapsulate the status code and error message:

namespace GeneralErrorHandler.Models
{
    public class ErrorDetail
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }

    }
}

The exception handling extension method does the following:

  1. Runs the request delegate asynchronously.
  2. Retrieves the feature of the http response context.
  3. Generate the error details.
  4. Output the error details as a string response to the HTTP response.
  5. Output the error stack trace to the log stream.

Note: The Run() extension method is a request delegate handler (from Microsoft.AspNetCore.Http.Abstractions):

public static void Run(this IApplicationBuilder app, RequestDelegate handler);

and the request delegate (from Microsoft.AspNetCore.Http) is invoked with the current HTTP request:

public delegate Task RequestDelegate(HttpContext context);

Note: We can further encapsulate the middleware delegate action block app.Run(a => { … })  by creating a per-request middleware delegate class:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace MiddlewareExtension
{
    public class CustomRequestMiddleware
    {
        private readonly RequestDelegate _next;

        public CustomRequestMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
	…
            await _next(context);
        }
    }
}

To be able to use the error handler, we configure the new middleware extension method ConfigureExceptionHandler() from the startup.cs as shown:

using GeneralErrorHandler.Extensions;

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
  	ILogger<Startup> logger) 
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.ConfigureExceptionHandler(logger);
    ...
}

We have seen from the above how to implement centralized logging and exception handling within an ASP.NET Core application. This has shown us how to delegate unhandled exceptions from any part of our application to a centralized point where we can analyze the errors and log them for further troubleshooting and analysis. In addition, a centralized handler for unhandled errors keeps our code tidier and more maintainable.

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial