asp.net core系列 62 CQRS架構下Equinox開源項目分析

来源:https://www.cnblogs.com/MrHSR/archive/2019/05/07/10820545.html
-Advertisement-
Play Games

一.DDD分層架構介紹 本篇分析CQRS架構下的Equinox開源項目。該項目在github上star占有2.4k。便決定分析Equinox項目來學習下CQRS架構。再講CQRS架構時,先簡述下DDD風格,在DDD分層架構中,一般包含表現層、應用程式層(應用服務層)、領域層(領域服務層)、基礎設施層 ...


一.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<IAuthorizationHandler, ClaimsRequirementHandler>();

  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));
    /// <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企業級應用架構設計 第二版

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 直接切入主題 有時候同一個項目下我們可能會使用多個窗體,窗體間方法互相調用也不可避免,好了,使用無參無返回值的方法,開始上圖 1、新建一個winform項目Form1,並再添加一個窗體Form2;拖入button和textbox,如下 2、先編輯Form2,定義屬性存放無參無返回值的方法,重載For ...
  • 今天在使用 C# 操作 Excel 時,一直在報錯誤: 檢索 COM 類工廠中 CLSID 為 {00024500-0000-0000-C000-000000000046} 的組件失敗,原因是出現以下錯誤: 8007065e 這個類型的數據不受支持。 (異常來自 HRESULT:0x8007065E ...
  • 一個字元串就是一個string類型數據,此類型變數我們可以把它看作一個只讀數組,其元素是char變數,在這裡我們來說下string類型的常用命令。 1、TocharArray():將此實例中的字元複製到 Unicode 字元數組。其示例是“char[] ch = <string類型變數>.ToCha ...
  • 因項目原因,需要使用SQLite的全文索引,用到了最新的fts5模塊 但在咱們.net framwork中卻會提示“SQL logic error no such module: fts5”:找不到fts5模塊…… 百度了很久都沒有找到項目解決方案,可能是C#用SQLite比較少的原因,更何況用的更 ...
  • https://3gstudent.github.io/3gstudent.github.io/Exchange-Web-Service(EWS)%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ EWS是郵箱的一個開放的介面服務,可以取到郵箱的各種信息,郵件收發、會議、日期 ...
  • 在之前的文章中我們是使用Rester來測試我們的WebAPI的。接下來,我們來創建一個實際的頁面來測試之前我們寫的WebAPI。我們創建一個HTML頁面,併在頁面使用 jQuery 來調用 Web API 。通過jQuery來調用增刪除改查WebAPI介面,並用 API 介面返回的響應中的詳細信息更... ...
  • 概述 在嵌入式開發的過程中,由於經常需要下位機與上位機通信,通信之間就需要協議,有協議就需要進行解碼,而產品開發得過程中,協議可能不斷更新,協議更新就需要解碼軟體更新,不斷更新解碼軟體就很麻煩,如果所有人都願意麻煩,那麼我不願意。在這裡就產生了一個通用的解碼類庫,使用者就可以簡單的改一下協議文件,通 ...
  • Modbus已經成為工業領域通信協議的業界標準(De facto),並且現在是工業電子設備之間常用的連接方式。 所以這也是我們工控領域軟體開發的所必懂的通訊協議,我也是初次學習,先貼上我的學習筆記 一 .協議概述 (1)Modbus協議是應用於控制器上的一種通用語言,實現控制器之間,控制器通過網路和 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...