【问题标题】:How best to handle data fetching needed for FluentValidation如何最好地处理 FluentValidation 所需的数据获取
【发布时间】:2020-04-01 16:44:36
【问题描述】:

在我正在开发的应用程序中,我使用 Mediatr 及其管道来处理数据库交互、一些次要业务逻辑、验证等。

我可以在管道中处理访问控制之类的一些检查,因为我使用此处描述的上下文对象https://jimmybogard.com/sharing-context-in-mediatr-pipelines/ 从 ASP.Net 身份转到具有用户信息和声明的自定义上下文对象.

我遇到的一个问题是,由于这个应用程序是多租户的,我需要确保即使一个对象存在,它也属于那个租户,唯一能确保这一点的方法就是抓取该对象从数据库中检查它。在我看来,验证不应该有副作用,所以我不想依赖它来填充上下文对象。但这会将一堆验证推入 Mediatr 处理程序,因为它们检查对象是否存在等等,从而导致大量重复代码。我真的不想多次查询数据库,因为有些查询可能很昂贵。

在实际请求处理程序中进行更复杂的验证的另一个问题是消除本质上的验证错误。目前,如果其中一项检查失败,我会抛出一个ValidationException,然后它会被中间件捕获并变成一个ProblemDetails,并返回给 API 调用者。这基本上是流控制的异常,无论如何验证失败都不是“异常”。

我对如何解决这个问题的想法是:

  1. 在管道中的某处,当我构建上下文时,包括尝试从数据库中获取所需的对象。如果其中任何一个为空,则验证将失败。这似乎会使测试变得更加困难,并且需要以某种方式装饰请求(或使用反射),以便管道可以知道尝试加载这些对象。

  2. 在验证器中有查询,但使用某种缓存感知存储库,因此当稍后查询相同的对象时,它是从缓存中提供的,而不是从数据库中提供的。处理程序还将使用此缓存感知存储库(当前处理程序直接与 EF Core DbContext 交互以进行查询)。然后,这增加了缓存失效的问题,无论如何我将不得不在某个时候处理(相当多的项目很少修改)。为了测试,可以注入一个实际上不缓存任何东西的虚拟缓存对象。

  3. 使来自请求的所有响应实现一个接口(或扩展一个抽象类),该接口具有验证信息、一般成功标志等。这可以通过 API 直接返回,或者具有一些转换失败的管道进入ProblemDetails。这将为每个响应和处理程序添加一些样板,但避免了流控制等异常以及其他选项中的缓存/反射问题。

假设 1 和 2 任何种类的竞争条件都不是问题。对象不会更改所有者,并且出于审计/会计目的而实际上很少从数据库中删除事物。

我知道没有真正的一刀切可以解决此类问题,但我想知道是否还有其他我遗漏的选项,或者任何具有类似管道的人在使用时遇到的任何长期可维护性问题这些列出的选项。

【问题讨论】:

    标签: asp.net-core fluentvalidation mediatr


    【解决方案1】:

    我们使用 MediatR IRequestPreProcessor 在 RequestHandler 和 FluentValidation 验证器中获取我们需要的数据。

    请求预处理器:

        public interface IProductByIdBinder
        {
            int ProductId { get; }
            ProductEntity Product { set; }
        }
    
        public class ProductByIdBinder<T> : IRequestPreProcessor<T> where T : IProductByIdBinder
        {
            private readonly IRepositoryReadAsync<ProductEntity> productRepository;
    
            public ProductByIdBinder(IRepositoryReadAsync<ProductEntity> productRepository)
            {
                this.productRepository = productRepository;
            }
    
            public async Task Process(T request, CancellationToken cancellationToken)
            {
                request.Product = await productRepository.GetAsync(request.ProductId);
            }
        }
    

    请求处理程序:

     public class ProductDeleteCommand : IRequest, IProductByIdBinder
        {
            public ProductDeleteCommand(int id)
            {
                ProductId = id;
            }
    
            public int ProductId { get; }
            public ProductEntity Product { get; set; }
    
            private class ProductDeleteCommandHandler : IRequestHandler<ProductDeleteCommand>
            {
                private readonly IRepositoryAsync<ProductEntity> productRepository;
    
                public ProductDeleteCommandHandler(
                    IRepositoryAsync<ProductEntity> productRepository)
                {
                    this.productRepository = productRepository;
                }
                
                public Task<Unit> Handle(ProductDeleteCommand request, CancellationToken cancellationToken)
                {
                    productRepository.Delete(request.Product);
                    
                    return Unit.Task;
                }
            }
        }
    

    FluentValidation 验证器:

     public class ProductDeleteCommandValidator : AbstractValidator<ProductDeleteCommand>
        {
            public ProductDeleteCommandValidator()
            {
                RuleFor(cmd => cmd)
                    .Must(cmd => cmd.Product != null)
                    .WithMessage(cmd => $"The product with id {cmd.ProductId} doesn't exist.");
            }
        }
    

    【讨论】:

      【解决方案2】:

      我认为在处理程序层处理业务逻辑验证没有任何问题。 而且,我认为给它们抛出异常是不对的,正如你所说的,它是作为流控制的异常。

      对于用例来说,引入缓存似乎也有点过头了。最合理的选择是第三个恕我直言。

      您可以使用漂亮的OneOf 库而不是实现接口,并拥有类似的东西

          using HandlerResponse = OneOf<Success, NotFound, ValidationResponse>;
      
          public class MediatorHandler : IRequestHandler<Command, HandlerResponse>
          {
             public async Task<HandlerResponse> Handle(
              Command command,
              CancellationToken cancellationToken)
          {
              Resource resource = await _userRepository
                  .GetResource(command.Id);
      
              if (resource is null)
                  return new NotFound();
      
              if (!resource.IsValid)
                  return new ValidationResponse(new ProblemDetails());
      
              return new Success();
          }
      

      然后将其映射到您的 API 层中,例如

          public async Task<IActionResult> PostAsync([FromBody] DummyRequest request)
          {
              HandlerResponse response = await _mediator.Send(
                  new Command(request.Id));
      
              return response.Match<IActionResult>(
                  success => Created(),
                  notFound => NotFound(),
                  failed => new UnprocessableEntityResult(failed.ProblemDetails))
              );
          }
      

      【讨论】:

      • 我不知道 OneOf 库。有区别的联合是我在 TypeScript 中越来越喜欢而在 C# 中错过的东西。我会试一试,看看它有多合适。
      猜你喜欢
      • 1970-01-01
      • 2021-10-08
      • 2010-10-01
      • 1970-01-01
      • 2020-10-03
      • 2016-10-14
      • 2013-06-13
      • 2014-01-28
      • 2018-06-15
      相关资源
      最近更新 更多