基于 MediatR 和 FluentValidation 的 CQRS 验证管线

CQRS Validation Pipeline with MediatR and FluentValidation - Code Maze (code-maze.com)

示例代码地址:https://github.com/CodeMazeBlog/cqrs-validation-mediatr-fluentvalidation/

让我们直接开始。

1. 什么是 CQRS?

CQRS 或者说命令查询职责分离,在近年来变得越来越流行。在 CQRS 背后的思想是将你的应用程序的逻辑处理流程拆分为两个独立的处理流程,一个处理改变,也就是 Command,另一个处理查询,也就是 Query 流程。

Command 用于改变应用程序的状态,如果我们谈论的是 CRUD ( 增、删、改、查 ) 的话,Command 对应的是增、删和修改。

而 Query 则用来获取应用程序的信息,自然而然,它对应的部分是查询。

如果要学习如何在 ASP.NET Core 应用程序中实现基于 MediatR 的 CQRS,那么请查阅 在 ASP.NET Core 中应用 MediatR 和 CQRS。你应该已经熟悉了 CQRS 和 MediatR 来继续阅读本文,所以,如果还没有的话,我们强烈建议你先阅读上面链接的文章。

1.1 CQRS 的优点和缺点

使用 CQRS 有哪些优点呢?为什么我们考虑在应用程序中使用它呢?

CQRS 的优点:

  • 单一职责:Command 和 Query 只需要负责一个任务。或者是改变应用程序的状态,或者是获取应用程序的状态。进而,它们变得非常容易理解
  • 解耦:Command 或者 Query 完全从处理器中解耦出来,从处理器层面给予你充分的灵活性,以最适合你的方式来实现它
  • 扩展性:在如何组织你的数据存储上,CQRS 模式可以非常灵活,给予你巨大的扩展空间,你既可以使用单一一个数据库来处理 Command 和 Query 两者,也可以对读和写使用各自独立的数据库,以改进性能,通过消息通讯或者数据库复制在数据库之间进行同步。
  • 可测试性:由于设计已经变得简单,非常容易测试 Command 或者 Query 的处理器,只需要执行一个任务即可。

当然,不会全是优点,下面是 CQRS 的一些缺点:

  • 复杂性:CQRS 是高级的设计模式,它需要你花费时间才能完全理解。它引入了一系列的复杂性,导致项目的摩擦和潜在问题。在真正在你的项目中使用它之前,确保你真的完全理解它。
  • 学习曲线:尽管看起来是很简单的设计模式,对于 CQRS 仍然存在陡峭的学习曲线。大多数的开发人员熟悉过程式 ( 命令式 ) 的代码风格,而 CQRS 与此非常不同。
  • 难以调试:由于 Command 和 Query 从它们的处理器中解耦出来,这样就不再存在应用程序中自然的命令处理流。这导致它比传统的应用程序难以调试。

2. 使用 MediatR 实现 Command 和 Query

前面我们已经介绍了 Command 和 Query,现在我们看一看如何使用 MediatR 来实现它们。

MediatR 使用 IRequest 接口来表示 Command 或者 Query。对于我们的场景,我们将创建对于 Command 和 Query 分别的抽象。

在 NuGet 包 MediatR.Contracts 中,定义了 MediatR 的接口抽象。

首先,让我们先定义 ICommand 接口来抽象 Command。

using MediatR;

namespace Application.Abstractions.Messaging
{
public interface ICommand<out TResponse> : IRequest<TResponse>
{
}
}

然后,定义 IQuery 接口来表示 Query 的抽象。

using MediatR;

namespace Application.Abstractions.Messaging
{
public interface IQuery<out TResponse> : IRequest<TResponse>
{
}
}

这里我们对泛型类型 TResponse 使用了 out 关键字,这表示它是协变的。这支持我们可以使用各种派生类型,而不只是特定的泛型参数。对于协变和逆变的更多知识,请参考微软的文档

