CQRS架构下Equinox开源项目分析

一.DDD分层架构介绍

  本篇分析CQRS架构下的Equinox开源项目。该项目在github上star占有2.4k。便决定分析Equinox项目来学习下CQRS架构。再讲CQRS架构时,先简述下DDD风格,在DDD分层架构中,一般包含表现层、应用程序层(应用服务层)、领域层(领域服务层)、基础设施层。在DDD中讲到服务这个术语时,比如领域服务,应用层服务等,这个服务是指业务逻辑,而不是指任何技术如wcf,web服务。

  下图是从经典三层构架演变为DDD下的分层架构图:

  1.表现层

    表现层前端往后端post的数据称"输入模型(InputModel)",后端控制器传给前端要显示的数据称"视图模型(ViewModel)",大多时候视图模型与输入模型是重合的,所在在下面要介绍的开源项目中,作者在应用服务层只定义了ViewModels文件夹。例如在MVC中,控制器里只是编排任务,调用应用程序层。在控制器中代码块应该尽可能轻薄,主要作用是找出层与层之间的分离,控制器只是业务逻辑占位符。

    在表现层中与运行环境密切相连,表现层需要关注的是http上下文、会话状态等。

  2. 应用服务层

    可以在应用服务层引用领域层和基础设施层,是在领域层之上编排业务用例的服务。该层对业务规则一无所知,不会包含任何与业务有关的状态信息。该层关键特点:

    (1) 该层是针对不同的前端。该层与表现层有关,是为表现层服务。不同的表现层(移动,webapi, web)都有自己的应用服务层。该层与表现层属于系统的前端。

    (2) 应用服务层可能是有状态的,至少就UI任务进度而言。

    (3) 它从表现层获取输入模型,然后把视图模型返回去。

  3. 领域层

    领域层是最重要和最复杂的一层。在DDD的领域模型架构下。该层包含了所有针对一个或多个用例业务逻辑,领域层包含一个领域模型和一组可能的服务。

    领域模型大多时候是一个实体关系模型,可以由方法组成。是拥有数据和行为。如果缺少重要行为,那就是一个数据结构,称为贫血模型。领域模型是实现统一语言和表达业务流程所需的操作。

    领域层包含的服务是领域服务,是涉及多个领域模型而无法放个单个领域模型中的领域逻辑。领域服务是一个类,包含了多个领域模型实体的行为。领域服务通常也需要访问基础设施层。

    在DDD的CQRS架构下,使用二个不同的领域层,而不是一个(在Equinox项目中混合成一个)。这种分离把查询操作放在一层(查询领域层),把命令操作放在另一层(命令领域层)。在CQRS里,查询栈仅仅基于SQL查询,可以完全没有模型、应用程序层和领域层。查询领域层只需要贫血模型类DTO来做传输对象。

  4. 基础设施层

    这层使用具体技术有关的任何东西:O/RM工具的数据访问持久层、IOC容器的实现(Unity)、以及很多其它横切关注点的实现,如安全(Oauth2)、日志记录、跟踪、缓存等。最突出的组件是持久层。

二.CQRS概述

  1.简介

    CQRS是DDD开发风格下对领域模型架构的一种简化改进。任何业务系统基本都是查询与写入,对应CQRS是指命令/查询责任分离,查询不以任何方式修改系统状态,只返回数据。另一方面,命令(写入)则修改系统的的状态,但不返回数据,除了状态代码或确认信息。在CQRS里,查询栈仅基于sql查询,可以完全没有模型,应用程序层和领域层。CQRS方案还可以为命令栈和查询栈准备不同的数据库(读与写)。

  2.CQRS的好处

    (1)是简化设计降低复杂性,对于查询来说,可以直接读取基础设施层的仓储。

    (2)是增强可伸缩性的潜能。比如读取是主导操作,可以引入某种程序的缓存,极大减少访问数据库的次数。比如写入在高峰期减慢系统,可以考虑从经典的同步写入模型换到异步写入甚至命令队列。分离了查询和命令,可以完全隔离处理这两个部分的可伸缩性。

  3.CQRS实现全局图

    在全局图中,右图通过虚线表示双重分层架构,分开了命令通道和查询通道,每个通道都有独立架构。在命令通道里,任何来自表现层的请求都会变成一个命令,并加入到处理器队列。每个命令都携带信息。每个命令都是一个逻辑单元,可以充分地验证相关对象的状态,智能的决定执行哪些更新以及拒绝哪些更新。处理命令可能会产生事件(事件通常是记录命令发生的事情),这些事件会被其它注册组件处理。

