一.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.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();
Equinox.Services.Api專案實現的功能與Web站點差不多,是透過暴露Web API來實現。下麵是表現層的二個專案:
五. 應用服務層分析
Equinox.Application應用服務層包括對AutoMapper的配置管理,透過AutoMapper實現檢視模型和領域模型的物體互轉。定義ICustomerAppService服務介面供表現層呼叫,由CustomerAppService類來實現該介面。專案包含了Customer需要的檢視模型。還有事件源EventSource。
由CustomerAppService類來實現表現層的查詢、命令、獲取事件源。專案結構如下:
六.領域層Domain.Core分析
領域層是專案分層架構中,最重要的一層,也是相對複雜的一層。該層作者用了二個專案包括:Domain.Core和Domain。Domain.Core專案結構如下所示:
對於Domain.Core專案主要是定義命令和事件的基類。源頭是定義的抽象類Message。對於命令和事件,任何前端都會傳送訊息給應用程式層, Message訊息就是資料傳輸物件,通常訊息定義為一個Message基類開始,作為資料容器。
這裡使用MediatR中介軟體作為命令和事件的實現。MediatR支援兩種訊息型別:Request/Response和Notification。先看下Message訊息基類定義:
//註入服務 services.AddMediatR(typeof(Startup));
/// /// Message訊息 /// 放入通用屬性,甚至是普通標記,沒有屬性 ///
public abstract class Message : IRequest<bool>
{
///
/// 訊息型別:實現Message的命令或事件型別
///
public string MessageType { get; protected set; }
///
/// 聚合ID
///
public Guid AggregateId { get; protected set; }
protected Message()
{
MessageType = GetType().Name;
}
}
訊息有二種:命令和事件。兩種訊息都包含了資料傳輸物件。命令和事件有些微妙差別,命令和事件都是Message派生類。
/// /// Event 領域訊息 /// 事件類是不可變的,它表示已經發生的事情,意味著只有私有set,沒有寫入方法。 /// 事件存放通用屬性,例如事件觸發時間,觸發的使用者,資料版本號。 ///
public abstract class Event : Message, INotification
{
public DateTime Timestamp { get; private set; }
protected Event()
{
//事件時間
Timestamp = DateTime.Now;
}
}
/// /// Command領域命令(增刪改),不傳回任何結果(void),但會改變資料物件的狀態。 ///
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 where T : ValueObject
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檔案夾下命令資料傳輸物件定義:
/// /// Customer資料轉輸物件抽象類,放Customer透過屬性 ///
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; }
}
/// /// Customer註冊命令訊息引數 ///
public class RegisterNewCustomerCommand : CustomerCommand
{
public RegisterNewCustomerCommand(string name, string email, DateTime birthDate)
{
Name = name;
Email = email;
BirthDate = birthDate;
}
///
/// 命令資訊引數驗證
///
///
public override bool IsValid()
{
ValidationResult = new RegisterNewCustomerCommandValidation().Validate(this);
return ValidationResult.IsValid;
}
}
當在應用服務層傳送命令(Bus.SendCommand)後,由領域層的CommandHandlers檔案夾下的類來處理命令,再呼叫EF持久層來改變物體狀態。下麵梳理下命令的執行流程,由表現層開始一個customer新增如下所示:
當在表現層點選Create後,呼叫應用服務層Register方法,觸發一個新增事件,程式碼如下:
/// /// 新增 ///
/// 檢視模型
public void Register(CustomerViewModel customerViewModel)
{
//將檢視模型 對映到 RegisterNewCustomerCommand 新增命令物體
var registerCommand = _mapper.Map(customerViewModel);
Bus.SendCommand(registerCommand);
}
當SendCommand傳送命令後,由領域層CustomerCommandHandler類中的Handle來處理該命令,如下所示:
/// /// Customer註冊命令處理 ///
///
///
///
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介面,是透過Equinox.Infra.Data.Repository來實現介面,來進行資料持久化。下麵是領域層倉儲介面:
/// /// 領域層倉儲介面,定義了通用的方法 ///
///
public interface IRepository : IDisposable where TEntity : class
{
void Add(TEntity obj);
TEntity GetById(Guid id);
IQueryable GetAll();
void Update(TEntity obj);
void Remove(Guid id);
int SaveChanges();
}
/// /// Customer倉儲介面,在基數倉儲上擴充套件 ///
public interface ICustomerRepository : IRepository
{
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,程式碼如下:
/// /// Message訊息 /// 放入通用屬性,甚至是普通標記,沒有屬性。IRequest - 有傳回值 ///
public abstract class Message : IRequest<bool>
最後Equinox.Infra.CrossCutting.Identity主要做使用者管理,授權,遷移管理。Equinox.Infra.CrossCutting.IoC做整個解決方案下專案需要的服務註入。
參考文獻:
Introduction-to-CQRS
Microsoft.NET企業級應用架構設計 第二版
朋友會在“發現-看一看”看到你“在看”的內容