另外,我们还需要对 Command 和 Query 的处理器分别进行抽象,为了完整性。让我们检查一下它们:

Command 的处理器

using MediatR;

namespace Application.Abstractions.Messaging
{
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
}
}

Query 的处理器

using MediatR;

namespace Application.Abstractions.Messaging
{
public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
}
}

为什么我们不嫌麻烦,定义定制的 Command 和 Query 处理器,而不使用 MediaR 已经提供的通用处理器呢?MediatR 提供的 IRequest 接口还不够吗?

使用定制的 Command 和 Query 抽象,这种方式可以在未来提供更多的灵活性。考虑一下,如果你希望增强你的 Command 或者 Query 以提供更多的特性呢?

这里有一个简单的示例,我们希望所有的 Command 都是幂等的,幂等意味着它们只能执行一次。

现在,你就可以扩展 ICommand 接口,创建一个新的幂等命令接口 IIdempotentCommand

public interface IIdempotentCommand<out TResponse> : ICommand<TResponse>
{
Guid RequestId { get; set; }
}

随后,基于该接口实现某些确保幂等的逻辑。不过这确实非常复杂,以后我们再讨论它。

另外,对 Command 和 Query 使用附加的抽象,给予我们使用 MediatR 的处理管道执行过滤的能力,在下一节我们就可以看到。

3. 使用 FluentValidation 实现验证

FluentValidation 库支持我们对自定义的类型,可以简单地定义非常丰富的自定义验证。由于我们正在实现 CQRS,对命令定义验证是很有意义的。

我们不应该为 Query 定义验证程序而烦恼,因为它们不包含任何行为。我们仅使用查询从应用程序获取数据。

例如,让我们看一下 UpdateUserCommand

public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : ICommand<Unit>

我们将使用这个 Command 来更新已有用户的姓名。

下面我们实现对于这个 UpdateUserCommand 的验证器

public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
{
public UpdateUserCommandValidator()
{
RuleFor(x => x.UserId).NotEmpty(); RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100); RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
}
}

使用这个 UpdateUserCommandValidator,我们希望确保该 Command 的参数不会是空,并且姓名的最大长度不会支持的最大长度。

就这样,非常简单。

在本文中,我们不会再进一步深入到 FluentValidation 库。要是你并不熟悉它,或者希望进一步学习它,请学习 在 ASP.NET Core 中的 FluentValidation

3. 使用 MediatR PipelineBehavior 创建装饰器

CQRS 模式使用 Command 或者 Query 来传递参数,然后获得响应。实质上来说,它表示一个 请求 - 响应 管道。 这支持我们可以更加容易地针对每个穿过管道的请求,引入额外的处理行为的能力,而不需要我们修改原始的请求。

你可能已经基于名称熟悉装饰器模式的技术。另一个使用使用装饰器模式的例子来自 ASP.NET Core 中的中间件概念。

MediaR 有一个类似于中间件的概念,它称为 IPipelineBehavior

public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}

这个 Pipeline Behavior 是针对 Request 实例的封装,给予你如何实现它的各种灵活性。管道行为非常适合应用程序中面向切面的思想。比较好的面向切面的例子是日志、缓存,当然了,还有验证。

4. 创建验证管道行为

为了实现我们 CQRS 中的验证,我们将使用刚刚讨论的概念,使用 MediatRIPipelineBehavior 和 FluentValidation。

首先看一看 ValidationBehavior 的实现。

public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, ICommand<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators; public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators; public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (!_validators.Any())
{
return await next();
} var context = new ValidationContext<TRequest>(request); var errorsDictionary = _validators
.Select(x => x.Validate(context))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.GroupBy(
x => x.PropertyName,
x => x.ErrorMessage,
(propertyName, errorMessages) => new
{
Key = propertyName,
Values = errorMessages.Distinct().ToArray()
})
.ToDictionary(x => x.Key, x => x.Values); if (errorsDictionary.Any())
{
throw new ValidationException(errorsDictionary);
} return await next();
}
}

现在,我们介绍在 ValidationBEhavior 的实现。

首先,注意到我们使用了 where 子句应用于实现了 IPipelineBehavior 的类型上,限制 TRequest 必须实现了接口 ICommand<TRequest>。这样,我们只允许 Command 类型穿过该管道。记住,我们前面提到过,我们只对 Command 进行验证,这就是如何实现。

然后,你看到我们通过构造函数注入了一个 IValidator 的集合。FluentValidation 库将扫描在我们项目中针对特定类型实现的所有 AbstractValidator 实现,并在运行时提供出来,这就是我们为什么可以在项目中应用实际的验证器的原因。

最后,如果有任何验证错误出现,我们就会抛出 ValidationException 异常,它包含一个验证错误信息的字典。当因为验证错误抛出异常的时候,管道将会短路,并防止进一步的执行。这里缺失的一块就是,在应用程序的更高层级处理异常,并提供更有意义的表示给消费者。下一节我们就处理这个问题。

5. 处理验证异常

为了处理 ValidationException,它会在我们遇到验证错误的时候跑出来。我们可以使用 ASP.NET Core 的中间件接口,实现一个全局的异常处理器。

internal sealed class ExceptionHandlingMiddleware : IMiddleware
{
private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) => _logger = logger; public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception e)
{
_logger.LogError(e, e.Message); await HandleExceptionAsync(context, e);
}
} private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception)
{
var statusCode = GetStatusCode(exception); var response = new
{
title = GetTitle(exception),
status = statusCode,
detail = exception.Message,
errors = GetErrors(exception)
}; httpContext.Response.ContentType = "application/json"; httpContext.Response.StatusCode = statusCode; await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response));
} private static int GetStatusCode(Exception exception) =>
exception switch
{
BadRequestException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
ValidationException => StatusCodes.Status422UnprocessableEnttity,
_ => StatusCodes.Status500InternalServerError
}; private static string GetTitle(Exception exception) =>
exception switch
{
ApplicationException applicationException => applicationException.Title,
_ => "Server Error"
}; private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception)
{
IReadOnlyDictionary<string, string[]> errors = null; if (exception is ValidationException validationException)
{
errors = validationException.ErrorsDictionary;
} return errors;
}
}

6. 设置依赖注入

在我们可以运行应用程序之前,我们需要确保在依赖注入容器中注入了所有需要的服务。

首先看一下,如何基于 MediatR 注册我们的 Command 和 Query,在 StartUp 类中的 ConfigureService() 方法中,我们填充如下代码:

services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

在 .NET 6 中,我们需要修改 Program 类。

builder.Services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

第一行调用将会扫描我们的 Application 程序集并添加所有的 Command、Query 和它们相应的处理器到 DI 容器中。

第二行方法调用是我们的 ValidationBehavior 注册步骤,没有它,验证管线就完全不会执行。

然后,我们需要确保注册我们使用 FluentValidation 定义的验证器:

services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

//Or in .NET 6 and above

builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);

最后,我们需要在 ConfigureService() 方法中注册定义的全局异常处理器。

services.AddTransient<ExceptionHandlingMiddleware>();

//or in .NET 6 and above

builder.Services.AddTransient<ExceptionHandlingMiddleware>();

Configure() 方法中,或者在 .NET 6 的 Program 类中,注册到 ASP.NET Core 的请求处理管道中

app.UseMiddleware<ExceptionHandlingMiddleware>();

就是这样,我们完成了在依赖注入容器中注册服务。现在所有的部分已经就绪,我们可以测试我们的实现了。

7. 使用验证管道

现在,我们可以在项目 Presentation 中的 UsersController 中使用它。