三. Equinox开源项目总览

  1.准备环境

    (1)  Github开源地址下载。Full ASP.NET Core 2.2 application with DDD, CQRS and Event Sourcing

    (2)  在sqlserver里执行sql文件GenerateDataBase.sql。

    (3)  修改appsettings.json中的ConnectionStrings的数据库连接地址。

  

  2.项目分层说明

表现层:Equinox.UI.Web、Equinox.Services.Api

应用服务层: Equinox.Application

领域层: Equinox.Domain、Equinox.Domain.Core

基础设施层: Equinox.Infra.Data(EF持久化)

基础设施层下的横切关注点:

  Equinox.Infra.CrossCutting.Bus(事件和命令总线)

  Equinox.Infra.CrossCutting.Identity(用户管理如登录、注册、授权)

   Equinox.Infra.CrossCutting.IoC(控制反转的服务注入)

  3. 项目架构流程梳理图

 

    流程图更正:领域层Equinox.Domain不需要 引用 基础设施层事件总线Equinox.Infra.CrossCutting.Bus。在DDD风格下领域层是独立的,原则上不依赖于其它层。

    

四.表现层分析

  在表现层是Equinox.UI.Web和Equinox.Services.Api 服务。在Equinox.UI.Web下主要是用控制器中的CustomerController来演示CQRS框架的实现,以及AccountController和ManageController的用户登录、注册、退出和用户信息管理。

  对于AccountController和ManageController两个控制器关联着Equinox.Infra.CrossCutting.Identity项目。Identity项目包括了需要用的视图模型、对系统的授权、自定义用户表数据、用户数据同步到数据库的迁移版本管理、邮件和SMS。对于授权方案通过Equinox.Infra.CrossCutting.IoC来注入服务。如下所示:

        // ASP.NET Authorization Polices
services.AddSingleton<IAuthorizationHandler, ClaimsRequirementHandler>();

  Equinox.Services.Api项目实现的功能与Web站点差不多,是通过暴露Web API来实现。下面是表现层的二个项目:

  

五. 应用服务层分析

  Equinox.Application应用服务层包括对AutoMapper的配置管理,通过AutoMapper实现视图模型和领域模型的实体互转。定义ICustomerAppService服务接口供表现层调用,由CustomerAppService类来实现该接口。项目包含了Customer需要的视图模型。还有事件源EventSource。

  由CustomerAppService类来实现表现层的查询、命令、获取事件源。项目结构如下:

六.领域层Domain.Core分析

  领域层是项目分层架构中,最重要的一层,也是相对复杂的一层。该层作者用了二个项目包括:Domain.Core和Domain项目结构如下所示:

  对于Domain.Core项目主要是定义命令和事件的基类。源头是定义的抽象类Message。对于命令和事件,任何前端都会发送消息给应用程序层, Message消息就是数据传输对象,通常消息定义为一个Message基类开始,作为数据容器。

  这里使用MediatR中间件作为命令和事件的实现。MediatR支持两种消息类型:Request/Response和Notification。先看下Message消息基类定义:

    //注入服务
services.AddMediatR(typeof(Startup));
    /// <summary>
/// Message消息
/// 放入通用属性,甚至是普通标记,没有属性
/// </summary>
public abstract class Message : IRequest<bool>
{
/// <summary>
/// 消息类型:实现Message的命令或事件类型
/// </summary>
public string MessageType { get; protected set; } /// <summary>
/// 聚合ID
/// </summary>
public Guid AggregateId { get; protected set; } protected Message()
{
MessageType = GetType().Name;
}
}

  

  消息有二种:命令和事件。两种消息都包含了数据传输对象。命令和事件有些微妙差别,命令和事件都是Message派生类。

    /// <summary>
/// Event 领域消息
/// 事件类是不可变的,它表示已经发生的事情,意味着只有私有set,没有写入方法。
/// 事件存放通用属性,例如事件触发时间,触发的用户,数据版本号。
/// </summary>
public abstract class Event : Message, INotification
{
public DateTime Timestamp { get; private set; } protected Event()
{
//事件时间
Timestamp = DateTime.Now;
}
}
    /// <summary>
/// Command领域命令(增删改),不返回任何结果(void),但会改变数据对象的状态。
/// </summary>
public abstract class Command : Message
{
public DateTime Timestamp { get; private set; } //DTO绑定验证,使用Fluent API来实现
public ValidationResult ValidationResult { get; set; } protected Command()
{
//命令时间
Timestamp = DateTime.Now;
} //实现Command抽象类的DTO数据验证
public abstract bool IsValid();
}

  

  Domain.Core项目还定义了领域实体和领域值对象的基类实现。例如:在领域实体基类中实现了相等性、运算符重载、重写HashCode。对于实体和值对象主要区别是:实体有明确的身份标识如主键ID,GUID。

      public abstract class Entity
  public abstract class ValueObject<T> where T : ValueObject<T>

  

  Domain.Core项目中的Notifications消息文件夹,用来确认消息发送后的处理状态。下面是表现层发送更新命令后,IsValidOperation()确认消息处理的状态情况。

        [HttpPost]
[Authorize(Policy = "CanWriteCustomerData")]
[Route("customer-management/edit-customer/{id:guid}")]
[ValidateAntiForgeryToken]
public IActionResult Edit(CustomerViewModel customerViewModel)
{
if (!ModelState.IsValid) return View(customerViewModel); _customerAppService.Update(customerViewModel); if (IsValidOperation())
ViewBag.Sucesso = "Customer Updated!"; return View(customerViewModel);
}

  

  Domain.Core项目中的Bus文件夹,用来做命令总线和事件总线的发送接口,由Equinox.Infra.CrossCutting.Bus项目来实现总线接口的发送。

七.领域层Domain分析

  下面是Domain项目结构如下:

  在上面结构中,Commands和Events文件夹分别用来存储命令和事件的数据传输对象,是贫血的DTO类,也可以理解为领域实体。例如Commands文件夹下命令数据传输对象定义:

     /// <summary>
/// Customer数据转输对象抽象类,放Customer通过属性
/// </summary>
public abstract class CustomerCommand : Command
{
public Guid Id { get; protected set; } public string Name { get; protected set; } public string Email { get; protected set; } public DateTime BirthDate { get; protected set; }
}
    /// <summary>
/// Customer注册命令消息参数
/// </summary>
public class RegisterNewCustomerCommand : CustomerCommand
{
public RegisterNewCustomerCommand(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
} /// <summary>
/// 命令信息参数验证
/// </summary>
/// <returns></returns>
public override bool IsValid()
{
ValidationResult = new RegisterNewCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}

  

  当在应用服务层发送命令(Bus.SendCommand)后,由领域层的CommandHandlers文件夹下的类来处理命令,再调用EF持久层来改变实体状态。下面梳理下命令的执行流程,由表现层开始一个customer新增如下所示:

    当在表现层点击Create后,调用应用服务层Register方法,触发一个新增事件,代码如下:

        /// <summary>
/// 新增
/// </summary>
/// <param name="customerViewModel">视图模型</param>
public void Register(CustomerViewModel customerViewModel)
{
//将视图模型 映射到 RegisterNewCustomerCommand 新增命令实体
var registerCommand = _mapper.Map<RegisterNewCustomerCommand>(customerViewModel);
Bus.SendCommand(registerCommand);
}

     当SendCommand发送命令后,由领域层CustomerCommandHandler类中的Handle来处理该命令,如下所示:

         /// <summary>
/// Customer注册命令处理
/// </summary>
/// <param name="message"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<bool> Handle(RegisterNewCustomerCommand message, CancellationToken cancellationToken)
{
//对实体属性进行验证
if (!message.IsValid())
{
NotifyValidationErrors(message);
return Task.FromResult(false);
} //将命令消息转成领域实体
var customer = new Customer(Guid.NewGuid(), message.Name, message.Email, message.BirthDate); //如果注册用户邮件已存在,发起一个事件
if (_customerRepository.GetByEmail(customer.Email) != null)
{
Bus.RaiseEvent(new DomainNotification(message.MessageType, "The customer e-mail has already been taken."));
return Task.FromResult(false);
} //由Equinox.Infra.Data.Repository来实现数据持久化。事件是过去在系统中发生的事情。该事件通常是命令的结果.
_customerRepository.Add(customer); //新增成功后,使用事件记录这次命令。
if (Commit())
{
Bus.RaiseEvent(new CustomerRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate));
} return Task.FromResult(true);
}

    下面是注册customer的信息,以及注册产生的事件数据,如下所示:

  在领域层的Interfaces文件夹中,最重要的包括IRepository<TEntity>接口,是通过Equinox.Infra.Data.Repository来实现接口,来进行数据持久化。下面是领域层仓储接口:

    /// <summary>
/// 领域层仓储接口,定义了通用的方法
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IRepository<TEntity> : IDisposable where TEntity : class
{
void Add(TEntity obj);
TEntity GetById(Guid id);
IQueryable<TEntity> GetAll();
void Update(TEntity obj);
void Remove(Guid id);
int SaveChanges();
}
    /// <summary>
/// Customer仓储接口,在基数仓储上扩展
/// </summary>
public interface ICustomerRepository : IRepository<Customer>
{
Customer GetByEmail(string email);
}

  Interfaces文件夹中还定义了IUser和IUnitOfWork接口类,也是需要Equinox.Infra.Data.Repository来实现。

八. 基础设施层分析

  Equinox.Infra.Data项目是EF用来持久化命令和事件,以及查询数据的仓储,结构如下:

  其中UoW文件夹下的UnitOfWork类用来实现领域层的IUnitOfWork,使用Commit保存数据。

      public bool Commit()
{
return _context.SaveChanges() > 0;
}

  Repository文件夹下的类用来实现领域层的IRepository接口,使用EF的DbSet来操作EF TEntity对象,再调用Commit提交到数据库。

      public virtual void Add(TEntity obj)
{
DbSet.Add(obj);
}

  Repository文件夹下还包含EventSourcing事件源,存储到StoredEvent表中。

九.命令总线分析

  Equinox.Infra.CrossCutting.Bus项目中使用了中间件MediatR,定义了InMemoryBus类来实现领域层的IMediatorHandler命令总线接口发送,使用SendCommand (T)和RaiseEvent (T)方法发送命令和事件。

  MediatR是用于消息发送和消息处理的解耦,MediatR是一种进程内消息传递机制。 支持以同步或异步的形式进行请求/响应,命令,查询,通知和事件的消息传递,并通过C#泛型支持消息的智能调度。 其中IRequest和INotification分别对应单播和多播消息的抽象。

  例如:在领域层中,Message消息实现IRequest,代码如下:

    /// <summary>
/// Message消息
/// 放入通用属性,甚至是普通标记,没有属性。IRequest<T> - 有返回值
/// </summary>
public abstract class Message : IRequest<bool>

  

  最后Equinox.Infra.CrossCutting.Identity主要做用户管理,授权,迁移管理。Equinox.Infra.CrossCutting.IoC做整个解决方案下项目需要的服务注入。

参考文献:

  Introduction-to-CQRS

  Microsoft.NET企业级应用架构设计 第二版

Equinox开源项目CQRS架构分析的更多相关文章

  1. b2c项目基础架构分析(二)前端框架 以及补漏的第一篇名词解释

    继续上篇,上篇里忘记了也很重要的前端部分,今天的网站基本上是以一个启示页,然后少量的整页切换,大量的浏览器后台调用web服务局部.动态更新页面显示状态这种方式在运作的,从若干年前简单的ajax流行起来 ...

  2. b2c项目基础架构分析(一)b2c 大型站点方案简述 已补充名词解释

    我最近一直在找适合将来用于公司大型bs,b2b b2c的基础架构. 实际情况是要建立一个bs架构b2b.b2c的网站,当然还包括wap站点.手机app站点. 一.现有公司技术人员现状: 1.熟悉asp ...

  3. 开源项目kcws代码分析--基于深度学习的分词技术

    http://blog.csdn.net/pirage/article/details/53424544 分词原理 本小节内容参考待字闺中的两篇博文: 97.5%准确率的深度学习中文分词(字嵌入+Bi ...

  4. asp.net core系列 62 CQRS架构下Equinox开源项目分析

    一.DDD分层架构介绍 本篇分析CQRS架构下的Equinox开源项目.该项目在github上star占有2.4k.便决定分析Equinox项目来学习下CQRS架构.再讲CQRS架构时,先简述下DDD ...

  5. Android 开源项目及其学习

    Android 系统研究:http://blog.csdn.net/luoshengyang/article/details/8923485 Android 腾讯技术人员博客 http://hukai ...

  6. 机器学习 Top 20 Python 开源项目

    转自:http://mp.weixin.qq.com/s?__biz=MzA4MjEyNTA5Mw==&mid=2652565022&idx=1&sn=9aa035097120 ...

  7. Python优秀开源项目Rich源码解析

    这篇文章对优秀的开源项目Rich的源码进行解析,OMG,盘他.为什么建议阅读源码,有两个原因,第一,单纯学语言很难在实践中灵活应用,通过阅读源码可以看到每个知识点的运用场景,印象会更深,以后写代码的时 ...

  8. 【.NET Core项目实战-统一认证平台】第一章 功能及架构分析

    [.NET Core项目实战-统一认证平台]开篇及目录索引 从本文开始,我们正式进入项目研发阶段,首先我们分析下统一认证平台应该具备哪些功能性需求和非功能性需求,在梳理完这些需求后,设计好系统采用的架 ...

  9. Diycode开源项目 BaseApplication分析+LeakCanary第三方+CrashHandler自定义异常处理

    1.BaseApplication整个应用的开始 1.1.看一下代码 /* * Copyright 2017 GcsSloop * * Licensed under the Apache Licens ...

随机推荐

  1. How would you differentiate JDK, JRE, JVM, and JIT?

    Q5. How would you differentiate JDK, JRE, JVM, and JIT?A5. There is no better way to get the big pic ...

  2. html css div固定底部

    <div id="father"> <footer></footer> </div> #father{ position:relat ...

  3. [转载]sql server死锁

    SQL Server Deadlocks by Examplehttps://www.red-gate.com/simple-talk/sql/performance/sql-server-deadl ...

  4. Windows上使用Linux命令

    WSL Windows Subsystem for Linux(简称WSL)是一个在Windows 10上能够运行原生Linux二进制可执行文件(ELF格式)的兼容层.它是由微软与Canonical公 ...

  5. Spring Could Stream 基本用法

    Spring Cloud Stream是一个建立在Spring Boot和Spring Integration之上的框架,有助于创建事件驱动或消息驱动的微服务. 通过它可以更方便的访问消息服务,如消费 ...

  6. LSTM的神经元个数

    小书匠深度学习 目录: 1.LSTM简单介绍 2.简单假设样例 3.神经元分析 3.1忘记门层 3.2细胞状态 3.3输出层 3.4总结 4.测试 1.LSTM简单介绍 LSTM在时间上展开 红框从左 ...

  7. Cookie 的 SameSite 属性

    转自http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSit ...

  8. [算法模板]ST表

    [算法模板]ST表 ST表和线段树一样,都能解决RMQ问题(范围最值查询-Range Minimum Query). 我们开一个数组数组\(f[maxn][maxn\log_2]\)来储存数据. 定义 ...

  9. [Gamma阶段]第九次Scrum Meeting

    Scrum Meeting博客目录 [Gamma阶段]第九次Scrum Meeting 基本信息 名称 时间 地点 时长 第九次Scrum Meeting 19/06/05 大运村寝室6楼 20min ...

  10. [Beta]Scrum Meeting#5

    github 本次会议项目由PM召开,时间为5月10日晚上10点30分 时长15分钟 任务表格 人员 昨日工作 下一步工作 木鬼 撰写博客整理文档 撰写博客整理文档 swoip 改进界面 改进界面 b ...