使用.NET 6開發TodoList應用(6)——使用MediatR實現POST請求


系列導航

需求

需求很簡單:如何創建新的TodoListTodoItem並持久化。

初學者按照教程去實現的話,應該分成以下幾步:創建Controller並實現POST方法;實用傳入的請求參數new一個數據庫實體對象;調用IRepository<T>完成數據庫的寫入,最多會在中間加一層Service。這個做法本身沒有問題,也是需要從初學階段開始扎實地掌握開發技能的必經之路,有助於幫助理解邏輯調用的過程。

對於稍微正式一些的項目,.NET工程上習慣的實現是通過使用一些比較成熟的類庫框架,有效地對業務邏輯進行分類管理、消除冗余代碼,以達到業務邏輯職責清晰簡潔的目的。在這個階段我們經常使用的兩個類庫分別是AutoMapperMediatR,本文結合POST請求,先介紹關於MediatR部分,下一篇關於GET請求,會涉及AutoMapper的部分。

目標

合理組織並使用MediatR,完成POST請求。

原理與思路

首先來簡單地介紹一下這個類庫。

關於CQRS模式、中介者模式和MediatR

CQRS模式

CQRS模式全稱是“Command Query Responsibility Segregation”,正如字面意思,CQRS模式的目的在於將讀取操作和寫入操作的指責區分開,並使用不同的Model去表示。從CRUD的角度來說,就是把RCUD區分開來對待。如下圖所示:

image

這個模式可以有效地應用到具有主從分離的數據庫架構中,當需要獲取數據時,從只讀數據庫(一般是從庫)中讀取數據,當需要寫入或更新數據時,向主庫進行操作。

CQRS模式旨在解決的問題是:為了屏蔽數據庫層面“寫優先”還是“讀優先”的優化設計策略,在業務邏輯側進行解耦。

任何設計模式都是對解決特定問題的一個Trade off,自然也帶來了一些缺點,首先就是服務內部的組件復雜度上升了,因為需要創建額外的類來實現CQRS模式;其次如果數據層是分離的,那么可能會有數據的狀態不一致問題。

中介者Mediator模式

這是23種基本設計模式中的一個,屬於行為型設計模式,它給出了組件之間交互的一種解耦的方式。簡單參考下圖,具體內容就不過多解釋了,任何一篇介紹設計模式的文章都有介紹。

image

這種設計模式實際上是一種采用依賴倒置(Inversion of Control, IoC)的方式,實現了圖中藍色組件的松耦合。

MediatR

這是在開發中被廣泛采用的實現以上兩種設計模式的類庫,更准確的說法是,它通過應用中介者模式,實現了進程內CQRS。基本思想是所有來自API接口和數據存儲之間的邏輯,都需要通過MediatR來組織(即所謂的“中介者”)。

從實現上看,MediatR提供了幾組用於不同場景的接口,我們在本文中處理的比較多的是IRequest<T>/IRequestHandler<T>以及INotification<T>/INotificationHander<T>兩組接口,更多的請參考官方文檔和例子。

實現

所有需要使用MediatR的地方都集中在Application項目中。

引入MediatR

$ dotnet add src/TodoList.Application/TodoList.Application.csproj package MediatR.Extensions.Microsoft.DependencyInjection

為了適配CQRS的模式,我們在Application項目中的TodoListsTodoItems下相同地創建幾個文件夾:

  • Commands:用於組織CUD相關的業務邏輯;
  • Queries:用於組織R相關的業務邏輯;
  • EventHandlers:用於組織領域事件處理的相關業務邏輯。

Application根目錄下同樣創建DependencyInjection.cs用於該項目的依賴注入:

  • DependencyInjection.cs
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace TodoList.Application;

public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddMediatR(Assembly.GetExecutingAssembly());
        return services;
    }
}

並在Api項目中使用:

// 省略其他...
// 添加應用層配置
builder.Services.AddApplication();
// 添加基礎設施配置
builder.Services.AddInfrastructure(builder.Configuration);

實現Post請求

在本章中我們只實現TodoListTodoItemCreate接口(POST),剩下的接口后面的文章中逐步涉及。

POST TodoList

Application/TodoLists/Commands/下新建一個目錄CreateTodoList用於存放創建一個TodoList相關的所有邏輯:

  • CreateTodoListCommand.cs
using MediatR;
using TodoList.Application.Common.Interfaces;

namespace TodoList.Application.TodoLists.Commands.CreateTodoList;

public class CreateTodoListCommand : IRequest<Guid>
{
    public string? Title { get; set; }
}

public class CreateTodoListCommandHandler : IRequestHandler<CreateTodoListCommand, Guid>
{
    private readonly IRepository<Domain.Entities.TodoList> _repository;

