asp.netasp.net-core.net-coremediatormediatr

Return meaningful errors in API using CQRS pattern


When using the Mediatr pattern I find quite challenging to return meaningful errors to an API controller. Let's take the OrdersController.CancelOrder method as an example (src).

In this example, they "only" return Ok() and BadRequest(). In that case how will they return errors like "This orderid does not exist" (404) OR "this order has been shipped" (400) (...).

We could introduce a new class called Result holding both the returned values (if any) and potentially error messages. In that case, all your commands, queries should return Result<YourModel>. We could also add the code directly inside the controller. I cannot make up my mind both solutions have pros and cons.

What do you think about that?

Thx Seb


Solution

  • That's exactly how I tend to do it using Mediatr.
    Return a wrapper class.

    If we take the eShopOnContainers example CancelOrder example, I would have the command, return a CancelOrderCommandResult

    public class CancelOrderCommand : IRequest<CancelOrderCommandResult>
    { }
    

    The CancelOrderCommandResult could be something along these lines:

    public class CancelOrderCommandResult
    {
        public CancelOrderCommandResult(IEnumerable<Error> errors)
        {
            Success = false;
            Errors = errors;
        }
    
        public CancelOrderCommandResult(bool success)
        {
            Success = success;
        }
    
        public bool Success {get; set;}
    
        public IEnumerable<Error> Errors {get; set;}
    }
    

    I've omitted the Error class, but it could just be POCO containing the error information, error code etc...

    Our handler then becomes

    public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderCommandResult>
    {
        private readonly IOrderRepository _orderRepository;
    
        public CancelOrderCommandHandler(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }
    
        public async Task<CancelOrderCommandResult> Handle(CancelOrderCommand command, CancellationToken cancellationToken)
        {
            var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);
    
            if(orderToUpdate == null)
            {
                return new CancelOrderCommandResult(false);
            }
    
            try 
            {
                orderToUpdate.SetCancelledStatus();
                await _orderRepository.UnitOfWork.SaveEntitiesAsync();
    
                //iff success, return true
                return new CancelOrderCommandResult(true);
            }
            catch (Exception ex)
            {
                var errors = MapErrorsFromException(ex);
                return new CancelOrderCommandResult(errors)
            }
        }
    }
    

    Again, MapErrorsFromException is omitted for brevity, but you could even inject this as a dependency.

    In your controller, when you call _mediator.Send you now get back the CancelOrderCommandResult - and if .Success is true, return a 200 as before.

    Otherwise, you have a collection of errors - with which you can make some decisions about what to return - a 400, 500, etc...