Handling Custom Exceptions with .NET Core Middleware

Home / Handling Custom Exceptions with .NET Core Middleware

A while back, I blogged about integrating client/server validation. I wanted to take this same concept a bit further with custom exception handling and .NET Core Middleware to make a more reusable delivery mechanism.


Using the same “RulesException” exception as described in the previous post, it is pretty easy to define a Middleware handler that will catch this exception whenever thrown. The .NET Core Middleware allows us to catch any unhandled exceptions that have bubbled up at any point in the request flow. You can see in the code below, that I’m catching specific exceptions, returning various error codes, and then writing a JSON output. The expectation, just like with the Angular “validationService” described in my previous blog post, the response will be parsed by the client and handled appropriately. You’ll also notice the addition of a CustomMessageException type which simply returns a message to the user. This will provide a convenient mechanism so avoid esoteric/cryptic messages being sent to the user (if all errors are handled).

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly JsonSerializerSettings _jsonSettings;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));

        _jsonSettings = new JsonSerializerSettings()
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            Formatting = Formatting.Indented,
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        };
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (RulesException re)
        {
            if (context.Response.HasStarted)
            {
                throw;
            }

            context.Response.StatusCode = 400;
            context.Response.Headers.Add("exception", "validationException");
            var modelState = new ModelStateDictionary();
            re.AddModelStateErrors(modelState);
            var json = JsonConvert.SerializeObject(modelState.Errors(true), _jsonSettings);
            await context.Response.WriteAsync(json);
        }
        catch (CustomMessageException cm)
        {
            if (context.Response.HasStarted)
            {
                throw;
            }

            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            context.Response.Headers.Add("exception", "messageException");
            var json = JsonConvert.SerializeObject(new { Message = cm.ExceptionMessage }, _jsonSettings);
            await context.Response.WriteAsync(json);
        }
    }
}

public static class CustomExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomExceptiontusMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomExceptionMiddleware>();
    }
}

With that defined, we can add it to the Middleware pipeline directly:

// If we're in a DEV environment, use the dev error pages
if (env.EnvironmentName.ToLowerTrim().StartsWith("dev"))
{
    app.UseDeveloperExceptionPage();
    app.UseCustomExceptiontusMiddleware();
    app.UseBrowserLink();
}
else
{
    app.UseCustomExceptiontusMiddleware();
    app.UseStatusCodePagesWithReExecute("/error/error{0}");
}

Testing that the Middleware is working as expected is pretty straight forward. Defining a Controller endpoint that simply throws lets use view the expected header and payload.

throw new RulesException("SomeProperty", "SomeProperty must be specified");
throw new CustomMessageException() { ExceptionMessage = "I couldn't execute your request due to bad search criteria." };

On the client, using a request interceptor, like in AngularJS or Angular, we can intercept the response and handle displaying the returned message(s) whenever an error occurs. This is a nice mechanism to keep things DRY. In this case, we can looking for specific status codes and headers and take action. Intercepting and handling the CustomMessageException would be like so:

// In our AngularJS application, push the interceptor onto the stack of interceptors
$httpProvider.interceptors.push('customMessageInterceptor');

(function () {
    var requestInterceptor = function ($q, $window, $timeout, notificationService) {
        var
            processResponse = function (response) {
                return response || $q.when(response);
            },
            processResponseError = function (rejection) {
                if (rejection.status == 500 && rejection.headers('exception') && rejection.headers('exception') === 'messageException') {
                    notificationService.error(rejection.data.message);
                }
                return $q.reject(rejection);
            };

        return {
            response: processResponse,
            responseError: processResponseError
        };
    };

    requestInterceptor.$inject = ['$q', '$window', '$timeout', 'notificationService'];
    angular.module('long2know.infrastructure')
        .factory('customMessageInterceptor', requestInterceptor);
})()

When we throw the exception to the client, the client will display a toast message with the JSON that was returned by the server:

This mechanism / Middleware would allow anyone to define a specific set of messages/criteria for displaying useful messages to the user.

The two exception classes/extensions are below for posterity’s sake.

public class CustomMessageException : Exception
{
    private string _exceptionMessage = string.Empty;

    public string ExceptionMessage { get { return _exceptionMessage; } set { _exceptionMessage = value; } }

    public CustomMessageException() : base() { }

    public CustomMessageException(string exceptionMessage) : base(exceptionMessage)
    {
        _exceptionMessage = exceptionMessage;
    }

    public CustomMessageException(string exceptionMessage, string message) : base(message)
    {
        _exceptionMessage = exceptionMessage;
    }

    public CustomMessageException(string exceptionMessage, string message, Exception innerException) : base(message, innerException)
    {
        _exceptionMessage = exceptionMessage;
    }
}

[Serializable]
public class RulesException : Exception
{
    private readonly List<ErrorInfo> _errors;

    public RulesException(string propertyName, string errorMessage, string prefix = "")
    {
        _errors = Errors;
        _errors.Add(new ErrorInfo($"{prefix}{propertyName}", errorMessage));
    }

    public RulesException(string propertyName, string errorMessage, object onObject, string prefix = "")
    {
        _errors = Errors;
        _errors.Add(new ErrorInfo($"{prefix}{propertyName}", errorMessage, onObject));
    }

    public RulesException()
    {
        _errors = Errors;
    }

    public RulesException(List<ErrorInfo> errors)
    {
        _errors = errors;
    }

    public List<ErrorInfo> Errors
    {
        get
        {
            return _errors ?? new List<ErrorInfo>();
        }
    }
}

public class ErrorInfo
{
    private readonly string _errorMessage;
    private readonly string _propertyName;
    private readonly object _onObject;

    public ErrorInfo(string propertyName, string errorMessage)
    {
        _propertyName = propertyName;
        _errorMessage = errorMessage;
        _onObject = null;
    }

    public ErrorInfo(string propertyName, string errorMessage, object onObject)
    {
        _propertyName = propertyName;
        _errorMessage = errorMessage;
        _onObject = onObject;
    }

    public string ErrorMessage
    {
        get
        {
            return _errorMessage;
        }
    }

    public string PropertyName
    {
        get
        {
            return _propertyName;
        }
    }
}

public static class RulesExceptionExtensions
{
    public static void AddModelStateErrors(this RulesException ex, ModelStateDictionary modelState)
    {
        foreach (var error in ex.Errors)
        {
            modelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }
    }

    public static void AddModelStateErrors(this IEnumerable<RulesException> errors, ModelStateDictionary modelState)
    {
        foreach (RulesException ex in errors)
        {
            ex.AddModelStateErrors(modelState);
        }
    }

    public static IEnumerable Errors(this ModelStateDictionary modelState)
    {
        if (!modelState.IsValid)
        {
            return modelState.ToDictionary(kvp => kvp.Key.ToCamelCase(),
                kvp => kvp.Value
                    .Errors
                    .Select(e => e.ErrorMessage).ToArray())
                    .Where(m => m.Value.Count() > 0);
        }
        return null;
    }

    public static IEnumerable Errors(this ModelStateDictionary modelState, bool fixName = false)
    {
        if (!modelState.IsValid)
        {
            return modelState.ToDictionary(kvp => fixName ? kvp.Key.Replace("model.", string.Empty).ToCamelCase() : kvp.Key.ToCamelCase(),
                kvp => kvp.Value
                    .Errors
                    .Select(e => string.IsNullOrWhiteSpace(e.ErrorMessage) ? "Invalid value entered." : e.ErrorMessage).ToArray())
                    .Where(m => m.Value.Count() > 0);
        }
        return null;
    }
}

Leave a Reply