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