系列导航及源代码

需求

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

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

目标

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

原理与思路

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

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

实现

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

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

  • ValidationException.cs
namespace TodoList.Application.Common.Exceptions;

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

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

  • ValidationBehaviour.cs
using FluentValidation;
using FluentValidation.Results;
using MediatR;
using ValidationException = TodoList.Application.Common.Exceptions.ValidationException; namespace TodoList.Application.Common.Behaviors; 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 (_validators.Any())
{
var context = new ValidationContext<TRequest>(request); var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults
.Where(r => r.Errors.Any())
.SelectMany(r => r.Errors)
.ToList(); // 如果有validator校验失败,抛出异常,这里的异常是我们自定义的包装类型
if (failures.Any())
throw new ValidationException(GetValidationErrorMessage(failures));
}
return await next();
} // 格式化校验失败消息
private string GetValidationErrorMessage(IEnumerable<ValidationFailure> failures)
{
var failureDict = failures
.GroupBy(e => e.PropertyName, e => e.ErrorMessage)
.ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); return string.Join(";", failureDict.Select(kv => kv.Key + ": " + string.Join(' ', kv.Value.ToArray())));
}
}

DependencyInjection中进行依赖注入:

  • DependencyInjection.cs
// 省略其他...
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)

添加Validation Pipeline Behavior

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

  • CreateTodoItemCommandValidator.cs
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Entities; namespace TodoList.Application.TodoItems.Commands.CreateTodoItem; public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
{
private readonly IRepository<TodoItem> _repository; public CreateTodoItemCommandValidator(IRepository<TodoItem> repository)
{
_repository = repository; // 我们把最大长度限制到10,以便更好地验证这个校验
// 更多的用法请参考FluentValidation官方文档
RuleFor(v => v.Title)
.MaximumLength(10).WithMessage("TodoItem title must not exceed 10 characters.").WithSeverity(Severity.Warning)
.NotEmpty().WithMessage("Title is required.").WithSeverity(Severity.Error)
.MustAsync(BeUniqueTitle).WithMessage("The specified title already exists.").WithSeverity(Severity.Warning);
} public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
{
return await _repository.GetAsQueryable().AllAsync(l => l.Title != title, cancellationToken);
}
}

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

验证

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

  • 请求

  • 响应

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

一点扩展

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

// 省略其他...
var response = await next();
//Response
_logger.LogInformation($"Handled {typeof(TResponse).Name}"); return response;

创建一个LoggingBehavior

using System.Reflection;
using MediatR.Pipeline;
using Microsoft.Extensions.Logging; 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关于请求的任何信息
_logger.LogInformation($"Handling {typeof(TRequest).Name}"); IList<PropertyInfo> props = new List<PropertyInfo>(request.GetType().GetProperties());
foreach (var prop in props)
{
var propValue = prop.GetValue(request, null);
_logger.LogInformation("{Property} : {@Value}", prop.Name, propValue);
}
}
}

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

// 省略其他...
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));

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

效果如下图所示:

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

总结

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

参考资料

  1. FluentValidation
  2. How to use MediatR Pipeline Behaviours

使用.NET 6开发TodoList应用(11)——使用FluentValidation和MediatR实现接口请求验证的更多相关文章

  1. 使用.NET 6开发TodoList应用(12)——实现ActionFilter

    系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 Filter在.NET Web API项目开发中也是很重要的一个概念,它运行在执行MVC响应的Pipeline中执行,允许我们 ...

  2. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  3. 使用.NET 6开发TodoList应用(1)——系列背景

    前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...

  4. 使用.NET 6开发TodoList应用(2)——项目结构搭建

    为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...

  5. 使用.NET 6开发TodoList应用(4)——引入数据存储

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...

  6. 使用.NET 6开发TodoList应用(5)——领域实体创建

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...

  7. 使用.NET 6开发TodoList应用(5.1)——实现Repository模式

    需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...

  8. 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求

    需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...

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

    系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...

随机推荐

  1. ceph块存储场景

    1.创建rbd使用的存储池. admin节点需要安装ceph才能使用该命令,如果没有,也可以切换到ceph-node1节点去操作. [cephfsd@ceph-admin ceph]$ ceph os ...

  2. 一起手写吧!sleep函数!

    Async/Await 版本 function sleep(delay) { return new Promise(reslove => { setTimeout(reslove, delay) ...

  3. canal整合springboot实现mysql数据实时同步到redis

    业务场景: 项目里需要频繁的查询mysql导致mysql的压力太大,此时考虑从内存型数据库redis里查询,但是管理平台里会较为频繁的修改增加mysql里的数据 问题来了: 如何才能保证mysql的数 ...

  4. transient关键字和volatile关键字

    看到HashSet的源代码的时候,有一个关键字不太认识它..transient,百度整理之: Java的Serialization提供了一种持久化对象实例的机制,当持久化对象时,可能有一些特殊的对象数 ...

  5. alert之后才执行

    如果在正常情况下,代码要在alert之后才执行,解决办法:将要执行的代码用setTimeout延迟执行即可(原因:页面未加载完毕)

  6. Nginx中指令

    Rewrite模块 1 return指令 Syntax: return code [text]; return code URL; return URL; Default: - Context: se ...

  7. 转:ios delegate

    首先,大家应该都明白的是委托是协议的一种,顾名思义,就是委托他人帮自己去做什么事.也就是当自己做什么事情不方便的时候,就可以建立一个委托,这样就可以委托他人帮自己去实现什么方法. 其次,我简单的总结了 ...

  8. Python语法之基本数据类型

    一.数据类型之字符串str 作用:主要用于记录描述性性质的数据,如姓名.地址.邮箱: 定义: 方式1 # 单引号 name = 'jason' 方式2 # 双引号 name = "jason ...

  9. 扬我国威,来自清华的开源项目火爆Github

    前几天TJ君跟大家分享了几个有趣的Github项目(加密解密.食谱.新冠序列,各种有趣的开源项目Github上都有),其中呢,有不少是来自斯坦福大学的项目,当时TJ君就不由得想,什么时候能看到的项目都 ...

  10. 项目管理的基本概念(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 关于项目管理的基本概念,我看了好久,也迷糊了好久--原谅我实在不是个善于理解概念的妖,最终我决定,就记些简单的东东吧,具体 ...