W
W
WhiteNinja2022-02-17 20:08:19
ASP.NET
WhiteNinja, 2022-02-17 20:08:19

How to properly organize Exception Handling in a business application?

Good afternoon!

There is a business application (.NET) that consists of several layers, for example:

  • Utils - Cross cutting concerns
  • Domain - Entities, DomainServices
  • Infrastructure - DB, External API, SmsService, etc.
  • Application - Application Logic, UseCases, ApplicationServices
  • WebUI - Presentation Layer, ASP.NET Core MVC


In the Domain (DomainServices) and Application (UseCases and ApplicationServices) layers, exceptions can occur either related to business logic or application logic.
Previously, such exceptions looked something like this (for example, in UseCases):

public class CreateOrderCommand : IRequest
{
  ...
  public PaymentType PaymentType { get; set; }
  public string CountryCode { get; set; }
  ...
}

internal class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
  private readonly IDbContext _dbContext;
  
  public CreateOrderCommandHandler(IDbContext dbContext)
  {
    _dbContext = dbContext;
  }
  
  public async Task<int> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
  {
    ...
    
    if (command.PaymentType == PaymentType.PayPal && command.CountryCode == "RU")
    {
      throw new AppException(Messages.InvalidPaymentType);
    }

    ...
  }
}


Where

public class AppException : Exception
{
  public class AppException(string message) : base(message)
  {}
}


And Messages.InvalidPaymentType is a localized string from the resource file (i.e. the value depends on the CultureInfo.CurrentCulture set at the time of the HttpRequest on the WebUI...).

The advantages of this approach are that on the WebUI layer it is enough to write a HandleErrorAttribute , which will contain lines like this:

public class HandleErrorAttribute : TypeFilterAttribute
{
  ....
  
  public void OnException(ExceptionContext context)
  {
    if (context.ExceptionHandled)
      return;
    
    ...
    
    if (context.HttpContext.Request.IsAjaxRequest())
      HandleAjaxRequestException(context);
    else
      HandleRequestException(context);

    context.ExceptionHandled = true;
  }
  
  private void HandleAjaxRequestException(ExceptionContext context)
  {
    string message = Messages.Error500;
    if (context.Exception is AppException)
    {
      message = context.Exception.Message; // Используем текст исключения заданный на Domain/Application слоях
    }

    context.Result = new JsonResult(new { Ok = false, Message =  message});
  }
}


Thus, we dragged the text of the business exception to the output as a result of an Ajax request, for example.

Obviously, the disadvantages of this approach are:

No typing of exceptions, i.e. they cannot be intercepted and processed, they are all AppException ;
When writing exception text, for example, to a log, the text will be localized and language dependent in the user interface.

And these shortcomings are very significant, so refactoring is required.

Accordingly, I see the following solution to the problem:

A separate class is created for each business exception. For the example above, an InvalidPaymentTypeException class will be generated and the code will be rewritten as:

public class InvalidPaymentTypeException : Exception
{
  public InvalidPaymentTypeException(PaymentType paymentType, string countryCode)
    : base($"Invalid payment type \"{paymentType}\" for country \"{countryCode}\".")
  {}
}


internal class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
  ...

  public async Task<int> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
  {
    ...
    
    if (command.PaymentType == PaymentType.PayPal && command.CountryCode == "RU")
    {
      throw new InvalidPaymentTypeException(command.PaymentType, command.CountryCode);
    }

    ...
  }
}


But then on the WebUI layer it will be necessary to iterate over all types of exceptions and replace them with localized messages. Something like:

public class HandleErrorAttribute : TypeFilterAttribute
{
  ....
  
  private void HandleAjaxRequestException(ExceptionContext context)
  {
    string message = Messages.Error500;
    if (context.Exception is InvalidPaymentTypeException)
    {
      message = Messages.InvalidPaymentType; // Локализованный текст из файла ресурсов
    }
    else if (context.Exception is InvalidCredentialsException)
    {
      message = Messages.InvalidCredentials; // Локализованный текст из файла ресурсов
    }
    // И так все исключения
    else {
      message = Messages.Error500; // Внутренняя ошибка
    }

    context.Result = new JsonResult(new { Ok = false, Message =  message});
  }
}


But then, if a new exception appears, for example, OrderNotFoundException , you must definitely remember to add it to the else if/switch to localize the error. And it's pretty inconvenient.

Another option is to create an abstract LocalizedException , which in turn stores the error message for developers in the Message property, and the localized error message from the resource file in the LocalizedMessage property. More or less like this:

public abstract class LocalizedException : Exception
{
  public string LocalizedMessage { get; }
  
  protected LocalizedException(string message, string localizedMessage)
    : base(message)
  {
    LocalizedMessage = localizedMessage;
  }
}


And all-all business exceptions are inherited from this exception, as follows:

public class InvalidPaymentTypeException : LocalizedException
{
  public InvalidPaymentTypeException(PaymentType paymentType, string countryCode)
    : base(
      message: $"Invalid payment type \"{paymentType}\" for country \"{countryCode}\".", 
      localizedMessage: Messages.InvaidPaymentType // Вторым параметром передаем локализованную строку из файла ресурсов
    ) 
  {}
}


And on the WebUI layer, we always use the LocalizedMessage property .

...

private void HandleAjaxRequestException(ExceptionContext context)
{
  string message = Messages.Error500;
  if (context.Exception is LocalizedException)
  {
    message = context.Exception.LocalizedMessage;
  }

  context.Result = new JsonResult(new { Ok = false, Message =  message});
}

...


Question:

Are the options described by me suitable for organizing Exception Handling (maybe I'm missing something)?
Or I ask you to suggest the best-practices for throwing (on the inner layers) and intercepting (on the presentation layer) business exceptions?


Please note that the presentation layer is exactly the UI, not the API, since the UI has a feature - the interface language.

Thanks for the help and advice!

Answer the question

In order to leave comments, you need to log in

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question