在閱讀本文章以前,你能夠先閱讀:數據庫
CQRS,即命令和查詢職責分離,是一種分離數據讀取與寫入的體系結構模式。 基本思想是把系統劃分爲兩個界限:api
咱們經過Udi Dahan的《Clarified CQRS》文章中的圖來介紹一下:app
上圖中,能夠看到Query不是經過DB來查詢,而是經過一個專門用於查詢的Cache(或ReadDB),ReadDB中的表是專門針對UI優化過的,例如最新的產品列表,銷量最好的產品列表等,基本屬於用空間換時間。框架
上圖中,Command相似於Application Service,Command中主要作的事情有兩個:一、經過調用領域層,把相關業務數據寫入到DB中。二、同時更新ReadDB。dom
上圖中,更新ReadDB有兩種方式,一種是直接在Command中進行更新,還有一種監聽領域事件,把相應更改的數據同步到ReadDB中。async
咱們在這裏使用最簡單的方法:只將查詢與命令分離,且執行這兩種操做時使用相同的數據庫。微服務
首先,命令類優化
命令是讓系統執行更改系統狀態的操做的請求。 命令具備命令性,且應僅處理一次。ui
因爲命令具備命令性,因此一般採用命令語氣使用謂詞(如「create」或「update」)命名,命令可能包括聚合類型,例如 CreateTodoCommand 與事件不一樣,命令不是過去發生的事實,它只是一個請求,所以能夠拒絕它。this
命令可能源自 UI,由用戶發出請求而產生,也可能來自進程管理器,由進程管理器指導聚合執行操做而產生。
命令的一個重要特徵是它應該由單一接收方處理,且僅處理一次。 這是由於命令是要在應用程序中執行的單個操做或事務。 例如,同一個「建立待辦事項」的處理次數不該超過一次。 這是命令和事件之間的一個重要區別。 事件可能會通過屢次處理,由於許多系統或微服務可能會對該事件感興趣。
命令經過包含數據字段或集合(其中包含執行命令所需的全部信息)的類實現。 命令是一種特殊的數據傳輸對象 (DTO),專門用於請求更改或事務。 命令自己徹底基於處理命令所需的信息,別無其餘。
下面的示例顯示了簡化的 CreateTodoCommand 類。
public class CreateTodoCommand : IRequest<TodoDTO> { public Guid Id { get; set; } public string Name { get; set; } }
而後,命令處理程序類
應爲每一個命令實現特定命令處理程序類。 這是該模式的工做原理,是應用命令對象、域對象和基礎結構存儲庫對象的情景。
命令處理程序收到命令,並從使用的聚合獲取結果。 結果應爲成功執行命令,或者異常。 出現異常時,系統狀態應保持不變。
命令處理程序一般執行如下步驟:
一般狀況下,命令處理程序處理由聚合根(根實體)驅動的單個聚合。 若是多個聚合應受到單個命令接收的影響,可以使用域事件跨多個聚合傳播狀態或操做。
做爲命令處理程序類的示例,下面的代碼演示本章開頭介紹的同一個 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); }
首先,定義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; } //... }