CQRS架构下Equinox开源项目分析
一.DDD分層架構(gòu)介紹
本篇分析CQRS架構(gòu)下的Equinox開源項目。該項目在github上star占有2.4k。便決定分析Equinox項目來學(xué)習(xí)下CQRS架構(gòu)。再講CQRS架構(gòu)時,先簡述下DDD風(fēng)格,在DDD分層架構(gòu)中,一般包含表現(xiàn)層、應(yīng)用程序?qū)?應(yīng)用服務(wù)層)、領(lǐng)域?qū)?領(lǐng)域服務(wù)層)、基礎(chǔ)設(shè)施層。在DDD中講到服務(wù)這個術(shù)語時,比如領(lǐng)域服務(wù),應(yīng)用層服務(wù)等,這個服務(wù)是指業(yè)務(wù)邏輯,而不是指任何技術(shù)如wcf,web服務(wù)。
下圖是從經(jīng)典三層構(gòu)架演變?yōu)镈DD下的分層架構(gòu)圖:
1.表現(xiàn)層
表現(xiàn)層前端往后端post的數(shù)據(jù)稱"輸入模型(InputModel)",后端控制器傳給前端要顯示的數(shù)據(jù)稱"視圖模型(ViewModel)",大多時候視圖模型與輸入模型是重合的,所在在下面要介紹的開源項目中,作者在應(yīng)用服務(wù)層只定義了ViewModels文件夾。例如在MVC中,控制器里只是編排任務(wù),調(diào)用應(yīng)用程序?qū)印T诳刂破髦写a塊應(yīng)該盡可能輕薄,主要作用是找出層與層之間的分離,控制器只是業(yè)務(wù)邏輯占位符。
在表現(xiàn)層中與運行環(huán)境密切相連,表現(xiàn)層需要關(guān)注的是http上下文、會話狀態(tài)等。
2. 應(yīng)用服務(wù)層
可以在應(yīng)用服務(wù)層引用領(lǐng)域?qū)雍突A(chǔ)設(shè)施層,是在領(lǐng)域?qū)又暇幣艠I(yè)務(wù)用例的服務(wù)。該層對業(yè)務(wù)規(guī)則一無所知,不會包含任何與業(yè)務(wù)有關(guān)的狀態(tài)信息。該層關(guān)鍵特點:
(1) 該層是針對不同的前端。該層與表現(xiàn)層有關(guān),是為表現(xiàn)層服務(wù)。不同的表現(xiàn)層(移動,webapi, web)都有自己的應(yīng)用服務(wù)層。該層與表現(xiàn)層屬于系統(tǒng)的前端。
(2) 應(yīng)用服務(wù)層可能是有狀態(tài)的,至少就UI任務(wù)進度而言。
(3) 它從表現(xiàn)層獲取輸入模型,然后把視圖模型返回去。
3. 領(lǐng)域?qū)?/strong>
領(lǐng)域?qū)邮亲钪匾妥顝?fù)雜的一層。在DDD的領(lǐng)域模型架構(gòu)下。該層包含了所有針對一個或多個用例業(yè)務(wù)邏輯,領(lǐng)域?qū)影粋€領(lǐng)域模型和一組可能的服務(wù)。
領(lǐng)域模型大多時候是一個實體關(guān)系模型,可以由方法組成。是擁有數(shù)據(jù)和行為。如果缺少重要行為,那就是一個數(shù)據(jù)結(jié)構(gòu),稱為貧血模型。領(lǐng)域模型是實現(xiàn)統(tǒng)一語言和表達業(yè)務(wù)流程所需的操作。
領(lǐng)域?qū)影姆?wù)是領(lǐng)域服務(wù),是涉及多個領(lǐng)域模型而無法放個單個領(lǐng)域模型中的領(lǐng)域邏輯。領(lǐng)域服務(wù)是一個類,包含了多個領(lǐng)域模型實體的行為。領(lǐng)域服務(wù)通常也需要訪問基礎(chǔ)設(shè)施層。
在DDD的CQRS架構(gòu)下,使用二個不同的領(lǐng)域?qū)?#xff0c;而不是一個(在Equinox項目中混合成一個)。這種分離把查詢操作放在一層(查詢領(lǐng)域?qū)?,把命令操作放在另一層(命令領(lǐng)域?qū)?。在CQRS里,查詢棧僅僅基于SQL查詢,可以完全沒有模型、應(yīng)用程序?qū)雍皖I(lǐng)域?qū)印2樵冾I(lǐng)域?qū)又恍枰氀P皖怐TO來做傳輸對象。
4. 基礎(chǔ)設(shè)施層
這層使用具體技術(shù)有關(guān)的任何東西:O/RM工具的數(shù)據(jù)訪問持久層、IOC容器的實現(xiàn)(Unity)、以及很多其它橫切關(guān)注點的實現(xiàn),如安全(Oauth2)、日志記錄、跟蹤、緩存等。最突出的組件是持久層。
二.CQRS概述
1.簡介
CQRS是DDD開發(fā)風(fēng)格下對領(lǐng)域模型架構(gòu)的一種簡化改進。任何業(yè)務(wù)系統(tǒng)基本都是查詢與寫入,對應(yīng)CQRS是指命令/查詢責(zé)任分離,查詢不以任何方式修改系統(tǒng)狀態(tài),只返回數(shù)據(jù)。另一方面,命令(寫入)則修改系統(tǒng)的的狀態(tài),但不返回數(shù)據(jù),除了狀態(tài)代碼或確認(rèn)信息。在CQRS里,查詢棧僅基于sql查詢,可以完全沒有模型,應(yīng)用程序?qū)雍皖I(lǐng)域?qū)印QRS方案還可以為命令棧和查詢棧準(zhǔn)備不同的數(shù)據(jù)庫(讀與寫)。
2.CQRS的好處
(1)是簡化設(shè)計降低復(fù)雜性,對于查詢來說,可以直接讀取基礎(chǔ)設(shè)施層的倉儲。
(2)是增強可伸縮性的潛能。比如讀取是主導(dǎo)操作,可以引入某種程序的緩存,極大減少訪問數(shù)據(jù)庫的次數(shù)。比如寫入在高峰期減慢系統(tǒng),可以考慮從經(jīng)典的同步寫入模型換到異步寫入甚至命令隊列。分離了查詢和命令,可以完全隔離處理這兩個部分的可伸縮性。
3.CQRS實現(xiàn)全局圖
在全局圖中,右圖通過虛線表示雙重分層架構(gòu),分開了命令通道和查詢通道,每個通道都有獨立架構(gòu)。在命令通道里,任何來自表現(xiàn)層的請求都會變成一個命令,并加入到處理器隊列。每個命令都攜帶信息。每個命令都是一個邏輯單元,可以充分地驗證相關(guān)對象的狀態(tài),智能的決定執(zhí)行哪些更新以及拒絕哪些更新。處理命令可能會產(chǎn)生事件(事件通常是記錄命令發(fā)生的事情),這些事件會被其它注冊組件處理。
三.?Equinox開源項目總覽
1.準(zhǔn)備環(huán)境
(1) ?Github開源地址下載。Full ASP.NET Core 2.2 application with DDD, CQRS and Event Sourcing
(2)? 在sqlserver里執(zhí)行sql文件GenerateDataBase.sql。
(3)? 修改appsettings.json中的ConnectionStrings的數(shù)據(jù)庫連接地址。
2.項目分層說明
? ? ? ? ? ? ? ? ? ?表現(xiàn)層:Equinox.UI.Web、Equinox.Services.Api
?????????????????? 應(yīng)用服務(wù)層: Equinox.Application
?????????????????? 領(lǐng)域?qū)? Equinox.Domain、Equinox.Domain.Core
?????????????????? 基礎(chǔ)設(shè)施層: Equinox.Infra.Data(EF持久化)
?????????????????? 基礎(chǔ)設(shè)施層下的橫切關(guān)注點:
?????????????????? Equinox.Infra.CrossCutting.Bus(事件和命令總線)
?????????????????? Equinox.Infra.CrossCutting.Identity(用戶管理如登錄、注冊、授權(quán))
?????????????????? Equinox.Infra.CrossCutting.IoC(控制反轉(zhuǎn)的服務(wù)注入)
3. 項目架構(gòu)流程梳理圖
四.表現(xiàn)層分析
在表現(xiàn)層是Equinox.UI.Web和Equinox.Services.Api 服務(wù)。在Equinox.UI.Web下主要是用控制器中的CustomerController來演示CQRS框架的實現(xiàn),以及AccountController和ManageController的用戶登錄、注冊、退出和用戶信息管理。
對于AccountController和ManageController兩個控制器關(guān)聯(lián)著Equinox.Infra.CrossCutting.Identity項目。Identity項目包括了需要用的視圖模型、對系統(tǒng)的授權(quán)、自定義用戶表數(shù)據(jù)、用戶數(shù)據(jù)同步到數(shù)據(jù)庫的遷移版本管理、郵件和SMS。對于授權(quán)方案通過Equinox.Infra.CrossCutting.IoC來注入服務(wù)。如下所示:
// ASP.NET Authorization Policesservices.AddSingleton<IAuthorizationHandler, ClaimsRequirementHandler>();
Equinox.Services.Api項目實現(xiàn)的功能與Web站點差不多,是通過暴露Web API來實現(xiàn)。下面是表現(xiàn)層的二個項目:
五. 應(yīng)用服務(wù)層分析
Equinox.Application應(yīng)用服務(wù)層包括對AutoMapper的配置管理,通過AutoMapper實現(xiàn)視圖模型和領(lǐng)域模型的實體互轉(zhuǎn)。定義ICustomerAppService服務(wù)接口供表現(xiàn)層調(diào)用,由CustomerAppService類來實現(xiàn)該接口。項目包含了Customer需要的視圖模型。還有事件源EventSource。
由CustomerAppService類來實現(xiàn)表現(xiàn)層的查詢、命令、獲取事件源。項目結(jié)構(gòu)如下:
六.領(lǐng)域?qū)覦omain.Core分析
領(lǐng)域?qū)邮琼椖糠謱蛹軜?gòu)中,最重要的一層,也是相對復(fù)雜的一層。該層作者用了二個項目包括:Domain.Core和Domain。Domain.Core項目結(jié)構(gòu)如下所示:
對于Domain.Core項目主要是定義命令和事件的基類。源頭是定義的抽象類Message。對于命令和事件,任何前端都會發(fā)送消息給應(yīng)用程序?qū)? Message消息就是數(shù)據(jù)傳輸對象,通常消息定義為一個Message基類開始,作為數(shù)據(jù)容器。
這里使用MediatR中間件作為命令和事件的實現(xiàn)。MediatR支持兩種消息類型:Request/Response和Notification。先看下Message消息基類定義:
//注入服務(wù)services.AddMediatR(typeof(Startup)); /// <summary>
/// Message消息
/// 放入通用屬性,甚至是普通標(biāo)記,沒有屬性
/// </summary>
public abstract class Message : IRequest<bool>
{
/// <summary>
/// 消息類型:實現(xiàn)Message的命令或事件類型
/// </summary>
public string MessageType { get; protected set; }
/// <summary>
/// 聚合ID
/// </summary>
public Guid AggregateId { get; protected set; }
protected Message()
{
MessageType = GetType().Name;
}
}
消息有二種:命令和事件。兩種消息都包含了數(shù)據(jù)傳輸對象。命令和事件有些微妙差別,命令和事件都是Message派生類。
/// <summary>/// Event 領(lǐng)域消息
/// 事件類是不可變的,它表示已經(jīng)發(fā)生的事情,意味著只有私有set,沒有寫入方法。
/// 事件存放通用屬性,例如事件觸發(fā)時間,觸發(fā)的用戶,數(shù)據(jù)版本號。
/// </summary>
public abstract class Event : Message, INotification
{
public DateTime Timestamp { get; private set; }
protected Event()
{
//事件時間
Timestamp = DateTime.Now;
}
} /// <summary>
/// Command領(lǐng)域命令(增刪改),不返回任何結(jié)果(void),但會改變數(shù)據(jù)對象的狀態(tài)。
/// </summary>
public abstract class Command : Message
{
public DateTime Timestamp { get; private set; }
//DTO綁定驗證,使用Fluent API來實現(xiàn)
public ValidationResult ValidationResult { get; set; }
protected Command()
{
//命令時間
Timestamp = DateTime.Now;
}
//實現(xiàn)Command抽象類的DTO數(shù)據(jù)驗證
public abstract bool IsValid();
}
Domain.Core項目還定義了領(lǐng)域?qū)嶓w和領(lǐng)域值對象的基類實現(xiàn)。例如:在領(lǐng)域?qū)嶓w基類中實現(xiàn)了相等性、運算符重載、重寫HashCode。對于實體和值對象主要區(qū)別是:實體有明確的身份標(biāo)識如主鍵ID,GUID。
public abstract class Entitypublic abstract class ValueObject<T> where T : ValueObject<T>
Domain.Core項目中的Notifications消息文件夾,用來確認(rèn)消息發(fā)送后的處理狀態(tài)。下面是表現(xiàn)層發(fā)送更新命令后,IsValidOperation()確認(rèn)消息處理的狀態(tài)情況。
[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文件夾,用來做命令總線和事件總線的發(fā)送接口,由Equinox.Infra.CrossCutting.Bus項目來實現(xiàn)總線接口的發(fā)送。
七.領(lǐng)域?qū)覦omain分析
下面是Domain項目結(jié)構(gòu)如下:
在上面結(jié)構(gòu)中,Commands和Events文件夾分別用來存儲命令和事件的數(shù)據(jù)傳輸對象,是貧血的DTO類,也可以理解為領(lǐng)域?qū)嶓w。例如Commands文件夾下命令數(shù)據(jù)傳輸對象定義:
/// <summary>/// Customer數(shù)據(jù)轉(zhuǎn)輸對象抽象類,放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注冊命令消息參數(shù)
/// </summary>
public class RegisterNewCustomerCommand : CustomerCommand
{
public RegisterNewCustomerCommand(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
}
/// <summary>
/// 命令信息參數(shù)驗證
/// </summary>
/// <returns></returns>
public override bool IsValid()
{
ValidationResult = new RegisterNewCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}
當(dāng)在應(yīng)用服務(wù)層發(fā)送命令(Bus.SendCommand)后,由領(lǐng)域?qū)拥?span lang="en-us">CommandHandlers文件夾下的類來處理命令,再調(diào)用EF持久層來改變實體狀態(tài)。下面梳理下命令的執(zhí)行流程,由表現(xiàn)層開始一個customer新增如下所示:
當(dāng)在表現(xiàn)層點擊Create后,調(diào)用應(yīng)用服務(wù)層Register方法,觸發(fā)一個新增事件,代碼如下:
/// <summary>/// 新增
/// </summary>
/// <param name="customerViewModel">視圖模型</param>
public void Register(CustomerViewModel customerViewModel)
{
//將視圖模型 映射到 RegisterNewCustomerCommand 新增命令實體
var registerCommand = _mapper.Map<RegisterNewCustomerCommand>(customerViewModel);
Bus.SendCommand(registerCommand);
}
?當(dāng)SendCommand發(fā)送命令后,由領(lǐng)域?qū)覥ustomerCommandHandler類中的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);
}
//將命令消息轉(zhuǎn)成領(lǐng)域?qū)嶓w
var customer = new Customer(Guid.NewGuid(), message.Name, message.Email, message.BirthDate);
//如果注冊用戶郵件已存在,發(fā)起一個事件
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來實現(xiàn)數(shù)據(jù)持久化。事件是過去在系統(tǒng)中發(fā)生的事情。該事件通常是命令的結(jié)果.
_customerRepository.Add(customer);
//新增成功后,使用事件記錄這次命令。
if (Commit())
{
Bus.RaiseEvent(new CustomerRegisteredEvent(customer.Id, customer.Name, customer.Email, customer.BirthDate));
}
return Task.FromResult(true);
}
下面是注冊customer的信息,以及注冊產(chǎn)生的事件數(shù)據(jù),如下所示:
在領(lǐng)域?qū)拥腎nterfaces文件夾中,最重要的包括IRepository<TEntity>接口,是通過Equinox.Infra.Data.Repository來實現(xiàn)接口,來進行數(shù)據(jù)持久化。下面是領(lǐng)域?qū)觽}儲接口:
/// <summary>/// 領(lǐng)域?qū)觽}儲接口,定義了通用的方法
/// </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倉儲接口,在基數(shù)倉儲上擴展
/// </summary>
public interface ICustomerRepository : IRepository<Customer>
{
Customer GetByEmail(string email);
}
? Interfaces文件夾中還定義了IUser和IUnitOfWork接口類,也是需要Equinox.Infra.Data.Repository來實現(xiàn)。
八. 基礎(chǔ)設(shè)施層分析
? Equinox.Infra.Data項目是EF用來持久化命令和事件,以及查詢數(shù)據(jù)的倉儲,結(jié)構(gòu)如下:
其中UoW文件夾下的UnitOfWork類用來實現(xiàn)領(lǐng)域?qū)拥腎UnitOfWork,使用Commit保存數(shù)據(jù)。
public bool Commit(){
return _context.SaveChanges() > 0;
}
Repository文件夾下的類用來實現(xiàn)領(lǐng)域?qū)拥腎Repository接口,使用EF的DbSet來操作EF TEntity對象,再調(diào)用Commit提交到數(shù)據(jù)庫。
public virtual void Add(TEntity obj){
DbSet.Add(obj);
}
Repository文件夾下還包含EventSourcing事件源,存儲到StoredEvent表中。
九.命令總線分析
Equinox.Infra.CrossCutting.Bus項目中使用了中間件MediatR,定義了InMemoryBus類來實現(xiàn)領(lǐng)域?qū)拥腎MediatorHandler命令總線接口發(fā)送,使用SendCommand (T)和RaiseEvent (T)方法發(fā)送命令和事件。
MediatR是用于消息發(fā)送和消息處理的解耦,MediatR是一種進程內(nèi)消息傳遞機制。 支持以同步或異步的形式進行請求/響應(yīng),命令,查詢,通知和事件的消息傳遞,并通過C#泛型支持消息的智能調(diào)度。 其中IRequest和INotification分別對應(yīng)單播和多播消息的抽象。
例如:在領(lǐng)域?qū)又?#xff0c;Message消息實現(xiàn)IRequest,代碼如下:
/// <summary>/// Message消息
/// 放入通用屬性,甚至是普通標(biāo)記,沒有屬性。IRequest<T> - 有返回值
/// </summary>
public abstract class Message : IRequest<bool>
最后Equinox.Infra.CrossCutting.Identity主要做用戶管理,授權(quán),遷移管理。Equinox.Infra.CrossCutting.IoC做整個解決方案下項目需要的服務(wù)注入。
參考文獻:
Introduction-to-CQRS
Microsoft.NET企業(yè)級應(yīng)用架構(gòu)設(shè)計 第二版
原文地址:https://www.cnblogs.com/MrHSR/p/10820545.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的CQRS架构下Equinox开源项目分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用ProGet搭建本地私有NuGet仓库
- 下一篇: 仿B站(一) 目的分析以及创建 WebA