/// <summary>
/// The users controller.
/// </summary>
[ApiController]
[Route("api/[controller]")]
public sealed class UsersController : ControllerBase
{
private readonly ISender _sender; /// <summary>
/// Initializes a new instance of the <see cref="UsersController"/> class.
/// </summary>
/// <param name="sender"></param>
public UsersController(ISender sender) => _sender = sender; /// <summary>
/// Updates the user with the specified identifier based on the specified request, if it exists.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <param name="request">The update user request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>No content.</returns>
[HttpPut("{userId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateUser(int userId, [FromBody] UpdateUserRequest request, CancellationToken cancellationToken)
{
var command = request.Adapt<UpdateUserCommand>() with
{
UserId = userId
}; await _sender.Send(command, cancellationToken); return NoContent();
}
}

上面的代码并不是 UserController 的完全实现,这里我们只关注更新用户的端点,完整的实现请查阅:our source code repository

可以看到 UpdateUser() 方法非常简单,它通过路由收集用户的标识,从请求体中获得用户名称,然后创建一个新的 UpdateUserCommand 实例,随后通过管线发送了这个命令,最后返回一个 204 - 没有内容的响应。

在 API 端点开发中,我们完全不顾虑验证问题,这个问题由 ValidationBehavior 处理。

通过 Swagger 界面发送一个请求

这里是服务器返回的响应内容

一旦我们提供正确的请求体,重新发布请求。

响应提示我们,数据库中没有对应的用户。

如果我们使用存在的用户标识,我们就会得到期望的响应 204,没有内容的状态码。

8. 总结

在本文中,我们学习两使用 CQRS 模式的高级概念,以及如何基于面向切面的编程实现验证问题。

我们从说明 CQRS 是什么开始,它的优点和缺点。然后我们学习如何使用 MediatR 库来创建 Command 和 Query。

然后,我们总结了如何使用 FluentValidation 库来验证我们创建的 Command

我们还学习了如何实现 ValidationBehavior 来封装以面向切面方式编程。我们说明了为什么我们仅仅希望验证 Command,以及如何在管道中实现过滤来不对 Query 进行验证。

最后,我们展示了如何将它与 ExceptionHandlingMiddleware 集成,以及如何通过依赖注入容器将这些内容变成整体。

基于 MediatR 和 FluentValidation 的 CQRS 验证管线的更多相关文章

  1. 基于jQuery的Validate表单验证

    表单验证可以说在前端开发工作中是无处不在的~ 有数据,有登录,有表单, 都需要前端验证~~  而我工作中用到最多的就是基于基于jQuery的Validate表单验证~  就向下面这样~ 因为今天有个朋 ...

  2. 基于Jquery Validate 的表单验证

    基于Jquery Validate 的表单验证 jquery.validate.js是jquery下的一个验证插件,运用此插件我们可以很便捷的对表单元素进行格式验证. 在讲述基于Jquery Vali ...

  3. java struts2入门学习--基于xml文件的声明式验证

    一.知识点总结 后台验证有两种实现方式: 1 手工验证顺序:validateXxx(针对Action中某个业务方法验证)--> validate(针对Action中所有的业务方法验证) 2 声明 ...

  4. 基于session和cookie的登录验证(CBV模式)

    基于session和cookie的登录验证(CBV模式) urls.py """cookie_session URL Configuration The `urlpatt ...

  5. 项目一:第十二天 1、常见权限控制方式 2、基于shiro提供url拦截方式验证权限 3、在realm中授权 5、总结验证权限方式(四种) 6、用户注销7、基于treegrid实现菜单展示

    1 课程计划 1. 常见权限控制方式 2. 基于shiro提供url拦截方式验证权限 3. 在realm中授权 4. 基于shiro提供注解方式验证权限 5. 总结验证权限方式(四种) 6. 用户注销 ...

  6. 基于 .NET 的 FluentValidation 数据验证

    学习地址:官方文档,更多更详细的内容可以看官方文档. FluentValidation 是一个基于 .NET 开发的验证框架,开源免费,而且优雅,支持链式操作,易于理解,功能完善,还是可与 MVC5. ...

  7. 基于Python的函数回归算法验证

    看机器学习看到了回归函数,看了一半看不下去了,看到能用方差进行函数回归,又手痒痒了,自己推公式写代码验证: 常见的最小二乘法是一阶函数回归回归方法就是寻找方差的最小值y = kx + bxi, yiy ...

  8. 重温WCF之WCF传输安全(十三)(3)基于SSL的WCF对客户端验证(转)

    转载地址:http://www.cnblogs.com/lxblog/archive/2012/09/18/2690719.html 上文我们演示了,客户端对服务器端身份的验证,这一篇来简单演示一下对 ...

  9. Asp.net Mvc4 基于Authorize实现的模块权限验证方式

    在MVC中,我们可以通过在action或者controller上设置Authorize[Role="xxx"] 的方式来设置用户对action的访问权限.显然,这样并不能满足我们的 ...

  10. .NetCore使用FluentValidation实现友好验证提示

    Nuget包导入FluentValidation.AspNetCore 官方的用法是在services中添加如下来操作 services.AddMvc().AddFluentValidation(co ...

随机推荐

  1. 使用duxapp开发 React Native App 事半功倍

    Taro的React Native端开发提供了两种开发方式,一种是将壳和代码分离,一种是将壳和代码合并在一起开发 壳是用来打包调试版或者发版安装包使用的 代码是运行在壳上的js代码 Taro壳子的代码 ...

  2. vue 赶鸭子上架入门笔记(一) 安装开发环境

    准备接手一个 vue 的前端项目,从零开始学习 vue.目标不高大上,能看得懂代码,能进行简单的修改,改完能打包和部署. 首先解决 vue 开发环境的准备.访问 Node.js 官方网站,下载适合你操 ...

  3. threejs 几何体的本质 顶点

    几何体的线框模式, 一个正方平面最少可以由4个顶点组成,两个三角形组成(公用了 2个顶点,使用了索引创建顶点属性) . // 导入 threejs import * as THREE from &qu ...

  4. DOM 的事件流

    事件流分为三个阶段:捕获 ==>目标 ==>冒泡 1. 事件捕获阶段:事件传播由目标节点的祖先节点逐级传播到目标节点.先由文档的根 节点 document(window)开始触发对象,最后 ...

  5. CSP-J2/S2 2023 游记

    可能早就应该发出来的游记. 2023-10-07 16:32. 前一天睡得比较晚,所以迟到了一点点. 上来先敲了个对拍,拍了一个 if a % 1000 = 0 then a++ 的 A + B,拍出 ...

  6. 一图为你揭秘云数据库GaussDB管理平台亮点

    云数据库GaussDB管理平台(TPOPS)是一款即开即用.稳定可靠.管理便捷的数据库运维管理平台.通过该平台,用户可以快速部署安装GauSSDB,实现智能化运维,大幅度提升运维和管理效率.一图带你揭 ...

  7. 云原生动态周刊:你订阅 GitHub README 播客了吗?

    云原生一周动态要闻: Apache Kafka 3.0.0 发布 Deis Labs 推出 WebAssembly PaaS 平台 Hippo Mirantis Flow 将数据中心重塑为云原生系统 ...

  8. 常见Linux查看文件、文本命令

    查看文件 find命令 按文件名 find 路径 -nmae "文件名" 按文件类型 find 路径 -type 类型 类型:普通文件 f 目录d 符号链接l 块设备文件b ​ 字 ...

  9. 从0到1实现项目Docker编排部署

    在深入讨论 Docker 编排之前,首先让我们了解一下 Docker 技术本身.Docker 是一个开源平台,旨在帮助开发者自动化应用程序的部署.扩展和管理.自 2013 年推出以来,Docker 迅 ...

  10. Java新特性-stream流

    Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据. Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达 ...