在阅读本文章以前,你能够先阅读:数据库
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; } //... }