Как лучше обрабатывать выборку данных, необходимых для FluentValidation

В приложении, над которым я работаю, я использую 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, что любые условия гонки не являются проблемой. Объекты не меняют владельцев, и данные редко удаляются из базы данных в целях аудита/бухгалтерского учета.

Я знаю, что не существует универсального решения для подобных проблем, но я хотел бы знать, есть ли дополнительные параметры, которые мне не хватает, или какие-либо проблемы с долгосрочной ремонтопригодностью, с которыми сталкивался любой, у кого есть аналогичный конвейер, если они использовали один из перечисленных опции.


person Matt Sieker    schedule 01.04.2020    source источник


Ответы (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))
        );
    }
person dummyDev    schedule 06.04.2020
comment
Я не знал о библиотеке OneOf. Дискриминированные объединения — это то, что я полюбил в TypeScript и пропустил в C#. Я попробую и посмотрю, насколько хорошо это подходит. - person Matt Sieker; 07.04.2020

Мы используем 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(
            IRepositoryReadAsync<ProductEntity> productRepository)
        {
            RuleFor(cmd => cmd)
                .Must(cmd => cmd.Product != null)
                .WithMessage(cmd => $"The product with id {cmd.ProductId} doesn't exist.");
        }
    }
person Dmitry Bondar    schedule 06.08.2021