DDD領域驅動設計是什麼DDD領域驅動設計:實體、值物件、聚合根DDD領域驅動設計:倉儲MediatR一個優秀的.NET中介者框架2 什麼是CQRS?
CQRS,即命令和查詢職責分離,是一種分離資料讀取與寫入的體系結構模式。 基本思想是把系統劃分為兩個界限:
查詢,不改變系統的狀態,且沒有副作用。命令,更改系統狀態。我們透過Udi Dahan的《Clarified CQRS》文章中的圖來介紹一下:
2.1 查詢 (Query)上圖中,可以看到Query不是透過DB來查詢,而是透過一個專門用於查詢的Cache(或ReadDB),ReadDB中的表是專門針對UI最佳化過的,例如最新的產品列表,銷量最好的產品列表等,基本屬於用空間換時間。
2.2 命令 (Command)上圖中,Command類似於Application Service,Command中主要做的事情有兩個:1、透過呼叫領域層,把相關業務資料寫入到DB中。2、同時更新ReadDB。
2.3 領域事件 (Domain Event)上圖中,更新ReadDB有兩種方式,一種是直接在Command中進行更新,還有一種監聽領域事件,把相應更改的資料同步到ReadDB中。
3 如何實現CQRS?我們在這裡使用最簡單的方法:只將查詢與命令分離,且執行這兩種操作時使用相同的資料庫。
3.1 命令 (Command)首先,命令類
命令是讓系統執行更改系統狀態的操作的請求。 命令具有命令性,且應僅處理一次。
由於命令具有命令性,所以通常採用命令語氣使用謂詞(如“create”或“update”)命名,命令可能包括聚合型別,例如 CreateTodoCommand 與事件不同,命令不是過去發生的事實,它只是一個請求,因此可以拒絕它。
命令可能源自 UI,由使用者發出請求而產生,也可能來自程序管理器,由程序管理器指導聚合執行操作而產生。
命令的一個重要特徵是它應該由單一接收方處理,且僅處理一次。 這是因為命令是要在應用程式中執行的單個操作或事務。 例如,同一個“建立待辦事項”的處理次數不應超過一次。 這是命令和事件之間的一個重要區別。 事件可能會經過多次處理,因為許多系統或微服務可能會對該事件感興趣。
命令透過包含資料欄位或集合(其中包含執行命令所需的所有資訊)的類實現。 命令是一種特殊的資料傳輸物件 (DTO),專門用於請求更改或事務。 命令本身完全基於處理命令所需的資訊,別無其他。
下面的示例顯示了簡化的 CreateTodoCommand 類。
public class CreateTodoCommand : IRequest<TodoDTO>{ public Guid Id { get; set; } public string Name { get; set; }}
然後,命令處理程式類
應為每個命令實現特定命令處理程式類。 這是該模式的工作原理,是應用命令物件、域物件和基礎結構儲存庫物件的情景。
命令處理程式收到命令,並從使用的聚合獲取結果。 結果應為成功執行命令,或者異常。 出現異常時,系統狀態應保持不變。
命令處理程式通常執行以下步驟:
它接收 DTO 等命令物件。它會驗證命令是否有效。它會例項化作為當前命令目標的聚合根例項。它會在聚合根例項上執行方法,從命令獲得所需資料。它將聚合的新狀態保持到相關資料庫。通常情況下,命令處理程式處理由聚合根(根實體)驅動的單個聚合。 如果多個聚合應受到單個命令接收的影響,可使用域事件跨多個聚合傳播狀態或操作。
作為命令處理程式類的示例,下面的程式碼演示本章開頭介紹的同一個 CreateTodoCommandHandler 類。 這個示例還強調了 Handle 方法以及域模型物件/聚合的操作。
public class CreateTodoCommandHandler : IRequestHandler<CreateTodoCommand, TodoDTO>{ private readonly IRepository repository; private readonly IMapper mapper; public CreateTodoCommandHandler(IRepository repository, IMapper mapper) { this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } public async Task<TodoDTO> Handle(CreateTodoCommand message, CancellationToken cancellationToken) { var todo = Todo.Create(message.Name); repository.Entry(todo); await repository.SaveAsync(); var todoForDTO = mapper.Map<TodoDTO>(todo); return todoForDTO; }}
最後,透過MediatR實現命令程序管道首先,讓我們看一下示例 WebAPI 控制器,你會在其中使用MediatR,如以下示例所示:
[Route("api/[controller]")][ApiController]public class TodosController : ControllerBase{ //... private readonly MediatR.IMediator mediator; public TodosController(MediatR.IMediator mediator) { this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } //...}
在控制器方法中,將命令傳送到MediatR的程式碼幾乎只有一行:
[HttpPost]public async Task<ActionResult<TodoDTO>> Create(CreateTodoCommand param){ var ret = await mediator.Send(param); return CreatedAtAction(nameof(Get), new { id = ret.Id }, ret);}
3.2 查詢 (Query)首先,定義DTO
[Table("T_Todo")]public class TodoDTO{ #region Public Properties public Guid Id { get; set; } public string Name { get; set; } #endregion }
然後,建立具體的查詢方法
public class TodoQueries{ private readonly TodoingQueriesContext context; public TodoQueries(TodoingQueriesContext context) { this.context = context; } //... public async Task<PaginatedItems<TodoDTO>> Query(int pageIndex, int pageSize) { var total = await context.Todos .AsNoTracking() .CountAsync(); var todos = await context.Todos .AsNoTracking() .OrderBy(o => o.Id) .Skip(pageSize * (pageIndex - 1)) .Take(pageSize) .ToListAsync(); return new PaginatedItems<TodoDTO>(total, todos); } //...}
請注意TodoingQueriesContext和命令處理中的Context不是同一個,實現查詢端除了用EFCore、還可以用儲存過程、檢視、具體化檢視或Dapper等等。
最後,呼叫查詢方法
[Route("api/[controller]")][ApiController]public class TodosController : ControllerBase{ private readonly TodoQueries todoQueries; public TodosController(TodoQueries todoQueries) { this.todoQueries = todoQueries ?? throw new ArgumentNullException(nameof(todoQueries)); } //... [HttpGet] public async Task<ActionResult<PaginatedItems<TodoDTO>>> Query(int pageIndex, int pageSize) { return todoQueries.Query(pageIndex, pageSize).Result; } //...}