Open Closed

AbpValidationException in Razor Page handler #6146


User avatar
0
russell.thompson created
  • ABP Framework version: v7.2.2
  • UI Type: MVC
  • Database System: EF Core (SQL Server)
  • Tiered (for MVC) or Auth Server Separated (for Angular): no

In our AppService methods like CreateAsync and UpdateAsync we have custom logic that may throw an AbpValidationException. We also have fluent validation logic on Domain entities that may throw an AbpValidationException where we explicitly validate the entity after mapping using IObjectValidator.ValidateAsync. Here is an example UpdateAsync method showing the common pattern we are using:

public async Task UpdateAsync(Guid id, UpdateCustomerDto input)
{
    var entity = await _customerRepository.GetAsync(id);

    entity = ObjectMapper.Map<UpdateCustomerDto, Customer>(input, entity);

    entity = await _customerManager.UpdateAsync(
        entity
    );

    // Validate the entity.
    await _objectValidator.ValidateAsync(entity);

    await _customerRepository.UpdateAsync(entity);
}

When we call this AppService method via REST api we get the expected behavior. Any AbpValidationException causes a 400 and shows the validation messages in the api JSON response. However, we are experiencing an issue in our Razor Page UI that injects this AppService and calls UpdateAsync in a Post page handler.

In our Razor Page, the user experience we want when an AbpValidationException is thrown should be the same as a traditional ModelState validation error. The default behavior shows the user a 400 error page. We have tried catching the AbpValidationException in our page handler and adding the errors to the ModelState. This gives us the desired UI behavior but has other side effects. Here is an example page handler implementation.

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    try
    {
        await _customerAppService.UpdateAsync(CustomerGuid, EditCustomer);
    }
    catch (AbpValidationException ex)
    {
        foreach (var error in ex.ValidationErrors)
        {
            ModelState.AddModelError($"EditCustomer.{error.MemberNames.FirstOrDefault()}", error.ErrorMessage);
        }

        return Page();
    }

    return RedirectToPage("Index");
}

The problem we are experiencing with this implementation is that even though the exception was thrown, because we catch it, the changes are still being saved to the database. This is being caused because we are mapping to the domain entity then validating it. At this point we are stuck on how to get our desired user experience without allowing the entity to be incorrectly saved.

What guidance or advise do you have on how to properly implement our use case?


7 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    You can consider call the UnitOfWorkManager.Current.RollbackAsync(); method.

  • User Avatar
    0
    russell.thompson created

    Ok, I thought we might be overlooking a more automatic way to take care of it. If our fluent validation rules were also on the Dto and not just the domain object would the app service automatic validation also apply those rules? Is duplicating the rules from the domain in that way recommended so we don't have to explicitly call await _objectValidator.ValidateAsync(entity);?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    If our fluent validation rules were also on the Dto and not just the domain object would the app service automatic validation also apply those rules? Is duplicating the rules from the domain in that way recommended so we don't have to explicitly call await _objectValidator.ValidateAsync(entity);?

    Yes, and validate DTO is the recommended way.

    ABP will automatically find this class and associate with the CreateUpdateBookDto on object validation.

    https://docs.abp.io/en/abp/latest/FluentValidation#using-the-fluentvalidation

  • User Avatar
    0
    russell.thompson created

    Thank you. I have one more follow-up related to this. In our Domain Managers we confirm that key references to other entities are valid. In some cases these are loose relationships to entities defined in other modules. For example, a Customer may have a SalesRepId. Currently we verify these are valid Ids in our domain manager CreateAsync and UpdateAsync using repositories for entities in the same module and app services for those in other modules and throw an exception if they are not. Is there a recommended approach to this?

    We specifically used this method of implementation because the Validation documentation recommends not to add this type of validation on the Dto. https://docs.abp.io/en/abp/latest/Validation#resolving-a-service

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Yes, you shouldn't validate on the Dto.

    We recommend validating on domain service: https://docs.abp.io/en/abp/latest/Domain-Services#application-services-vs-domain-services

  • User Avatar
    0
    russell.thompson created

    Ok, that is how we are doing it. If the related keys are not valid do you typically throw a BusinessException or a AbpValidationException? We originally implemented custom BusinessException classes but are thinking of swapping to an AbpValidationException for consistency with the other errors.

  • User Avatar
    0
    jfistelmann created

    both do the job.

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11