使用 MediatR 實作 CQRS

關於 CQRS 模式,推薦閱讀 CQRS (Command Query Responsibility Segregation). By Ajay Kumar。書中的範例專案是用 Java 撰寫,但只要熟悉 C# 或物件導向程式語言的讀者,都可以很容易理解。此書以學生選課管理系統為範例,透過重構為任務型介面,再進一步重構為 CQRS 模式,來說明主要概念。此書也是以實務的角度出發,提到了很多權衡,例如建立查詢專用資料庫的必要性。

基礎配置

  1. 建立 .NET 8 專案

    • Purchasing.Core (Class Library)

    • Purchasing.Infrastructure (Class Library)

    • Purchasing.UseCases (Class Library)

    • Purchasing.Api (Web API)

  2. 安裝 MediatR Nuget Packages

    版本為 12.2.0,安裝到 Purchasing.UseCasesPurchasing.Api

    1
    2
    Install-Package MediatR -Project Purchasing.UseCases
    Install-Package MediatR -Project Purchasing.Api
  3. 配置 MediatR

    Purchasing.UseCases 新增下列程式碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // Extensions/DependencyInjection/ServiceCollectionExtensions.cs

    using Microsoft.Extensions.DependencyInjection;

    namespace Purchasing.UseCases.Extensions.DependencyInjection
    {
    public static class ServiceCollectionExtensions
    {
    public static IServiceCollection AddMediatR(this IServiceCollection services)
    {
    services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(typeof(ServiceCollectionExtensions).Assembly));

    return services;
    }
    }
    }

    Purchasing.Api 新增下列程式碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Program.cs

    // ...

    using Purchasing.UseCases.Extensions.DependencyInjection

    // ...

    builder.Services.AddMediatR();

    // ...

建立 Query

  1. Purchasing.UseCases 新增 Query

    1
    2
    3
    4
    5
    6
    7
    8
    // Queries/ListPurchaseOrdersQuery.cs

    using MediatR;

    namespace Purchasing.UseCases.Queries
    {
    public record ListPurchaseOrdersQuery(string? UserId) : IRequest<List<PurchaseOrderDto>>;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Handlers/ListPurchaseOrdersHandler.cs

    using MediatR;
    using Purchasing.UseCases.Contracts;
    using Purchasing.UseCases.Queries;

    namespace Purchasing.UseCases.Handlers
    {
    public class ListPurchaseOrdersHandler: IRequestHandler<ListPurchaseOrdersQuery, List<PurchaseOrderDto>>
    {
    public async Task<List<PurchaseOrderDto>> Handle(ListPurchaseOrdersQuery request, CancellationToken cancellationToken)
    {
    // ...
    }
    }
    }

建立 Command

  1. Purchasing.UseCases 新增 Command

    1
    2
    3
    4
    5
    6
    7
    8
    // Commands/CreatePurchaseOrderCommand.cs

    using MediatR;

    namespace Purchasing.UseCases.Commands
    {
    public record CreatePurchaseOrderCommand(string UserId) : IRequest<int>;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Handlers/CreatePurchaseOrderHandler.cs

    using MediatR;
    using Purchasing.UseCases.Commands;

    namespace Purchasing.UseCases.Handlers
    {
    public class CreatePurchaseOrderHandler: IRequestHandler<CreatePurchaseOrderCommand, int>
    {
    public async Task<int> Handle(CreatePurchaseOrderCommand request, CancellationToken cancellationToken)
    {
    // ...
    }
    }
    }

建立 Controller

  1. Purchasing.Api 新增 Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // Controllers/PurchaseOrdersController.cs

    using Purchasing.UseCases.Commands;
    using Purchasing.UseCases.Contracts;
    using Purchasing.UseCases.Queries;
    using MediatR;
    using Microsoft.AspNetCore.Mvc;

    namespace Purchasing.Api.Controllers
    {
    [Route("[controller]")]
    [ApiController]
    public class PurchaseOrdersController : ControllerBase
    {
    private readonly IMediator _mediator;

    public PurchaseOrdersController(IMediator mediator)
    {
    _mediator = mediator;
    }

    [HttpGet]
    public async Task<ActionResult<List<PurchaseOrderDto>>> ListPurchaseOrdersAsync([FromQuery] ListPurchaseOrdersQuery query)
    {
    var purchaseOrders = await _mediator.Send(query);

    return purchaseOrders;
    }

    [HttpPost]
    public async Task<ActionResult<int>> CreatePurchaseOrderAsync([FromBody] CreatePurchaseOrderCommand command)
    {
    var purchaseOrderId = await _mediator.Send(command);

    return purchaseOrderId;
    }
    }
    }