使用MediatR和FluentValidation實現CQRS應用程式的數據驗證

来源:https://www.cnblogs.com/zhaorong/archive/2023/08/16/fluent-validation-with-mediatr.html
-Advertisement-
Play Games

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已經提供了IRequestIRequest<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定義的一個特殊類,表示請求不返回數據,相當於voidTask

這個命令將用於更新已有用戶(通過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介面:

Swagger interface showing PUT method to update users resource.

可以看到,請求的FirstName和LastName都是空白字元串。

Swagger interface showing PUT method error response.

補充內容之後再次發送請求。

Swagger interface showing PUT method and response when user is not found.

結論

在本文中,我們介紹了CQRS 模式的一些更高級的概念,以及如何在應用程式中通過橫切的方式實現數據驗證,同時也簡單的介紹瞭如何通過ASP.NTE Core的中間件實現全局異常處理。


點關註,不迷路。

如果您喜歡這篇文章,請不要忘記點贊、關註、轉發,謝謝!如果您有任何高見,歡迎在評論區留言討論……

公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 說到分散式事務,大家並不陌生。在實際工作中,用得比較多的還是柔性分散式事務,今天主要把在工作中運用到的幾種柔性分散式事務的場景及實現方式做一個簡單介紹,也可以看做是柔性分散式事務的一個演進過程。 ...
  • 本文是一次工作中對併發問題的處理案例,問題發生在快遞分揀的流程中,我儘可能將業務背景簡化,讓大家只關註併發問題本身。 ...
  • 本文從EXPLAIN分析SQL的執行計劃開始,進行示例展示,並對輸出結果進行解讀,同時總結了EXPLAIN可產生額外的擴展信息以及EXPLAIN的估計查詢性能,整篇文章基於MySQL 8.0編寫,理論支持MySQL 5.0及更高版本。 ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • > 😂 好久前寫了關於 `getStaticProps` 和 `getStaticPaths` 的內容,然而半年過去了源碼解析就一直忘記了,不久前有人提醒才想起來,補下坑。 本文主要是解讀下 `getStaticProps`、`getStaticPaths` 相關的源碼,不瞭解這兩個 `API` ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、需求 微信掃碼授權,如果允許授權,則登錄成功,跳轉到首頁。 二、問題 1、微信掃碼授權有幾種實現方式? 2、說一下這幾種實現方式的原理是什麼? 3、vue中的微信掃碼授權登錄,與uniapp和原生小程式的微信授權登錄,它們之間有共同點 ...
  • 本文,將向大家介紹 CSS 規範中,最新的 Anchor Positioning,翻譯為**錨點定位**。 Anchor Position 的出現,極大的豐富了 CSS 的能力,雖然語法稍顯複雜,但是有了它,能夠實現非常多之前實現起來非常困難,或者壓根無法使用純 CSS 實現的功能。 Anchor ...
  • 索引簽名是 TypeScript 中一個強大的特性,它允許我們在對象和類中使用動態的屬性名稱。通常情況下,我們會在對象或類中定義固定的屬性,但有時我們需要處理具有動態屬性名稱的情況。這時,索引簽名就派上了用場。 在這篇技術博文中,我們將介紹索引簽名的使用方法和用例,將展示如何定義帶有索引簽名的介面... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...