如何最好地处理 FluentValidation 所需的数据获取

Mat*_*ker 1 fluentvalidation mediatr asp.net-core

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

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

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

在实际请求处理程序中进行更复杂的验证的另一个问题是获取本质上验证错误的内容。目前,如果其中一项检查失败,我会抛出一个ValidationException,然后由中间件捕获并转换为ProblemDetails返回给 API 调用者的 a 。这基本上是流量控制的例外情况,无论如何,验证失败实际上并不是“例外”。

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

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

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

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

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

我知道没有真正适合所有此类问题的方法,但我想知道我是否缺少其他选项,或者拥有类似管道的任何人在使用列出的其中之一时遇到的任何长期可维护性问题选项。

Dmy*_*dar 6

我们使用 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

请求处理程序:

 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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

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.");
        }
    }
Run Code Online (Sandbox Code Playgroud)