Contents

CQRS(一)

CQRS(一)

CQRS 已經使用一陣子了,從接觸到專案正式使用,也經過不少風雨,趁現在比較有空,來整理一下學習過程。

CQRS(命令查詢職責分離,Command Query Responsibility Segregation)是一種軟體架構模式,用於將讀取寫入操作分離。這種模式通常應用於複雜的系統中,以提高可擴展性和效能。在CQRS中,命令(Commands)用於執行寫入操作,而查詢(Queries)則用於執行讀取操作。透過將這兩種操作分開處理,可以針對不同的需求進行優化和擴展。

前言

關於 MediatR

CQRS 在C#實作上通常與 MediatR 放在一起,但事實上也不一定要使用 MediatR 才能使用,雖然打算寫系列文章,但其實每篇都是獨立的,沒有強制一定要在專案導入第三方插件,依賴越多,對專案不一定是好事。

關於 DDD(domain-driven design)

另外 CQRS 大部分也與 DDD 掛勾,但這邊也不是討論DDD的內容,僅是一步一步實現 CQRS 架構的文章,所以內容僅停留在 Application 層為主。

關於 RESTful API

RESTful API 其實可以視為 CQRS 架構的一種,HttpGet對應 Query,HttpPostHttpPutHttpDelete等對應 Command,但我認為這並不是絕對的,例如帳密查詢,有些情況下,會使用Post 作為傳送資料,另外有些查詢帳密會寫額外的紀錄,如限定搜尋次數等,這部分或許用命令Command,在語意上更加符合意境。

建立命令 Command 和查詢 Query 介面

CQRS 的核心思想是將資料的讀取和寫入分開處理,並使用不同的模型來處理這兩種操作。這樣可以使系統更加靈活,並且可以根據不同的需求進行優化和擴展,所以第一步就是先將命令和查詢分開,並且建立對應的介面。

1
2
3
4
5
6
7
8
public interface ICommandHandler<TCommand> where TCommand : class
{
    Task<bool> Execute(TCommand command);
}

public interface ICommand
{
}
1
2
3
4
5
6
7
8
public interface IQueryHandler<in TQuery, TResult> where TQuery : IQuery<TResult>
{
    Task<TResult> Execute(TQuery query);
}

public interface IQuery<TResult>
{
}

實作 Command

我們直接使用微軟內建的天氣預報來作範例,並加入國家與城市,所有的命令都實作ICommand

1
2
3
4
5
6
7
8
public class CreateWeatherForecastCommand : ICommand
{
    public string Nation { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public string? Summary { get; set; }
}

實作 CommandHandler

如果是採用 DDD 的結構,這層依然是很薄的,將主要邏輯留在核心層

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CreateWeatherForecastCommandHandler : ICommandHandler<CreateWeatherForecastCommand>
{
    private readonly IWeatherForecastRepository _weatherForecastRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateWeatherForecastCommandHandler(IWeatherForecastRepository weatherForecastRepository, IUnitOfWork unitOfWork)
    {
        _weatherForecastRepository = weatherForecastRepository ?? throw new ArgumentNullException(nameof(weatherForecastRepository));
        _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
    }

    public async Task<bool> Execute(CreateWeatherForecastCommand command)
    {
        WeatherForecast weatherForecast = new(command.Nation, command.City, command.Date, command.TemperatureC)
        {
            Summary = command.Summary,
        };

        _weatherForecastRepository.AddWeatherForecast(weatherForecast);

        await _unitOfWork.SaveChangesAsync();
        return true;
    }
}

Controller 調用命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast(
    [FromBody] CreateWeatherForecastRequest request,
    [FromServices] CreateWeatherForecastCommandHandler handler)
{
    var command = new CreateWeatherForecastCommand
    {
        Nation = request.Nation,
        City = request.City,
        Date = request.Date,
        TemperatureC = request.TemperatureC,
        Summary = request.Summary,
    };

    var result = await handler.Execute(command);

    return result ? Ok() : BadRequest();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CreateWeatherForecastRequest
{
    [Display(Name = "國家")]
    [Required(ErrorMessage = "{0} 為必填欄位")]
    [StringLength(50, ErrorMessage = "{0} 最多可輸入 {1} 字")]
    public string Nation { get; set; } = string.Empty;

    [Display(Name = "城市")]
    [Required(ErrorMessage = "{0} 為必填欄位")]
    [StringLength(50, ErrorMessage = "{0} 最多可輸入 {1} 字")]
    public string City { get; set; } = string.Empty;

    public DateTime Date { get; set; }

    [Display(Name = "溫度")]
    [Range(-50, 50, ErrorMessage = "{0} 必須介於 {1} 至 {2}")]
    public int TemperatureC { get; set; }

    public string? Summary { get; set; }
}

專案結構

結構上並沒有嚴格限定,有些人習慣以 CRUD 資料夾分類,個人是習慣以 Commands 和 Queries 分,我也習慣將 CommandHandlerCommand放在同一個 Class,減少大量類別的產生,這部分完全看個人偏好。

/static/CQRS(一)_0f589462225f477fb9b66ccc608bd2e1/2023-10-03_14-42-42.png