飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

使用.NET 6开发TodoList应用(11)——使用FluentValidation和MediatR实现接口请求验证

时间:2021-12-31  作者:code4nothing  

系列导航及源代码

  • 使用.NET 6开发TodoList应用文章索引

需求

在响应请求处理的过程中,我们经常需要对请求参数的合法性进行校验,如果参数不合法,将不继续进行业务逻辑的处理。我们当然可以将每个接口的参数校验逻辑写到对应的Handler方法中,但是更好的做法是借助MediatR提供的特性,将这部分与实际业务逻辑无关的代码整理到单独的地方进行管理。

为了实现这个需求,我们需要结合FluentValidation和MediatR提供的特性。

目标

将请求的参数校验逻辑从CQRS的Handler中分离到MediatR的Pipeline框架中处理。

原理与思路

MediatR不仅提供了用于实现CQRS的框架,还提供了IPipelineBehavior<TRequest, TResult>接口用于实现CQRS响应之前进行一系列的与实际业务逻辑不紧密相关的特性,诸如请求日志、参数校验、异常处理、授权、性能监控等等功能。

在本文中我们将结合FluentValidationIPipelineBehavior<TRequest, TResult>实现对请求参数的校验功能。

实现

添加MediatR参数校验Pipeline Behavior框架支持

首先向Application项目中引入域名ndencyInjectionExtensionsNuget包。为了抽象所有的校验异常,先创建ValidationException类:

  • 域名
namespace 域名域名ptions;

public class ValidationException : Exception
{
    public ValidationException() : base("One or more validation failures have occurred.")
    {
    }

    public ValidationException(string failures)
        : base(failures)
    {
    }
}

参数校验的基础框架我们创建到Application/Common/Behaviors/中:

  • 域名
using FluentValidation;
using 域名lts;
using MediatR;
using ValidationException = 域名域名dationException;

namespace 域名域名viors;

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    // 注入所有自定义的Validators
    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators) 
        => _validators = validators;

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (域名())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await 域名All(域名ct(v => 域名dateAsync(context, cancellationToken)));

            var failures = validationResults
                .Where(r => 域名())
                .SelectMany(r => 域名rs)
                .ToList();

            // 如果有validator校验失败,抛出异常,这里的异常是我们自定义的包装类型
            if (域名())
                throw new ValidationException(GetValidationErrorMessage(failures));
        }
        return await next();
    }

    // 格式化校验失败消息
    private string GetValidationErrorMessage(IEnumerable<ValidationFailure> failures)
    {
        var failureDict = failures
            .GroupBy(e => 域名ertyName, e => 域名rMessage)
            .ToDictionary(failureGroup => 域名, failureGroup => 域名ray());

        return 域名(";", 域名ct(kv => 域名 + ": " + 域名(\' \', 域名ray())));
    }
}

DependencyInjection中进行依赖注入:

  • 域名
// 省略其他...
域名alidatorsFromAssembly(域名xecutingAssembly());
域名ransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)

添加Validation Pipeline Behavior

接下来我们以添加TodoItem接口为例,在Application/TodoItems/CreateTodoItem/中创建CreateTodoItemCommandValidator

  • 域名
using FluentValidation;
using 域名tyFrameworkCore;
using 域名域名rfaces;
using 域名ties;

namespace 域名域名teTodoItem;

public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
{
    private readonly IRepository<TodoItem> _repository;

    public CreateTodoItemCommandValidator(IRepository<TodoItem> repository)
    {
        _repository = repository;

        // 我们把最大长度限制到10,以便更好地验证这个校验
        // 更多的用法请参考FluentValidation官方文档
        RuleFor(v => 域名e)
            .MaximumLength(10).WithMessage("TodoItem title must not exceed 10 characters.").WithSeverity(域名ing)
            .NotEmpty().WithMessage("Title is required.").WithSeverity(域名r)
            .MustAsync(BeUniqueTitle).WithMessage("The specified title already exists.").WithSeverity(域名ing);
    }

    public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
    {
        return await 域名sQueryable().AllAsync(l => 域名e != title, cancellationToken);
    }
}

其他接口的参数校验添加方法与此类似,不再继续演示。

验证

启动Api项目,我们用一个校验会失败的请求去创建TodoItem:

  • 请求
    image

  • 响应
    image

因为之前测试的时候已经在没有加校验的时候用同样的请求生成了一个TodoItem,所以校验失败的消息里有两项校验都没有满足。

一点扩展

我们在前文中说了使用MediatR的PipelineBehavior可以实现在CQRS请求前执行一些逻辑,其中就包含了日志记录,这里就把实现方式也放在下面,在这里我们使用的是Pipeline里的IRequestPreProcessor<TRequest>接口实现,因为只关心请求处理前的信息,如果关心请求处理返回后的信息,那么和前文一样,需要实现IPipelineBehavior<TRequest, TResponse>接口并在Handle中返回response对象:

// 省略其他...
var response = await next();
//Response
域名nformation($"Handled {typeof(TResponse).Name}");

return response;

创建一个LoggingBehavior

using 域名ection;
using 域名line;
using 域名ing;

public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull
{
    private readonly ILogger<LoggingBehaviour<TRequest>> _logger;

    // 在构造函数中后面我们还可以注入类似ICurrentUser和IIdentity相关的对象进行日志输出
    public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger)
    {
        _logger = logger;
    }

    public async Task Process(TRequest request, CancellationToken cancellationToken)
    {
        // 你可以在这里log关于请求的任何信息
        域名nformation($"Handling {typeof(TRequest).Name}");

        IList<PropertyInfo> props = new List<PropertyInfo>(域名ype().GetProperties());
        foreach (var prop in props)
        {
            var propValue = 域名alue(request, null);
            域名nformation("{Property} : {@Value}", 域名, propValue);
        }
    }
}

如果是实现IPipelineBehavior<TRequest, TResponse>接口,最后注入即可。

// 省略其他...
域名ransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));

如果实现IRequestPreProcessor<TRequest>接口,则不需要再进行注入。

效果如下图所示:
image

可以看到日志中已经输出了Command名称和请求参数字段值。

总结

在本文中我们通过FluentValidationMediatR实现了不侵入业务代码的请求参数校验逻辑,在下一篇文章中我们将介绍.NET开发中会经常用到的ActionFilters

参考资料

  1. FluentValidation
  2. How to use MediatR Pipeline Behaviours

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。