CQRS也叫命令查詢職責分離,是近年來非常流行的應用程式架構模式。本文將重點介紹如何通過MediatR的管道功能將FluentValidation集成到CQRS項目中實現驗證功能。 ...
本文將重點介紹如何通過MediatR的管道功能將FluentValidation集成到項目中實現驗證功能。
什麼是CQRS?
CQRS(Command Query Responsibility Segregation)也叫命令查詢職責分離,是近年來非常流行的應用程式架構模式。CQRS 背後的理念是在邏輯上將應用程式的流程分成兩個獨立的流程,即命令或查詢。
命令用於改變應用程式的狀態。對應CRUD的創建、更新和刪除部分。查詢用於檢索應用程式中的信息,對應CRUD的讀取部分。
CQRS 的優缺點
優點:
- 單一職責 – 命令和查詢只有一個職責。要麼更改應用程式的狀態,要麼檢索它。因此它們很容易推理和理解。
- 解耦 – 命令或查詢與其處理程式完全解耦,因此在處理程式方面有很大的靈活性,可以按照自己認為最合適的方式來實現。
- 可擴展性 – CQRS 模式在如何組織數據存儲方面非常靈活,為您提供了多種可擴展性選擇。您可以將一個資料庫用於命令和
查詢。您可以使用獨立的讀/寫資料庫來提高性能,併在資料庫之間使用消息傳遞或複製來實現同步。 - 可測試性 – 測試命令或查詢處理程式非常簡單,因為它們的設計非常簡單,只執行一項任務。
缺點:
- 複雜性 – CQRS 是一種高級設計模式,您需要花時間才能完全理解它。它引入了很多複雜性,會給項目帶來摩擦和潛在問題。在決定在項目中使用之前,請務必考慮清楚。
- 學習曲線 – 雖然 CQRS 看起來是一種簡單明瞭的設計模式,但仍存在學習曲線。大多數開發人員習慣於用過程式(命令式)風格編寫代碼,而 CQRS 則與之大相徑庭。
- 難以調試 – 由於命令和查詢與其處理程式是分離的,因此應用程式沒有自然的命令式流程。這使得它比傳統應用程式更難調試。
使用 MediatR 的命令和查詢
MediatR 使用介面(interface)來表示命令和查詢。在我們的項目中,我們將為命令和查詢創建單獨的抽象。
首先,讓我們看看介面是如何定義的:
using MediatR;
namespace Application.Abstractions.Messaging
{
public interface ICommand<out TResponse> : IRequest<TResponse>
{
}
}
using MediatR;
namespace Application.Abstractions.Messaging
{
public interface IQuery<out TResponse> : IRequest<TResponse>
{
}
}
我們在聲明TResponse
泛型時使用了 out 關鍵字,這表示它是協變的。這樣,我們就可以使用比泛型參數指定的類型更多的派生類型。要瞭解有關協變和逆變的更多信息,請查看微軟文檔。
此外,為了完整起見,我們需要對命令和查詢處理程式進行單獨的抽象。
using MediatR;
namespace Application.Abstractions.Messaging
{
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
}
}
using MediatR;
namespace Application.Abstractions.Messaging
{
public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
}
}
這裡留下一個小問題,MediatR已經提供了
IRequest
和IRequest<TResponse>
兩個介面,那我們為什麼還要再次定義IQuery<out TResponse>
和ICommand<out TResponse>
呢?
使用FluentValidation進行驗證
FluentValidation 庫允許我們輕鬆地為我們的類定義非常豐富的自定義驗證。由於我們正在實現 CQRS,所以這裡我們僅討論對Command進行驗證。由於Query對象僅僅是從應用程式獲取數據,意思我們不必多此一舉為Query設計驗證器。
我們先設計一個UpdateUserCommand
public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : ICommand<Unit>;
Unit
是MediatR定義的一個特殊類,表示請求不返回數據,相當於void
或Task
。
這個命令將用於更新已有用戶(通過UserId查找)的FirstName和LastName,關於MediatR如何新增、查詢和修改數據,在之前的文章中我們已經介紹過了,這裡不再贅述。
接下來我們需要為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);
}
}
此驗證器將對UpdateUserCommand
的屬性進行以下驗證:
- UserId - 不可空
- FirstName - 不可空且最大長度不超過100個字元
- LastName - 不可空且最大長度不超過100個字元
使用 MediatR PipelineBehavior創建裝飾器
CQRS 模式使用命令和查詢來傳達信息並接收響應。實質上是請求-響應管道。這使我們能夠輕鬆地圍繞通過管道的每個請求引入其他行為,而無需實際修改原始請求。
您可能熟悉這種名為裝飾器模式的技術。使用裝飾器模式的典型例子就是ASP.NET Core中間件。MediatR與中間件的概念類似,稱為:IPipelineBehavior
public interface IPipelineBehavior<in TRequest, TResponse> where TRequest : notnull
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next);
}
PipelineBehavior是請求實例的包裝器,在如何實現它方面為您提供了很大的靈活性。PipelineBehavior非常適合應用程式中的橫切關註點。橫切關註點的很好的例子是日誌記錄、緩存,當然還有驗證!
創建驗證PipelineBehavior
為了在 CQRS 管道中實現驗證,我們將使用剛纔談到的概念,即 MediatR 的 IPipelineBehavior 和 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();
}
}
處理驗證異常
為了處理遇到驗證錯誤時拋出的ValidationException
,我們可以使用 ASP.NET Core的 IMiddleware
介面。
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;
}
}
設置依賴註入
在運行應用程式之前,我們需要確保已向 DI 容器註冊了所有服務。MediatR的DI註入方式之前已經介紹過,這裡主要演示FluentValidation的註入。由於ValidationBehavior
依賴IValidator<T>
,因此需要註入我們定義的Validator。
// 在Startup.cs中配置
services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
// 在Program.cs中配置(≥ net 6.0)
builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
最後我們需要將ExceptionHandlingMiddleware
也註冊到DI容器和ASP.NET Core的管道中:
// 在Startup.cs中配置
services.AddTransient<ExceptionHandlingMiddleware>();
// 在Program.cs中配置(≥ net 6.0)
builder.Services.AddTransient<ExceptionHandlingMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
測試驗證管道
在項目的Controllers文件夾中找到UserController:
/// <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}")]
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();
}
}
我們可以看到,UpdateUser 操作非常簡單,它從路由中獲取用戶Id,從請求正文中獲取FirstName和LastName,然後創建一個新的 UpdateUserCommand實例並且通過管道發送命令。最後返回204(請求成功但無響應內容)狀態碼。
接下來我們通過Swagger調用API介面:
可以看到,請求的FirstName和LastName都是空白字元串。
補充內容之後再次發送請求。
結論
在本文中,我們介紹了CQRS 模式的一些更高級的概念,以及如何在應用程式中通過橫切的方式實現數據驗證,同時也簡單的介紹瞭如何通過ASP.NTE Core的中間件實現全局異常處理。
點關註,不迷路。
如果您喜歡這篇文章,請不要忘記點贊、關註、轉發,謝謝!如果您有任何高見,歡迎在評論區留言討論……