CQRS(一)
CQRS 已經使用一陣子了,從接觸到專案正式使用,也經過不少風雨,趁現在比較有空,來整理一下學習過程。
CQRS(命令查詢職責分離,Command Query Responsibility Segregation)是一種軟體架構模式,用於將讀取和寫入操作分離。這種模式通常應用於複雜的系統中,以提高可擴展性和效能。在CQRS中,命令(Commands)用於執行寫入操作,而查詢(Queries)則用於執行讀取操作。透過將這兩種操作分開處理,可以針對不同的需求進行優化和擴展。
前言
CQRS 在C#
實作上通常與 MediatR 放在一起,但事實上也不一定要使用 MediatR 才能使用,雖然打算寫系列文章,但其實每篇都是獨立的,沒有強制一定要在專案導入第三方插件,依賴越多,對專案不一定是好事。
關於 DDD(domain-driven design)
另外 CQRS 大部分也與 DDD 掛勾,但這邊也不是討論DDD的內容,僅是一步一步實現 CQRS 架構的文章,所以內容僅停留在 Application 層為主。
關於 RESTful API
RESTful API 其實可以視為 CQRS 架構的一種,HttpGet
對應 Query,HttpPost
、HttpPut
、HttpDelete
等對應 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 分,我也習慣將 CommandHandler
、Command
放在同一個 Class,減少大量類別的產生,這部分完全看個人偏好。