    public CreateTodoListCommandHandler(IRepository<Domain.Entities.TodoList> repository)
    {
        _repository = repository;
    }

    public async Task<Guid> Handle(CreateTodoListCommand request, CancellationToken cancellationToken)
    {
        var entity = new Domain.Entities.TodoList
        {
            Title = request.Title
        };

        await _repository.AddAsync(entity, cancellationToken);
        return entity.Id;
    }
}

有一些實踐是將RequestRequestHandler分開兩個文件,我更傾向於像這樣將他倆放在一起,一是保持簡潔,二是當你需要順着一個Command去尋找它對應的Handler時,不需要更多的跳轉。

接下來在TodoListController里實現對應的POST方法,

using MediatR;
using Microsoft.AspNetCore.Mvc;
using TodoList.Application.TodoLists.Commands.CreateTodoList;

namespace TodoList.Api.Controllers;

[ApiController]
[Route("/todo-list")]
public class TodoListController : ControllerBase
{
    private readonly IMediator _mediator;

    // 注入MediatR
    public TodoListController(IMediator mediator)
        => _mediator = mediator;

    [HttpPost]
    public async Task<Guid> Create([FromBody] CreateTodoListCommand command)
    {
        var createdTodoList = await _mediator.Send(command);

        // 出於演示的目的,這里只返回創建出來的TodoList的Id,
        // 實際使用中可能會選擇IActionResult作為返回的類型並返回CreatedAtRoute對象,
        // 因為我們還沒有去寫GET方法,返回CreatedAtRoute會報錯(找不到對應的Route),等講完GET后會在那里更新
        return createdTodoList.Id;
    }
}

POST TodoItem

類似TodoListControllerCreateTodoListCommand的實現,這里我直接把代碼貼出來了。

  • CreateTodoItemCommand.cs
using MediatR;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Entities;
using TodoList.Domain.Events;

namespace TodoList.Application.TodoItems.Commands.CreateTodoItem;

public class CreateTodoItemCommand : IRequest<Guid>
{
    public Guid ListId { get; set; }

    public string? Title { get; set; }
}

public class CreateTodoItemCommandHandler : IRequestHandler<CreateTodoItemCommand, Guid>
{
    private readonly IRepository<TodoItem> _repository;

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

    public async Task<Guid> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken)
    {
        var entity = new TodoItem
        {
            // 這個ListId在前文中的代碼里漏掉了,需要添加到Domain.Entities.TodoItem實體上
            ListId = request.ListId,
            Title = request.Title,
            Done = false
        };

        await _repository.AddAsync(entity, cancellationToken);

        return entity.Id;
    }
}
  • TodoItemController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using TodoList.Application.TodoItems.Commands.CreateTodoItem;

namespace TodoList.Api.Controllers;

[ApiController]
[Route("/todo-item")]
public class TodoItemController : ControllerBase
{
    private readonly IMediator _mediator;

    // 注入MediatR
    public TodoItemController(IMediator mediator) 
        => _mediator = mediator;

    [HttpPost]
    public async Task<Guid> Create([FromBody] CreateTodoItemCommand command)
    {
        var createdTodoItem = await _mediator.Send(command);

        // 處於演示的目的,這里只返回創建出來的TodoItem的Id,理由同前
        return createdTodoItem.Id;
    }
}

驗證

運行Api項目,通過Hoppscotch發送對應接口請求:

創建TodoList驗證

  • 請求
    image

  • 返回
    image

  • 數據庫
    image
    第一條數據是種子數據,第二條是我們剛才創建的。

創建TodoItem驗證

繼續拿剛才創建的這個TodoList的Id來創建新的TodoItem

  • 請求
    image

  • 返回
    image

  • 數據庫
    image
    最后一條是我們新創建的,其余是種子數據。

總結

我們已經通過演示在POST請求中實現MediatR庫帶來的CQRS模式,在這篇文章里我留了一個坑。就是領域事件的Handler並沒有任何演示,只是創建了一個文件夾,結合在這篇文章中留下來的發布領域事件的坑,會在DELETE的文章中填完。

看起來使用CQRS模式使得我們的代碼結構變得更加復雜了,但是對於一些再復雜一些的實際項目中,正確使用CQRS模式有助於你分析和整理業務需求,並將相關的業務需求以及相關模型梳理到統一的位置進行管理,包括在后續的文章里我們會陸續向其中加入諸如入參校驗、出參類型轉換等邏輯。認真思考並運用習慣之后,大家可以自行體會這樣做的“權衡”。

參考資料

  1. MediatR
  2. Mediator


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM