Source code
.NET Core C# Diagnostics Visual Studio

Centralized Logging and Exception Handling in a .NET Core Application

Welcome to today’s post.

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

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.

In the application pipeline IAppBuilder, we will be using the middleware extensions for 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();
  });
  …
}

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 sanitise 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. 

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

That’s all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial