本篇分析CQRS架構下的Equinox開源項目。該項目在github上star佔有2.4k。便決定分析Equinox項目來學習下CQRS架構。再講CQRS架構時,先簡述下DDD風格,在DDD分層架構中,通常包含表現層、應用程序層(應用服務層)、領域層(領域服務層)、基礎設施層。在DDD中講到服務這個術語時,好比領域服務,應用層服務等,這個服務是指業務邏輯,而不是指任何技術如wcf,web服務。前端
下圖是從經典三層構架演變爲DDD下的分層架構圖:git
1.表現層github
表現層前端日後端post的數據稱"輸入模型(InputModel)",後端控制器傳給前端要顯示的數據稱"視圖模型(ViewModel)",大多時候視圖模型與輸入模型是重合的,所在在下面要介紹的開源項目中,做者在應用服務層只定義了ViewModels文件夾。例如在MVC中,控制器裏只是編排任務,調用應用程序層。在控制器中代碼塊應該儘量輕薄,主要做用是找出層與層之間的分離,控制器只是業務邏輯佔位符。web
在表現層中與運行環境密切相連,表現層須要關注的是http上下文、會話狀態等。sql
2. 應用服務層數據庫
能夠在應用服務層引用領域層和基礎設施層,是在領域層之上編排業務用例的服務。該層對業務規則一無所知,不會包含任何與業務有關的狀態信息。該層關鍵特色:json
(1) 該層是針對不一樣的前端。該層與表現層有關,是爲表現層服務。不一樣的表現層(移動,webapi, web)都有本身的應用服務層。該層與表現層屬於系統的前端。後端
(2) 應用服務層多是有狀態的,至少就UI任務進度而言。api
(3) 它從表現層獲取輸入模型,而後把視圖模型返回去。緩存
3. 領域層
領域層是最重要和最複雜的一層。在DDD的領域模型架構下。該層包含了全部針對一個或多個用例業務邏輯,領域層包含一個領域模型和一組可能的服務。
領域模型大多時候是一個實體關係模型,能夠由方法組成。是擁有數據和行爲。若是缺乏重要行爲,那就是一個數據結構,稱爲貧血模型。領域模型是實現統一語言和表達業務流程所需的操做。
領域層包含的服務是領域服務,是涉及多個領域模型而沒法放個單個領域模型中的領域邏輯。領域服務是一個類,包含了多個領域模型實體的行爲。領域服務一般也須要訪問基礎設施層。
在DDD的CQRS架構下,使用二個不一樣的領域層,而不是一個(在Equinox項目中混合成一個)。這種分離把查詢操做放在一層(查詢領域層),把命令操做放在另外一層(命令領域層)。在CQRS裏,查詢棧僅僅基於SQL查詢,能夠徹底沒有模型、應用程序層和領域層。查詢領域層只須要貧血模型類DTO來作傳輸對象。
4. 基礎設施層
這層使用具體技術有關的任何東西:O/RM工具的數據訪問持久層、IOC容器的實現(Unity)、以及不少其它橫切關注點的實現,如安全(Oauth2)、日誌記錄、跟蹤、緩存等。最突出的組件是持久層。
1.簡介
CQRS是DDD開發風格下對領域模型架構的一種簡化改進。任何業務系統基本都是查詢與寫入,對應CQRS是指命令/查詢責任分離,查詢不以任何方式修改系統狀態,只返回數據。另外一方面,命令(寫入)則修改系統的的狀態,但不返回數據,除了狀態代碼或確認信息。在CQRS裏,查詢棧僅基於sql查詢,能夠徹底沒有模型,應用程序層和領域層。CQRS方案還能夠爲命令棧和查詢棧準備不一樣的數據庫(讀與寫)。
2.CQRS的好處
(1)是簡化設計下降複雜性,對於查詢來講,能夠直接讀取基礎設施層的倉儲。
(2)是加強可伸縮性的潛能。好比讀取是主導操做,能夠引入某種程序的緩存,極大減小訪問數據庫的次數。好比寫入在高峯期減慢系統,能夠考慮從經典的同步寫入模型換到異步寫入甚至命令隊列。分離了查詢和命令,能夠徹底隔離處理這兩個部分的可伸縮性。
3.CQRS實現全局圖
在全局圖中,右圖經過虛線表示雙重分層架構,分開了命令通道和查詢通道,每一個通道都有獨立架構。在命令通道里,任何來自表現層的請求都會變成一個命令,並加入處處理器隊列。每一個命令都攜帶信息。每一個命令都是一個邏輯單元,能夠充分地驗證相關對象的狀態,智能的決定執行哪些更新以及拒絕哪些更新。處理命令可能會產生事件(事件一般是記錄命令發生的事情),這些事件會被其它註冊組件處理。
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項目結構以下所示:
對於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項目結構以下:
在上面結構中,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作整個解決方案下項目須要的服務注入。
參考文獻:
Microsoft.NET企業級應用架構設計 第二版