Search code examples
c#asp.netasp.net-web-apimediatr

MediatR: Global exception handler is being executed multiple times


I am implementing CQRS in my ASP.NET Web API using MediatR. To catch and log exceptions to the database, I want to use a global exception handler:

public class GlobalRequestExceptionHandler<TRequest, TResponse, TException>(IMediator mediator, ILoggerService loggerService) : IRequestExceptionHandler<TRequest, TResponse, TException>
    where TRequest : notnull
    where TException : Exception
{
    private readonly IMediator _mediator = mediator;
    private readonly ILoggerService _loggerService = loggerService;

    public async Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state, CancellationToken cancellationToken)
    {
        try
        {
            if (IsLogSystemErrorCommandException(request))
                return;

            var systemError = SystemError.Create(request.GetType().Name, exception.GetType().Name, exception.Message, exception.StackTrace, VentureDomain.Common, VentureModule.Common);
            var command = new LogSystemErrorCommand(systemError);
            var result = await _mediator.Send(command, cancellationToken);
        }
        catch (Exception ex)
        {
            _loggerService.Log(ex.Message, LogLevel.Error);
        }
    }

    private bool IsLogSystemErrorCommandException(object? errorObject)
    {
        return errorObject?.GetType() == typeof(LogSystemErrorCommand);
    }
}

Error logging command for reference:

public class LogSystemErrorCommandHandler(DbContext context, IResultService resultService) : IRequestHandler<LogSystemErrorCommand, Result>
{
    private readonly DbContext _context = context;
    private readonly IResultService _resultService = resultService;

    public async Task<Result> Handle(LogSystemErrorCommand request, CancellationToken cancellationToken)
    {
        _context.Add(request.SystemError);
        await _context.SaveChangesAsync(cancellationToken);

        return _resultService.CreateSuccess();
    }
}

However that handler is executed 3/4 times whenever an exception in the original request handler for a command or a query is thrown, meaning I log 3/4 system error logs into my database.

I have read about other handlers executing multiple times but I don't see an error in my service registration, like some posts might suggest.

I use a simple extension method for this:

public static class RequestRegistration
{
    public static void AddSystemSurveillanceRequestHandlers(this MediatRServiceConfiguration configuration)
    {
        configuration.RegisterServicesFromAssembly(typeof(RequestRegistration).Assembly);
    }
}

Edit:

I should add that the callstack is the same everytime the exception handler is called. The original request handler is only running once. Not storing the error log to the database doesnt prevent the exception handler from being called multiple times either.


Solution

  • Turns out I missed something crucial

    When handling an exception using an IRequestExceptionHandler you need to set the RequestExceptionHandlerState<TResponse> to handled using SetHandled(TResponse).

    You can adjust the implementation from above like like this:

    public async Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state, CancellationToken cancellationToken)
    {
        try
        {
            if (IsLogSystemErrorCommandException(request))
                return;
    
            var systemError = SystemError.Create(request.GetType().Name, exception.GetType().Name, exception.Message, exception.StackTrace, VentureDomain.Common, VentureModule.Common);
            var logSystemErrorCommand = new LogSystemErrorCommand(systemError);
            var logSystemErrorResult = await _mediator.Send(command, cancellationToken);
    
            state.SetHandled(null!);
        }
        catch (Exception ex)
        {
            _loggerService.Log(ex.Message, LogLevel.Error);
        }
    }
    

    Note that used added null! as a parameter while the method actually expects a non-null value, however if you step into the source code you can see that the TResponse? property in the state is nullable by itself and the .SetState() method really just passes your value along. Its not the best solution but should be fine if there arent any other solutions to actually using a proper value.

    Using a Result-Pattern like approach

    I wrap the data I return in result records like these ones:

    public record Result(ResultCode Code, object? Message);
    
    public record Result<TData>(ResultCode Code, object? Message, TData? Data) : Result(Code, Message);
    

    Thats why I chose to try to instantiate a proper result representing that an exception occurred during the request:

    public class GlobalRequestExceptionHandler<TRequest, TResponse, TException>(IMediator mediator, ILoggerService loggerService) : IRequestExceptionHandler<TRequest, TResponse, TException>
        where TRequest : notnull
        where TException : Exception
    {
        private readonly IMediator _mediator = mediator;
        private readonly ILoggerService _loggerService = loggerService;
    
        public async Task Handle(TRequest request, TException exception, RequestExceptionHandlerState<TResponse> state, CancellationToken cancellationToken)
        {
            try
            {
                if (IsLogSystemErrorCommandException(request))
                    return;
    
                var requestName = request.GetType().Name;
                var systemError = SystemError.Create(requestName, exception.GetType().Name, exception.Message, exception.StackTrace, VentureDomain.Common, VentureModule.Common);
                var logSystemErrorCommand = new LogSystemErrorCommand(systemError);
                var logSystemErrorResult = await _mediator.Send(logSystemErrorCommand, cancellationToken);
    
                var exceptionResult = GetExceptionResult(requestName);
                state.SetHandled(exceptionResult!);
            }
            catch (Exception ex)
            {
                _loggerService.Log(ex.Message, LogLevel.Error);
            }
        }
    
        private bool IsLogSystemErrorCommandException(object? errorObject)
        {
            return errorObject?.GetType() == typeof(LogSystemErrorCommand);
        }
    
        private TResponse? GetExceptionResult(string requestName)
        {
            var resultType = typeof(TResponse);
    
            if (resultType == typeof(Result))
                return (TResponse?)Activator.CreateInstance(typeof(TResponse), ResultCode.Exception, requestName);
            else if (IsResultWithGenericData(resultType))
                return (TResponse?)Activator.CreateInstance(typeof(TResponse), ResultCode.Exception, requestName, null);
    
            return default;
        }
    
        private bool IsResultWithGenericData(Type resultType)
        {
            var genericDataResultType = typeof(Result<>);
            var typeArguments = resultType.GetGenericArguments();
    
            if (typeArguments.Length == 1)
            {
                var genericTypeDefinition = resultType.GetGenericTypeDefinition();
    
                return genericTypeDefinition == genericDataResultType;
            }
    
            return false;
        }
    }