简体   繁体   中英

Handling domain errors in MassTransit

I'm wondering how I should handle domain exceptions in a proper way?

Does all of my consumer's code should be wrapped into a try, catch block, or I should just thrown an Exception, which will be handled by apropriate FaultConsumer?

Consider this two samples:

Example-1 - whole operation is wrapped into try...catch block.

public async Task Consume(ConsumeContext<CreateOrder> context)
{         

try
{
  //Consumer that creates order 
  var order = new Order();
  var product = store.GetProduct(command.ProductId); // check if requested product exists

  if (product is null)
  {
    throw new DomainException(OperationCodes.ProductNotExist);
  }

  order.AddProduct(product);
  store.SaveOrder(order);

  context.Publish<OrderCreated>(new OrderCreated
  {
    OrderId = order.Id;
  });
}
catch (Exception exception)
{
  if (exception is DomainException domainException)
  {
    context.Publish<CreateOrderRejected>(new CreateOrderRejected
    {
      ErrorCode = domainException.Code;
    });
  }                
 }
}

Example-2 - MassTransit handles DomainException, by pushing message into CreateOrder_error queue. Another service subscribes to this event, and after the event is published on this particular queue, it process it;

public async Task Consume(ConsumeContext<CreateOrder> context)
{         

  //Consumer that creates order 
  var order = new Order();
  var product = store.GetProduct(command.ProductId); // check if requested product exists

  if (product is null)
  {
    throw new DomainException(OperationCodes.ProductNotExist);
  }

  order.AddProduct(product);
  store.SaveOrder(order);

  context.Publish<OrderCreated>(new OrderCreated
  {
    OrderId = order.Id;
  });
}

Which approach should be better?

I know that I can use Request/Response and gets information about error immediately, but in my case, it must be done via message broker.

In your first example, you are handling a domain condition (in your example, a product not existing in the catalog) by producing an event that the order was rejected for an unknown product. This makes complete sense.

Now, if the database query to check the product couldn't connect to the database, that's a temporary situation that may resolve itself, and thus using a retry or scheduled redelivery makes sense - to try again before giving up entirely. Those are exceptions you would want to throw.

But the business exception you'd want to catch, and handle by publishing an event.

public async Task Consume (ConsumeContext<CreateOrder> context) {

  try {
    var order = new Order ();
    var product = store.GetProduct (command.ProductId); // check if requested product exists
    if (product is null) {
      throw new DomainException (OperationCodes.ProductNotExist);
    }

    order.AddProduct (product);
    store.SaveOrder (order);

    context.Publish<OrderCreated> (new OrderCreated {
      OrderId = order.Id;
    });
  } catch (DomainException exception) {
    await context.Publish<CreateOrderRejected> (new CreateOrderRejected {
      ErrorCode = domainException.Code;
    });
  }
}

My take on this is that you seem to go to the fire-and-forget commands mess. Of course, it is very context-specific, since there are scenarios, especially integration when you don't have a user on the other side sitting and wondering if their command was eventually executed and what is the outcome.

So, for integration scenarios, I concur with Chris' answer, publishing a domain exception event makes perfect sense.

For the user-interaction scenarios, however, I'd rather suggest using request-response that can return different kinds of response, like a positive and negative response, as described in the documentation . Here is the snippet from the docs:

Service side:

public class CheckOrderStatusConsumer : 
    IConsumer<CheckOrderStatus>
{
    public async Task Consume(ConsumeContext<CheckOrderStatus> context)
    {
        var order = await _orderRepository.Get(context.Message.OrderId);
        if (order == null)
            await context.RespondAsync<OrderNotFound>(context.Message);
        else        
            await context.RespondAsync<OrderStatusResult>(new 
            {
                OrderId = order.Id,
                order.Timestamp,
                order.StatusCode,
                order.StatusText
            });
    }
}

Client side:

var (statusResponse,notFoundResponse) = await client.GetResponse<OrderStatusResult, OrderNotFound>(new { OrderId = id});
// both tuple values are Task<Response<T>>, need to find out which one completed
if(statusResponse.IsCompletedSuccessfully)
{
    var orderStatus = await statusResponse;
    // do something
}
else
{
    var notFound = await notFoundResponse;
    // do something else
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM