ABP vNext 框架自己就是圍繞着 DDD 理念進行設計的,因此在 DDD 裏面咱們可以見到的實體、倉儲、值對象、領域服務,ABP vNext 框架都爲咱們進行了實現,這些基礎設施都存放在 Volo.Abp.Ddd.Domain 項目當中。html
本篇文章將會側重於理論講解,但也只是一個拋磚引玉的做用,關於 DDD 相關的知識能夠閱讀 Eric Evans 所編寫的 《領域驅動設計:軟件核心複雜性應對之道》。前端
PS:數據庫
該書也是目前我正在閱讀的 DDD 理論書籍,由於基於 DDD 理論,咱們可以精準地劃分微服務的業務邊界,爲後續微服務架構的可擴展性提供堅實的基礎。編程
Volo.Abp.Ddd.Domain 分爲 Volo 和 Microsoft 兩個文件夾,在 Microsoft 文件夾當中主要是針對倉儲和實體進行自動注入。數組
只要用過 EF Core 框架的人,基本都知道什麼是實體。不過不少人就跟我同樣,只是將實體做爲數據庫表在 C# 語言當中的另外一種展示方式,認爲它跟普通的對象沒什麼不同。架構
PS:雖然每一個對象都會有一個內在的 對象引用指針 來做爲惟一標識。併發
在 DDD 的概念當中,經過標識定義的對象被稱爲實體(Entity)。雖然它們的屬性可能由於不一樣的操做而被改變(多種生命週期),但必須保證一種內在的連續性。爲了保證這種內在的連續性,就須要一個有意義而且惟一的屬性。app
標識是否重要則徹底取決於它是否有用,例若有個演唱會訂票程序,你能夠將座位與觀衆都看成一個實體處理。那麼在分配座位時,每一個座位確定都會有一個惟一的座位號(惟一標識),可也能擁有其餘描述屬性(是不是 VIP 座位、價格等...)。框架
那麼座位是否須要惟一標識,是否爲一個實體,就取決於不一樣的入場方式。假如說是一人一票制,而且每張門票上面都有固定的座位號,這個時候座位就是一個實體,由於它須要座位號來區分不一樣的座位。dom
另外一種方式就是入場卷方式,門票上沒有座位號,你想坐哪兒就坐哪兒。這個時候座位號就不須要與門票創建關聯,在這種狀況下座位就不是一個實體,因此不須要惟一標識。
* 上述例子與描述改編自 《領域驅動設計:軟件核心複雜性應對之道》的 ENTITY 一節。
瞭解了 DDD 概念裏面的實體描述以後,咱們就來看一下 ABP vNext 爲咱們準備了怎樣的基礎設施。
首先看 Entities 文件夾下關於實體的基礎定義,在實體的基礎定義類裏面,爲每一個實體定義了惟一標識。而且在某些狀況下,咱們須要確保 ID 在多個計算機系統之間具備惟一性。
尤爲是在多個系統/平臺進行對接的時候,若是每一個系統針對於 「張三」 這個用戶的 ID 不是一致的,都是本身生成 ID ,那麼就須要介入一個新的抽象層進行關係映射。
在 IEntity<TKey>
的默認實現 Entity<TKey>
中,不只提供了標識定義,也重寫了 Equals()
比較方法和 ==
!=
操做符,用於區別不一樣實體。它爲對象統必定義了一個 TKey
屬性,該屬性將會做爲實體的惟一標識字段。
public override bool Equals(object obj) { // 比較的對象爲 NULL 或者對象不是派生自 Entity<T> 都視爲不相等。 if (obj == null || !(obj is Entity<TKey>)) { return false; } // 比較的對象與當前對象屬於同一個引用,視爲相等的。 if (ReferenceEquals(this, obj)) { return true; } // 當前比較主要適用於 EF Core,若是任意對象是使用的默認 Id,即臨時對象,則其默認 ID 都爲負數,視爲不相等。 var other = (Entity<TKey>)obj; if (EntityHelper.HasDefaultId(this) && EntityHelper.HasDefaultId(other)) { return false; } // 主要判斷當前對象與比較對象的類型信息,看他們兩個是否屬於 IS-A 關係,若是不是,則視爲不相等。 var typeOfThis = GetType().GetTypeInfo(); var typeOfOther = other.GetType().GetTypeInfo(); if (!typeOfThis.IsAssignableFrom(typeOfOther) && !typeOfOther.IsAssignableFrom(typeOfThis)) { return false; } // 若是兩個實體他們的租戶 Id 不一樣,也視爲不相等。 if (this is IMultiTenant && other is IMultiTenant && this.As<IMultiTenant>().TenantId != other.As<IMultiTenant>().TenantId) { return false; } // 經過泛型的 Equals 方法進行最後的比較。 return Id.Equals(other.Id); }
實體自己是支持序列化的,因此特別標註了 [Serializable]
特性。
[Serializable] public abstract class Entity<TKey> : Entity, IEntity<TKey> { // ... 其餘代碼。 }
針對於某些實體多是 複合主鍵 的狀況,ABP vNext 則推薦使用 IEntity
和 Entity
進行處理。
/// <summary> /// 定義一個實體,但它的主鍵可能不是 「Id」,也有多是否複合主鍵。 /// 開發人員應該儘量使用 <see cref="IEntity{TKey}"/> 來定義實體,以便更好的與其餘框架/結構進行集成。 /// </summary> public interface IEntity { /// <summary> /// 返回當前實體的標識數組。 /// </summary> object[] GetKeys(); }
在 Entities 文件夾裏面,還有一個 Auditing 文件夾。在這個文件夾裏面定義了不少對象,咱們最爲經常使用的就是 FullAuditiedEntity
對象了。從字面意思來看,它是一個包含了全部審計屬性的實體。
[Serializable] public abstract class FullAuditedEntity<TKey> : AuditedEntity<TKey>, IFullAuditedObject { // 軟刪除標記,爲 true 時說明實體已經被刪除,反之亦然。 public virtual bool IsDeleted { get; set; } // 刪除實體的用戶 Id。 public virtual Guid? DeleterId { get; set; } // 實體被刪除的時間。 public virtual DateTime? DeletionTime { get; set; } }
那麼,什麼是審計屬性呢?在 ABP vNext 內部將如下屬性定義爲審計屬性:建立人、建立時間、修改人、修改時間、刪除人、刪除時間、軟刪除標記。這些屬性不須要開發人員手動去書寫/控制,ABP vNext 框架將會自動跟蹤這些屬性並設置其值。
開發人員除了能夠直接繼承 FullAuditedEntity
之外,也能夠考慮集成其餘的審計實例,例如只包含建立人與建立時間的 CreationAuditedEntity
。若是你以爲你只想要建立人、軟刪除標記、修改時間的話,也能夠直接繼承相應的接口。
public class TestEntity : Entity<int>,IMayHaveCreator,ISoftDelete,IHasModificationTime { /// <summary> /// 建立人的 Id。 /// </summary> public Guid? CreatorId { get; set; } /// <summary> /// 軟刪除標記。 /// </summary> public bool IsDeleted { get; set; } /// <summary> /// 最後的修改時間。 /// </summary> public DateTime? LastModificationTime { get; set; } }
這裏我只重點提一下關於審計實體相關的內容,對於聚合的根對象的審計實體,內容也是類似的,就再也不贅述。
DDD 關於值對象某一個概念來講,每一個值對象都是單一的副本,這個概念你能夠類比 C# 裏面關於值對象和引用對象的區別。
值對象與實體最大的區別就在於,值對象是沒有概念標識的,還有比較重要的一點就是值對象是不可變的,所謂的不可變,就是值對象產生任何變化應該直接替換掉原有副本,而不是在原有副本上進行修改。若是值對象是可變的,那麼它必定不能被共享。值對象能夠引用實體或者其餘的值對象。
這裏仍然以書中的例子進行說明值對象的標識問題,例如 「地址」 這個概念。
若是我在淘寶買了一個鍵盤,個人室友也從淘寶買了同款鍵盤。對於淘寶系統來講,咱們兩個是否處於同一個地址並不重要,因此這裏 「地址」 就是一個值對象。由於系統不須要關心兩個地址的惟一標識是否一致,在業務上來講也沒有這個須要。
另外一個狀況就是家裏停電了,我和個人室友同時在電力服務系統提交了工單。這個時候對於電力系統來講,若是兩個工單的地址是在同一個地方,那麼只須要派一我的去進行維修便可。這種狀況下,地址就是一個實體,由於地址涉及到比較,而比較的依據則是地址的惟一標識。
上述狀況還有的另外一種實現方式,即咱們將住處抽象爲一個實體,電力系統與住處進行關聯。住處裏面包含地址,這個時候地址就是一個值對象。由於這個時候電力系統關心的是住處是否一致,而地址則做爲一個普通的屬性而已。
關於值對象的另外一個用法則更加通俗,例如一個 Person 類,他原來的定義是擁有一個 Id、姓名、街道、社區、城市。那麼咱們能夠將街道、社區、城市抽象到一個值對象 Address 類裏面,每一個值對象內部包含的屬性應該造成一個概念上的總體。
ABP vNext 對於值對象的實現是比較粗糙的,他僅參考 MSDN 定義了一個簡單的 ValueObject
類型,具體的用法開發人員能夠參考 MSDN 實現值對象的細節,下文僅是摘抄部份內容進行簡要描述。
MSDN 也是以地址爲例,他將 Address 定義爲一個值對象,以下代碼。
public class Address : ValueObject { public String Street { get; private set; } public String City { get; private set; } public String State { get; private set; } public String Country { get; private set; } public String ZipCode { get; private set; } private Address() { } public Address(string street, string city, string state, string country, string zipcode) { Street = street; City = city; State = state; Country = country; ZipCode = zipcode; } protected override IEnumerable<object> GetAtomicValues() { // Using a yield return statement to return each element one at a time yield return Street; yield return City; yield return State; yield return Country; yield return ZipCode; } }
不過咱們知道,若是一個值對象須要持久化到數據庫,沒有 Id 標識咋辦?MSDN 上面也說明了在 EF Core 1.1 和 EF Core 2.0 的處理方法,這裏咱們只着重說明 EF Core 2.0 的處理方法。
EF Core 2.0 可使用 owned entity(固有實體類型) 來實現值對象,固有實體的如下特徵能夠幫助咱們實現值對象。
但一個類型無論怎樣都是會擁有它本身的標識的,這裏再也不詳細敘述,更加詳細的能夠參考 MSDN 英文原版說明。(中文版翻譯有問題)
- The identity of the owner
- The navigation property pointing to them
- In the case of collections of owned types, an independent component (not yet supported in EF Core 2.0, coming up on 2.2).
EF Core 不會自動發現固有實體類型,須要顯示聲明,這裏以 MSDN 官方的 eShopOnContainers DEMO 爲例。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration()); modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration()); //...Additional type configurations }
接着咱們來到 OrderEntityTypeConfiguration
類型的 Configure()
方法中。
public void Configure(EntityTypeBuilder<Order> orderConfiguration) { orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA); orderConfiguration.HasKey(o => o.Id); orderConfiguration.Ignore(b => b.DomainEvents); orderConfiguration.Property(o => o.Id) .ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA); // 說明 Address 屬性是 Order 類型的固有實體。 orderConfiguration.OwnsOne(o => o.Address); orderConfiguration.Property<DateTime>("OrderDate").IsRequired(); //...Additional validations, constraints and code... //... }
默認狀況下,EF Core 會將固有實體的數據庫列名,以 <實體的屬性名> _ <固有實體的屬性> 。以上面的 Address
類型字段爲例,將會生成 Address_Street
、 Address_City
這樣的名稱。你也能夠經過流暢接口來重命名這些列,代碼以下:
orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.Street).HasColumnName("ShippingStreet"); orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.City).HasColumnName("ShippingCity");
若是說實體的概念還比較好理解的話,那麼聚合則是在實體之上新的抽象。聚合就是一組相關對象的集合,他會有一個根對象(root),和它的一個邊界(boundary)。對於聚合外部來講,只可以引用它的根對象,而在聚合內部的其餘對象則能夠相互引用。
一個簡單的例子(《領域驅動設計》)來講,汽車是一個具備全局標識的實體,每一輛汽車都擁有本身惟一的標識。在某些時候,咱們可能會須要知道輪胎的磨損狀況與千米數,由於汽車有四個輪胎,因此咱們也須要將輪胎視爲實體,爲其分配惟一本地的標識,這個標識是聚合內惟一的。可是在脫離了汽車這個邊界以後,咱們就不須要關心這些輪胎的標識。
因此在上述例子當中,汽車是一個聚合的根實體,而輪胎處於這個聚合的邊界以內。
那麼一個聚合應該怎樣進行設計呢?這裏我引用湯雪華大神的 《關於領域驅動設計(DDD)中聚合設計的一些思考》 和 《聚合(根)、實體、值對象精煉思考總結》 說明一下聚合根要怎麼設計才合理。
聚合的幾大設計原則:
以上內容咱們仍是以經典的訂單系統來舉例子,說明咱們的實體與聚合應該怎樣進行劃分。咱們有一個訂單系統,其結構以下圖:
其中有一個固定規則,就是採購項(Line Item)的總量不可以超過 PO 總額(approved limit)的限制,這裏的 Part 是具體採購的部件(產品),它擁有一個 price 屬性做爲它的金額。
從上述業務場景咱們就能夠得出如下問題:
場景 1:
當用戶編輯任何一個對象時,鎖定該對象,直到編輯完成提交事務。這樣就會形成 George 編輯訂單 #0001 的採購項 001 時,Amanda 沒法修改該採購項。可是 Amanda 能夠修改其餘的採購項,這樣最後提交的時候就會致使 #0001 訂單破壞了固定規則。
場景 2:
若是鎖定單行對象不行,那麼咱們直接鎖定 PO 對象,而且爲了防止 Part 的價格被修改,Part 對象也須要被鎖定。這樣就會形成太多的數據爭用,如今 3 我的都須要等待。
從上述場景來看,咱們能夠得出如下結論:
有以上結論能夠知道,咱們能夠將 Part 的價格冗餘到採購項,PO 和採購項的建立與刪除是很天然的業務規則,而 Part 的建立與刪除是獨立的,因此將 PO 與採購項能劃爲一個聚合。
Abp vNext 框架也爲咱們提供了聚合的定義與具體實現,即 AggregateRoot
類型。該類型也繼承自 Entity
類型,而且內部提供了一個併發令牌防止併發衝突。
而且在其內部也提供了領域事件的快速增刪方法,其餘的與常規實體基本一致。經過領域事件,咱們能夠完成對事務的拆分。例如上述的例子當中,咱們也能夠爲 Part 增長一個領域事件,當價格被更新時,PO 能夠訂閱這個事件,實現對應的採購項更新。
只是這裏你會奇怪,增長的事件到哪兒去了呢?他們這些事件最終會被添加到 EntityChangeReport
類型的 DomainEvents
集合裏面,而且在實體變動時進行觸發。
關於聚合的 示例,在 ABP vNext 官網已經有十分詳細的描述,這裏我貼上代碼供你們理解如下,官方的例子仍然是以訂單和採購項來講的。
public class Order : AggregateRoot<Guid> { public virtual string ReferenceNo { get; protected set; } public virtual int TotalItemCount { get; protected set; } public virtual DateTime CreationTime { get; protected set; } public virtual List<OrderLine> OrderLines { get; protected set; } protected Order() { } public Order(Guid id, string referenceNo) { Check.NotNull(referenceNo, nameof(referenceNo)); Id = id; ReferenceNo = referenceNo; OrderLines = new List<OrderLine>(); } public void AddProduct(Guid productId, int count) { if (count <= 0) { throw new ArgumentException( "You can not add zero or negative count of products!", nameof(count) ); } var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId); if (existingLine == null) { OrderLines.Add(new OrderLine(this.Id, productId, count)); } else { existingLine.ChangeCount(existingLine.Count + count); } TotalItemCount += count; } } public class OrderLine : Entity { public virtual Guid OrderId { get; protected set; } public virtual Guid ProductId { get; protected set; } public virtual int Count { get; protected set; } protected OrderLine() { } internal OrderLine(Guid orderId, Guid productId, int count) { OrderId = orderId; ProductId = productId; Count = count; } internal void ChangeCount(int newCount) { Count = newCount; } }
根據 DDD 理論來講,每一個實體或者值對象已經具備一些業務方法,爲何還須要服務對象來進行處理呢?
由於在某些狀況下,某些重要的領域動做都不屬於任何實體或者值對象,強行將它概括在某一個對象裏面,那麼就會產生概念上的混淆。
服務都是沒有本身的狀態,它們除了承載領域操做之外沒有其餘任何意義。服務則是做爲一種接口提供操做,一個良好的服務定義擁有一下幾個特徵。
從上述定義來看,咱們的控制器(Controller)就符合這幾個特徵,尤爲是無狀態的定義。那麼咱們哪些操做可以放到服務對象當中呢?根據 DDD 理論來講,只有領域當中某個重要的過程或者轉換操做不是實體或值對象的天然職責的時候,就應該添加一個獨立的服務來承載這些操做。
那麼問題來了,在層級架構來講,領域層的服務對象和應用層的服務對象最難以區分。以書中的例子舉例,當客戶餘額小於某個閾值的時候,就會向客戶發送電子郵件。在這裏,應用服務負責通知的設置,而領域服務則須要肯定客戶是否知足閾值。這裏就涉及到了銀行領域的業務,說白了領域服務是會涉及到具體業務規則的。
下面就是書中關於不一樣分層當中服務對象的劃分:
從上面的描述來看,領域層的應用服務就對應着 ABP vNext 框架當中的應用服務。因此咱們能夠將應用服務做爲 API 接口暴露給前端(表現層),由於應用服務僅僅是起一個協調領域層和基礎設施層的做用。(相似腳本)
上面咱們瞭解了什麼是領域服務,ABP vNext 爲咱們提供了領域服務的基本抽象定義 IDomainService
與 DomainService
。
它們的內部實現比較簡單,只注入了一些經常使用的基礎組件,咱們使用的時候直接繼承 DomainService
類型便可。
public abstract class DomainService : IDomainService { public IServiceProvider ServiceProvider { get; set; } protected readonly object ServiceProviderLock = new object(); protected TService LazyGetRequiredService<TService>(ref TService reference) { // 比較簡單的雙重檢查鎖定模式。 if (reference == null) { lock (ServiceProviderLock) { if (reference == null) { reference = ServiceProvider.GetRequiredService<TService>(); } } } return reference; } public IClock Clock => LazyGetRequiredService(ref _clock); private IClock _clock; // Guid 生成器。 public IGuidGenerator GuidGenerator { get; set; } // 日誌工廠。 public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); private ILoggerFactory _loggerFactory; // 獲取當前租戶。 public ICurrentTenant CurrentTenant => LazyGetRequiredService(ref _currentTenant); private ICurrentTenant _currentTenant; // 日誌組件。 protected ILogger Logger => _lazyLogger.Value; private Lazy<ILogger> _lazyLogger => new Lazy<ILogger>(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); protected DomainService() { GuidGenerator = SimpleGuidGenerator.Instance; } }
應用服務的內容比較複雜繁多,會在下一篇文章《[Abp vNext 源碼分析] - 6. DDD 的應用層支持 (應用服務)》裏面進行詳細描述,這裏就暫不進行說明。
倉儲這個東西你們應該都不會陌生,畢竟倉儲模式這玩意兒玩了這麼久了,我等 Crud 碼農必備利器。那麼這裏的倉儲和 DDD 概念裏面的倉儲有什麼異同呢?
咱們首先要明確 DDD 裏面爲何會引入倉儲這個概念,雖然咱們能夠經過遍歷對象的關聯來獲取相關的對象,但老是要有一個起點。傳統開發人員會構造一個 SQL 查詢,將其傳遞給基礎設施層的某個查詢服務,而後根據獲得的表/行數據重建實體對象,ORM 框架就是這樣誕生的。
經過上述手段,開發人員就會試圖繞開領域模型,轉而直接獲取或者操做它們所須要的數據,這樣就會致使愈來愈多的領域規則被嵌入到查詢代碼當中。更爲嚴重的是,開發人員將會直接查詢數據庫從中提取它們須要的數據,而不是經過聚合的根來獲得這些對象。這樣就會致使領域邏輯(業務規則)進入查詢代碼當中,而咱們的實體和值對象最終只是存放數據的容器而已。最後咱們的領域層只是一個空殼,最後使得模型可有可無。
因此咱們須要一種組件,可以經過根遍歷查找對象,而且禁止其餘方法對聚合內部的任何對象進行訪問。而持久化的值對象能夠經過遍歷某個實體找到,因此值對象是不須要全局搜索的。
而倉儲就可以解決上述問題,倉儲能夠將某種類型的全部對象表示爲一個概念上的集合。開發人員只須要調用倉儲對外提供的簡單接口,就能夠重建實體,而具體的查詢、插入等技術細節徹底被倉儲封裝。這樣開發人員只須要關注領域模型。
倉儲的優勢有如下幾點:
ABP vNext 爲咱們提供了幾種類型的倉儲 IRepository
、IBasicRepository
、IReadOnlyRepository
等,其實從名字就能夠看出來它們具體的職責。首先咱們來看 IReadonly<XXX>
倉儲,很明顯這種類型的倉儲只提供了查詢方法,由於它們是隻讀的。
public interface IReadOnlyBasicRepository<TEntity> : IRepository where TEntity : class, IEntity { // 得到全部實體對象。 List<TEntity> GetList(bool includeDetails = false); // 得到全部實體對象。 Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default); // 得到實體對象的數據量。 long GetCount(); // 得到實體對象的數據量。 Task<long> GetCountAsync(CancellationToken cancellationToken = default); } public interface IReadOnlyBasicRepository<TEntity, TKey> : IReadOnlyBasicRepository<TEntity> where TEntity : class, IEntity<TKey> { // 根據實體的惟一標識重建對象,沒有找到對象時拋出 EntityNotFoundException 異常。 [NotNull] TEntity Get(TKey id, bool includeDetails = true); [NotNull] Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default); // 根據實體的惟一標識重建對象,沒有找到對象時返回 null。 [CanBeNull] TEntity Find(TKey id, bool includeDetails = true); Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default); }
除了只讀倉儲之外, 還擁有支持插入、更新、刪除的倉儲定義,它們都存放在 IBasicRepository
當中。在 Volo.Abp.Ddd.Domain 模塊裏面爲咱們提供了倉儲類型的抽象實現 RepositoryBase
。
這個抽象基類裏面咱們須要注意幾個基礎組件:
BasicRepositoryBase
基類裏面注入的 ICancellationTokenProvider
對象。RepositoryBase
基類注入的 IDataFilter
對象。RepositoryBase
基類注入的 ICurrentTenant
對象。以上三個對象都不是咱們講過的組件,這裏我先大概說一下它們的做用。
CancellationToken
不少人都用過,它的做用是用來取消某個耗時的異步任務。ICancellationTokenProvider
顧名思義就是 CancellationToken
的提供者,那麼誰提供呢?
能夠看到它有兩個定義,一個是從 Http 上下文獲取,一個是默認實現,首先來看通常都很簡單的默認實現。
public class NullCancellationTokenProvider : ICancellationTokenProvider { public static NullCancellationTokenProvider Instance { get; } = new NullCancellationTokenProvider(); public CancellationToken Token { get; } = CancellationToken.None; private NullCancellationTokenProvider() { } }
emmm,確實很簡單,他直接返回的就是 CancellationToken.None
空值。那咱們如今去看一下 Http 上下文的實現吧:
[Dependency(ReplaceServices = true)] public class HttpContextCancellationTokenProvider : ICancellationTokenProvider, ITransientDependency { public CancellationToken Token => _httpContextAccessor.HttpContext?.RequestAborted ?? CancellationToken.None; private readonly IHttpContextAccessor _httpContextAccessor; public HttpContextCancellationTokenProvider(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } }
從上面能夠看到,這個提供者是從 HttpContext
裏面拿的 RequestAborted
,這個屬性是哪兒來的呢?看它的說明是:
Notifies when the connection underlying this request is aborted and thus request operations should be cancelled.
Soga,這個意思就是若是一個 Http 請求被停止的時候,就會觸發的取消標記哦。
那麼它放在倉儲基類裏面幹什麼呢?確定是要取消掉耗時的查詢/持久化異步任務啊,否則一直等麼...
這個接口名字跟以前同樣,很通俗,數據過濾器,用來過濾查詢數據用的。使用過 ABP 框架的同窗確定知道這玩意兒,主要是用來過濾多租戶和軟刪除標記的。
protected virtual TQueryable ApplyDataFilters<TQueryable>(TQueryable query) where TQueryable : IQueryable<TEntity> { // 若是實體實現了軟刪除標記,過濾掉已刪除的數據。 if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { query = (TQueryable)query.WhereIf(DataFilter.IsEnabled<ISoftDelete>(), e => ((ISoftDelete)e).IsDeleted == false); } // 若是實體實現了多租戶標記,根據租戶 Id 過濾數據。 if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { var tenantId = CurrentTenant.Id; query = (TQueryable)query.WhereIf(DataFilter.IsEnabled<IMultiTenant>(), e => ((IMultiTenant)e).TenantId == tenantId); } return query; }
更加詳細的咱們放在後面說明...這裏你只須要知道它是用來過濾數據的就好了。
英語在學習編程的時候仍是很重要的,這個接口的意思是當前租戶,確定這玩意兒就是提供當前登陸用戶的租戶 Id 咯,在上面的例子裏面有使用到。
不管是 ABP vNext 提供的默認倉儲也好,仍是說咱們本身定義的倉儲也好,都須要注入到 IoC 容器當中。ABP vNext 爲咱們提供了一個倉儲註冊基類 RepositoryRegisterarBase<TOptions>
,查看這個基類的實現就會發現倉儲的具體實現模塊都實現了這個基類。
這是由於倉儲確定會有多種實現的,例如 EF Core 的倉儲實現確定有本身的一套註冊機制,因此這裏僅提供了一個抽象基類給開發人員。
在基類裏面,ABP vNext 首先會註冊自定義的倉儲類型,由於從倉儲的 DDD 定義來看,咱們有些業務可能會須要一些特殊的倉儲接口,這個時候就須要自定義倉儲了。
public virtual void AddRepositories() { // 遍歷自定義倉儲。 foreach (var customRepository in Options.CustomRepositories) { // 調用註冊方法,註冊這些倉儲。 Options.Services.AddDefaultRepository(customRepository.Key, customRepository.Value); } // 是否註冊 ABP vNext 生成的默認倉儲。 if (Options.RegisterDefaultRepositories) { RegisterDefaultRepositories(); } }
CustomRepositories 裏面的倉儲是經過基類 CommonDbContextRegistrationOptions
所定義的 AddRepository()
方法進行添加的。例如單元測試裏面就有使用範例:
public override void ConfigureServices(ServiceConfigurationContext context) { var connStr = Guid.NewGuid().ToString(); Configure<DbConnectionOptions>(options => { options.ConnectionStrings.Default = connStr; }); // 添加自定義倉儲。 context.Services.AddMemoryDbContext<TestAppMemoryDbContext>(options => { options.AddDefaultRepositories(); options.AddRepository<City, CityRepository>(); }); }
接着咱們看自定義倉儲是如何註冊到 IoC 容器裏面的呢?這裏調用的 AddDefaultRepository()
方法就是在 Microsoft 文件夾裏面定義的註冊擴展方法。
public static IServiceCollection AddDefaultRepository(this IServiceCollection services, Type entityType, Type repositoryImplementationType) { // 註冊複合主鍵實體所對應的倉儲。 //IReadOnlyBasicRepository<TEntity> var readOnlyBasicRepositoryInterface = typeof(IReadOnlyBasicRepository<>).MakeGenericType(entityType); if (readOnlyBasicRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(readOnlyBasicRepositoryInterface, repositoryImplementationType); //IReadOnlyRepository<TEntity> var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType); if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(readOnlyRepositoryInterface, repositoryImplementationType); } //IBasicRepository<TEntity> var basicRepositoryInterface = typeof(IBasicRepository<>).MakeGenericType(entityType); if (basicRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(basicRepositoryInterface, repositoryImplementationType); //IRepository<TEntity> var repositoryInterface = typeof(IRepository<>).MakeGenericType(entityType); if (repositoryInterface.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(repositoryInterface, repositoryImplementationType); } } } // 首先得到實體的主鍵類型,再進行註冊。 var primaryKeyType = EntityHelper.FindPrimaryKeyType(entityType); if (primaryKeyType != null) { //IReadOnlyBasicRepository<TEntity, TKey> var readOnlyBasicRepositoryInterfaceWithPk = typeof(IReadOnlyBasicRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyBasicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType); //IReadOnlyRepository<TEntity, TKey> var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(readOnlyRepositoryInterfaceWithPk, repositoryImplementationType); } //IBasicRepository<TEntity, TKey> var basicRepositoryInterfaceWithPk = typeof(IBasicRepository<,>).MakeGenericType(entityType, primaryKeyType); if (basicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(basicRepositoryInterfaceWithPk, repositoryImplementationType); //IRepository<TEntity, TKey> var repositoryInterfaceWithPk = typeof(IRepository<,>).MakeGenericType(entityType, primaryKeyType); if (repositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { services.TryAddTransient(repositoryInterfaceWithPk, repositoryImplementationType); } } } } return services; }
上面代碼沒什麼好說的,只是根據不一樣的類型來進行不一樣的註冊而已。
以上是註冊咱們自定義的倉儲類型,只要開發人員調用過 AddDefaultRepositories()
方法,那麼 ABP vNext 會爲每一個不一樣的實體註冊響應的默認倉庫。
public ICommonDbContextRegistrationOptionsBuilder AddDefaultRepositories(bool includeAllEntities = false) { // 能夠看到將參數設置爲 true 了。 RegisterDefaultRepositories = true; IncludeAllEntitiesForDefaultRepositories = includeAllEntities; return this; }
默認倉庫僅包含基礎倉儲所定義的增刪改查等方法,開發人員只須要注入相應的接口就可以直接使用。既然要爲每一個實體類型注入對應的默認倉儲,確定就須要知道當前項目有多少個實體,並得到它們的類型定義。
這裏咱們基類僅僅是調用抽象方法 GetEntityTypes()
,而後根據具體實現返回的類型定義來註冊默認倉儲。
protected virtual void RegisterDefaultRepositories() { foreach (var entityType in GetEntityTypes(Options.OriginalDbContextType)) { // 判斷該實體類型是否須要註冊默認倉儲。 if (!ShouldRegisterDefaultRepositoryFor(entityType)) { continue; } // 爲實體對象註冊相應的默認倉儲,這裏仍然調用以前的擴展方法進行註冊。 RegisterDefaultRepository(entityType); } }
找到 EF Core 定義的倉儲註冊器,就可以看到他是經過遍歷 DbContext 裏面的屬性來獲取全部實體類型定義的。
public static IEnumerable<Type> GetEntityTypes(Type dbContextType) { return from property in dbContextType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance) where ReflectionHelper.IsAssignableToGenericType(property.PropertyType, typeof(DbSet<>)) && typeof(IEntity).IsAssignableFrom(property.PropertyType.GenericTypeArguments[0]) select property.PropertyType.GenericTypeArguments[0]; }
最後的最後,這個註冊器在何時被調用的呢?註冊器通常是在項目的基礎設施模塊當中進行調用,這裏以單元測試的代碼爲例,它是使用的 EF Core 做爲持久層的基礎設施。
[DependsOn(typeof(AbpEntityFrameworkCoreModule))] public class AbpEfCoreTestSecondContextModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { // 注意這裏。 context.Services.AddAbpDbContext<SecondDbContext>(options => { options.AddDefaultRepositories(); }); // 注意這裏。 context.Services.AddAbpDbContext<ThirdDbContext.ThirdDbContext>(options => { options.AddDefaultRepositories<IThirdDbContext>(); }); } }
跳轉到 ABP vNext 提供的 EF Core模塊,找到 AddAbpDbContext()
方法當中,發現了倉儲註冊器。
public static class AbpEfCoreServiceCollectionExtensions { public static IServiceCollection AddAbpDbContext<TDbContext>( this IServiceCollection services, Action<IAbpDbContextRegistrationOptionsBuilder> optionsBuilder = null) where TDbContext : AbpDbContext<TDbContext> { services.AddMemoryCache(); var options = new AbpDbContextRegistrationOptions(typeof(TDbContext), services); optionsBuilder?.Invoke(options); services.TryAddTransient(DbContextOptionsFactory.Create<TDbContext>); foreach (var dbContextType in options.ReplacedDbContextTypes) { services.Replace(ServiceDescriptor.Transient(dbContextType, typeof(TDbContext))); } // 在這裏。 new EfCoreRepositoryRegistrar(options).AddRepositories(); return services; } }
在 ABP vNext 中,除了本地事件總線之外,還爲咱們提供了基於 Rabbit MQ 的分佈式事件總線。關於事件總線的內容,這裏就再也不詳細贅述,後面會有專門的文章講解事件總線的相關知識。
在這裏,主要提一下什麼是領域事件。其實領域事件與普通的事件並沒什麼本質上的不一樣,只是它們觸發的地方和攜帶的參數有點特殊罷了。而且按照聚合的特性來講,其實聚合與聚合之間的通信,主要是經過領域事件來實現的。
這裏的領域事件都是針對於實體產生變動時須要被觸發的事件,例如咱們有一個學生實體,在它被修改以後,ABP vNext 框架就會觸發一個實體更新事件。
觸發領域事件這些動做都被封裝在 EntityChangeEventHelper
裏面,以剛纔的例子來講,咱們能夠看到它會觸發如下代碼:
public virtual async Task TriggerEntityUpdatedEventOnUowCompletedAsync(object entity) { // 觸發本地事件總線。 await TriggerEventWithEntity( LocalEventBus, typeof(EntityUpdatedEventData<>), entity, false ); var eto = EntityToEtoMapper.Map(entity); if (eto != null) { // 觸發分佈式事件總線。 await TriggerEventWithEntity( DistributedEventBus, typeof(EntityUpdatedEto<>), eto, false ); } }
關於領域事件其餘的細節就再也不描述,若是你們想要更加全面的瞭解,請直接閱讀 ABP vNext 的相關源碼。
本篇文章更多的注重 DDD 理論,關於 ABP vNext 的技術實現細節並未體如今當前模塊,後續我會在其餘章節注重描述關於上述 DDD 概念的技術實現。