1. 程式人生 >實用技巧 >其它 >ABP 框架實戰系列(三)-領域層深入篇

ABP 框架實戰系列(三)-領域層深入篇


技術標籤:流行技術架構設計思想資料庫c#mysql.net

ABP 領域層-實體(Entities)

實體是DDD(領域驅動設計)的核心概念之一。Eric Evans是這樣描述的“很多物件不是通過它們的屬性定義的,而是通過一連串的連續性事件和標識定義的”。

在框架實戰系列(二)-領域層介紹篇中有做程式碼簡單介紹(推薦關注該公眾號,乾貨滿滿)

在此我們深入理解實體概念

在ABP 中,實體類繼承Entity類,如下所示,Product為一個實體

    public class Product : Entity
    {
        public virtual string Name { get; set; }
        public virtual string Code { get; set; }
        public virtual DateTime CreationTime { get; set; }

        public Product()
        {
            CreationTime=DateTime.Now;
        }

    }

在ABP 框架中Entity 實體類部分內容如下所示,它有一個主鍵ID,所有繼承自Entity的類,都使用該主鍵,你可以設定主鍵的值為任何格式,如下程式碼所示,它為泛型,可以為string,Guid或者其它資料型別。同時實體類重寫了 equality (==) 操作符用來判斷兩個實體物件是否相等(兩個實體的Id是否相等)。還定義了一個IsTransient()方法來檢測實體是否有Id屬性。

public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
  {
    /// <summary>Unique identifier for this entity.</summary>
    public virtual TPrimaryKey Id { get; set; }

    /// <summary>
    /// Checks if this entity is transient (it has not an Id).
    /// </summary>
    /// <returns>True, if this entity is transient</returns>
    public virtual bool IsTransient()
    {
      if (EqualityComparer<TPrimaryKey>.Default.Equals(this.Id, default (TPrimaryKey)))
        return true;
      if (typeof (TPrimaryKey) == typeof (int))
        return Convert.ToInt32((object) this.Id) <= 0;
      return typeof (TPrimaryKey) == typeof (long) && Convert.ToInt64((object) this.Id) <= 0L;
    }

    public virtual bool EntityEquals(object obj)
    {
      if (obj == null || !(obj is Entity<TPrimaryKey>))
        return false;
      if (this == obj)
        return true;
      Entity<TPrimaryKey> entity = (Entity<TPrimaryKey>) obj;
      if (this.IsTransient() && entity.IsTransient())
        return false;
      Type type1 = this.GetType();
      Type type2 = entity.GetType();
      if (!type1.GetTypeInfo().IsAssignableFrom(type2) && !type2.GetTypeInfo().IsAssignableFrom(type1))
        return false;
      if (this is IMayHaveTenant && entity is IMayHaveTenant)
      {
        int? tenantId1 = this.As<IMayHaveTenant>().TenantId;
        int? tenantId2 = entity.As<IMayHaveTenant>().TenantId;
        if (!(tenantId1.GetValueOrDefault() == tenantId2.GetValueOrDefault() & tenantId1.HasValue == tenantId2.HasValue))
          return false;
      }
      return (!(this is IMustHaveTenant) || !(entity is IMustHaveTenant) || this.As<IMustHaveTenant>().TenantId == entity.As<IMustHaveTenant>().TenantId) && this.Id.Equals((object) entity.Id);
    }

    public override string ToString() => string.Format("[{0} {1}]", (object) this.GetType().Name, (object) this.Id);
 }

介面約定

在很多應用程式中,很多實體具有像CreationTime的屬性(資料庫表也有該欄位)用來指示該實體是什麼時候被建立的。APB提供了一些有用的介面來實現這些類似的功能。也就是說,為這些實現了這些介面的實體,提供了一個通用的編碼方式(通俗的說只要實現指定的介面就能實現指定的功能),以ABPUser 為例,如下所示AbpUser類,繼承了很多預設實現介面。

  • IAudited 審計
  • ISoftDelete 軟刪除
  • IPassivable 啟用/閒置狀態

public abstract class AbpUser<TUser> : AbpUserBase, IFullAudited<TUser>, 
      IAudited<TUser>, IAudited, ICreationAudited, IHasCreationTime, 
      IModificationAudited, IHasModificationTime, ICreationAudited<TUser>, 
      IModificationAudited<TUser>, IFullAudited, IDeletionAudited, 
      IHasDeletionTime, ISoftDelete, IDeletionAudited<TUser>
    where TUser : AbpUser<TUser>

ABP領域層——倉儲(Repositories)

ABP框架實戰系列(一)-持久層介紹篇 中有涉及倉儲概念的講解,(推薦關注該公眾號,乾貨滿滿)

在這裡,我們深入對倉儲部分加以討論,在ABP中,倉儲類要實現IRepository介面.最好的方式是針對不同倉儲物件定義各自不同的介面。 對於倉儲類IRepository定義了許多泛型的方法。比如:Select、Insert、Update、Delete方法(CRUD操作)。在大多數的時候,,這些方法已足已應付一般實體的需要。如果這些方對於實體來說已足夠,我們便不需要再去建立這個實體所需的倉儲介面/類。即為自定義倉儲 。

IRepository定義了從資料庫中檢索實體的常用方法。在ABP框架中AbpRepositoryBase派生了IRepository介面

  public abstract class AbpRepositoryBase<TEntity, TPrimaryKey> : IRepository<TEntity, TPrimaryKey>, IRepository, ITransientDependency, IUnitOfWorkManagerAccessor
    where TEntity : class, IEntity<TPrimaryKey>
 

介面方法主要包括:Query、GetAll、Single、Insert、Update、Delete、Count等幾大方法的過載。

  GetAll():IQueryable<TEntity>
  GetAllIncluding(params Expression<Func<TEntity,object>>[] propertySelectors):IQueryable<TEntity>
  GetAllList():List<TEntity>
  GetAllListAsync():Task<List<TEntity>>
  GetAllList(Expression<Func<TEntity,bool>> predicate):List<TEntity>
  GetAllListAsync(Expression<Func<TEntity,bool>> predicate):Task<List<TEntity>>
  Query<T>(Func<IQueryable<TEntity>,T> queryMethod):T
  Get(TPrimaryKey id):TEntity
  GetAsync(TPrimaryKey id):Task<TEntity>
  Single(Expression<Func<TEntity,bool>> predicate):TEntity
  SingleAsync(Expression<Func<TEntity,bool>> predicate):Task<TEntity>
  FirstOrDefault(TPrimaryKey id):TEntity
  FirstOrDefaultAsync(TPrimaryKey id):Task<TEntity>
  FirstOrDefault(Expression<Func<TEntity,bool>> predicate):TEntity
  FirstOrDefaultAsync(Expression<Func<TEntity,bool>> predicate):Task<TEntity>
  Load(TPrimaryKey id):TEntity
  Insert(TEntity entity):TEntity
  InsertAsync(TEntity entity):Task<TEntity>
  InsertAndGetId(TEntity entity):TPrimaryKey
  InsertAndGetIdAsync(TEntity entity):Task<TPrimaryKey>
  InsertOrUpdate(TEntity entity):TEntity
  InsertOrUpdateAsync(TEntity entity):Task<TEntity>
  InsertOrUpdateAndGetId(TEntity entity):TPrimaryKey
  InsertOrUpdateAndGetIdAsync(TEntity entity):Task<TPrimaryKey>
  Update(TEntity entity):TEntity
  UpdateAsync(TEntity entity):Task<TEntity>
  Update(TPrimaryKey id, Action<TEntity> updateAction):TEntity
  UpdateAsync(TPrimaryKey id, Func<TEntity,Task> updateAction):Task<TEntity>
  Delete(TEntity entity):void
  DeleteAsync(TEntity entity):Task
  Delete(TPrimaryKey id):void
  DeleteAsync(TPrimaryKey id):Task
  Delete(Expression<Func<TEntity,bool>> predicate):void
  DeleteAsync(Expression<Func<TEntity,bool>> predicate):Task
  Count():int
  CountAsync():Task<int>
  Count(Expression<Func<TEntity,bool>> predicate):int
  CountAsync(Expression<Func<TEntity,bool>> predicate):Task<int>
  LongCount():long
  LongCountAsync():Task<long>
  LongCount(Expression<Func<TEntity,bool>> predicate):long
  LongCountAsync(Expression<Func<TEntity,bool>> predicate):Task<long>

在ABP 框架中,若直接在NuGet中安裝的ABP 或者在官網下載的ABP 模板,很少會看到IRepository相關程式碼,看到應用的,基本都在包括在 EntityFrameworkCore類庫中,系統預設給出自定義倉庫的基本格式。

 public abstract class TestProjectRepositoryBase<TEntity, TPrimaryKey> : EfCoreRepositoryBase<TestProjectDbContext, TEntity, TPrimaryKey>
        where TEntity : class, IEntity<TPrimaryKey>
    {
        protected TestProjectRepositoryBase(IDbContextProvider<TestProjectDbContext> dbContextProvider)
            : base(dbContextProvider)
        {


            
        }

        // Add your common methods for all repositories
    }

    /// <summary>
    /// Base class for custom repositories of the application.
    /// This is a shortcut of <see cref="TestProjectRepositoryBase{TEntity,TPrimaryKey}"/> for <see cref="int"/> primary key.
    /// </summary>
    /// <typeparam name="TEntity">Entity type</typeparam>
    public abstract class TestProjectRepositoryBase<TEntity> : TestProjectRepositoryBase<TEntity, int>, IRepository<TEntity>
        where TEntity : class, IEntity<int>
    {
        protected TestProjectRepositoryBase(IDbContextProvider<TestProjectDbContext> dbContextProvider)
            : base(dbContextProvider)
        {

         
        }

        // Do not add any method here, add to the class above (since this inherits it)!!!
    }


倉儲的實現
ABP在設計上是採取不指定特定ORM框架或其它存取資料庫技術的方式。只要實現IRepository介面,任何框架都可以使用。

倉儲要使用NHibernate或EF來實現都很簡單,當你使用NHibernate或EntityFramework,如果提供的方法已足夠使用,你就不需要為你的實體建立倉儲物件了。我們可以直接注入IRepository(或IRepository<TEntity, TPrimaryKey>)。

管理資料庫連線
資料庫連線的開啟和關閉,在倉儲方法中,ABP會自動化的進行連線管理。

當倉儲方法被呼叫後,資料庫連線會自動開啟且啟動事務。當倉儲方法執行結束並且返回以後,所有的實體變化都會被儲存, 事務被提交併且資料庫連線被關閉,一切都由ABP自動化的控制。如果倉儲方法丟擲任何型別的異常,事務會自動地回滾並且資料連線會被關閉。上述所有操作在實現了IRepository介面的倉儲類所有公開的方法中都可以被呼叫。

如果倉儲方法呼叫其它倉儲方法(即便是不同倉儲的方法),它們共享同一個連線和事務。連線會由倉儲方法呼叫鏈最上層的那個倉儲方法所管理。更多關於資料庫管理,詳見UnitOfWork檔案。

倉儲的生命週期
所有的倉儲物件都是暫時性的。這就是說,它們是在有需要的時候才會被建立。ABP大量的使用依賴注入,當倉儲類需要被注入的時候,新的類實體會由注入容器會自動地建立。

倉儲的最佳實踐
對於一個T型別的實體,是可以使用IRepository。但別任何情況下都建立定製化的倉儲,除非我們真的很需要。預定義倉儲方法已經足夠應付各種案例。
假如你正建立定製的倉儲(可以實現IRepository)
倉儲類應該是無狀態的。這意味著, 你不該定義倉儲等級的狀態物件並且倉儲方法的呼叫也不應該影響到其它呼叫。    
當倉儲可以使用相根據注入,儘可較少或是不相根據於其它服務。

ABP領域層——工作單元(Unit Of work)

  • 通用連線和事務管理方法連線和事務管理是使用資料庫的應用程式最重要的概念之一,應用程式中,有兩個通用的方來建立/釋放一個數據庫連線:
    • 長連線
    • 短連線
  • ABP的連線和事務管理
    • ABP提供長連線、短連線模型,Repository是資料庫操作的類,在執行Repository庫中的方法的時候,ABP開啟了一個數據庫連線,並且在進入倉庫方法的時候,會啟用一個事務,因此,你可以安全的使用連線於倉儲中的方法,若方法執行過程中產生異常,事務會被回滾並且釋放掉連線。在這個模式中,倉儲方法是單元性的(Unit of work)。 檢視原始碼部分:
  public class EfCoreRepositoryBase<TDbContext, TEntity> :
      EfCoreRepositoryBase<TDbContext, TEntity, int>, IRepository<TEntity>, 
      IRepository<TEntity, int>, IRepository, ITransientDependency
    where TDbContext : DbContext
    where TEntity : class, IEntity<int>
  {
    public EfCoreRepositoryBase(IDbContextProvider<TDbContext> dbContextProvider)
      : base(dbContextProvider)
    {
    }

  • 工作單元

    工作單元的關鍵字為(Unit of work),為倉儲提供事務服務,一共可以以兩種方式使用

    • 在方法上新增[UnitOfWorkAttribute]確保事務一致性

    • 在方法體重使用IUnitOfWorkManager.Begin(…)、IUnitOfWorkManager.Begin(…)、xxunitOfWork.Complete()確保事務一致性。

      ABP 框架推薦使用UnitOfWorkattribute的在方法體上加屬性的方式。

    • 禁用工作單元(Disabling unit of work)

      方法體上設定如為[UnitOfWork(IsDisabled = true)]

    • 無事務的工作單元(Non-transactional unit of work):[UnitOfWork(false)]

    • 工作單元呼叫其它工作單元(A unit of work method calls another)

      • 工作單元方法(一個貼上UnitOfWork屬性標籤的方法)呼叫另一個工作單元方法,他們共享同一個連線和事務
      • 如果建立了一個不同的執行緒/任務,它使用自己所屬的工作單元
    • 自動化的saving changes (Automatically saving changes)

      當我們使用工作單元到方法上,ABP自動的儲存所有變化於方法的末端。

  • 選項:IsolationLevel、Timeout等基本屬性,可以通過配置或者初始化賦值修改

  • 方法:工作單元系統運作是無縫且不可視的。但是,在有些特例下,你需要呼叫它的方法。ABP儲存所有的變化於工作單元的尾端,你不需要做任何事情。但是,有些時候,你或許會想要在工作單元的過程中就儲存所有變化。 你可以注入IUnitOfWorkManager並且呼叫IUnitOfWorkManager.Current.SaveChanges()方法,即可完成儲存工作。

  • 事件:工作單元具有Completed/Failed/Disposed事件

ABP領域層——資料過濾器(Data filters)

在資料庫開發中,我們一般會運用軟刪除(soft-delete)模式,即不直接從資料庫刪除資料,而是標記這筆資料為已刪除。因此,如果實體被軟刪除了,那麼它就應該不會在應用程式中被檢索到。要達到這種效果,我們需要在每次檢索實體的查詢語句上新增SQL的Where條件IsDeleted = false。這是個乏味的工作,但它是個容易被忘掉的事情。因此,我們應該要有個自動的機制來處理這些問題。

ABP提供資料過濾器(Data filters),它使用自動化的,基於規則的過濾查詢。ABP已經有一些預定義的過濾器,當然也可以自行建立你專屬的過濾器。

  • 過濾
  • 預定義過濾器
    • 軟刪除介面(ISoftDelete)
    • 多租介面(IMustHaveTenant)
    • 多租介面(IMayHaveTenant)
  • 禁用過濾器

可以在工作單元(unit of work)中呼叫DisableFilter方法來禁用某個過濾器

var people1 = _personRepository.GetAllList();
using(_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete)) {
      var people2 = _personRepository.GetAllList();
   }

var people3 = _personRepository.GetAllList();
  • 啟用過濾器

    在工作單元(unit of work)中使用EnableFilter方法啟用過濾器,如同DisableFilter方法一般(兩者互為正反兩面)

  • 設定過濾器引數

過濾器可以被引數化(parametric)。IMustHaveTenant過濾器是這類過濾器的一個範本,因為當前租戶(tenant)的Id是在執行時期決定的。對於這些過濾器,如果真有需要,我們可以改變過濾器的值

CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42);
  • 自定義過濾器

  • 其它物件關係對映工具

    ABP資料過濾器僅實現在Entity Framework上。對於其它ORM工具則尚不可用。

ABP領域層——領域事件(Domain events)

在C#中,一個類可以定義其專屬的事件並且其它類可以註冊該事件並監聽,當事件被觸發時可以獲得事件通知。這對於對於桌面應用程式或獨立的Windows Service來說非常有用。但是, 對於Web應用程式來說會有點問題,因為物件是根據請求(request)被建立並且它們的生命週期都很短暫。我們很難註冊其它類別的事件。同樣地,直接註冊其它類別的事件也造成了類之間的耦合性。

在應用系統中,領域事件被用於解耦並且重用(re-use)商業邏輯。

事件匯流排

事件匯流排為一個單體(singleton)的物件,它由所有其它類所共享,可通過它觸發和處理事件

獲取預設例項

EventBus.Default.Trigger(…); //觸發事件

注入IEventBus事件介面(Injecting IEventBus)

public class TaskAppService : ApplicaService {
     public IEventBus EventBus { get; set; }
     public TaskAppService() {
        EventBus = NullEventBus.Instance;
     }
  }

事件引數繼承於EventData類,原始碼如下

    [Serializable]
    public abstract class EventData : IEventData
    {
        /// <summary>
        /// The time when the event occurred.
        /// </summary>
        public DateTime EventTime { get; set; }

        /// <summary>
        /// The object which triggers the event (optional).
        /// </summary>
        public object EventSource { get; set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        protected EventData()
        {
            EventTime = Clock.Now;
        }
    }

定義事件

ABP定義AbpHandledExceptionData事件並且在異常發生的時候自動地觸發這個事件。這在你想要取得更多關於異常的資訊時特別有用(即便ABP已自動地紀錄所有的異常)。你可以註冊這個事件並且設定它的觸發時機是在異常發生的時候。

ABP也提供在實體變更方面許多的通用事件資料類: EntityCreatedEventData, EntityUpdatedEventData和EntityDeletedEventData。它們被定義在Abp.Events.Bus.Entitis名稱空間中。當某個實體新增/更新/刪除後,這些事件會由ABP自動地觸發。如果你有一個Person實體,可以註冊到EntityCreatedEventData,事件會在新的Person實體建立且插入到資料庫後被觸發。這些事件也支援繼承。如果Student類繼承自Person類,並且你註冊到EntityCreatedEventData中,接著你將會在Person或Student新增後收到觸發。

觸發事件

public class TaskAppService : ApplicationService {
     public IEventBus EventBus { get; set; }
     public TaskAppService() {
        EventBus = NullEventBus.Instance;
     }

     public void CompleteTask(CompleteTaskInput input) {
        //TODO: 已完成資料庫上的任務
        EventBus.Trigger(new TaskCompletedEventData { TaskId = 42 } );
     }
  }

事件處理

要進行事件的處理,你應該要實現IEventHandler介面如下所示

public class ActivityWriter : IEventHandler<EventData>, ITransientDependency {
     public void HandleEvent(EventData eventData) {
        WriteActivity("A task is completed by id = " + eventData.TaskId);
     }
  } 

處理多個事件(Handling multiple events)

 public class ActivityWriter :
     IEventHandler<TaskCompletedEventData>,
     IEventHandler<TaskCreatedEventData>,
     ITransientDependency
  {
     public void HandleEvent(TaskCompletedEventData eventData) {
        //TODO: 處理事件
     }
     public void HandleEvent(TaskCreatedEventData eventData) {
        //TODO: 處理事件
     }
  }

註冊處理器

  • 自動型Automatically
    ABP框架掃描所有繼承了IEventHandler的介面實現類,並註冊到事件匯流排中,當事件發生時,可以通過DI來例項化物件,並呼叫事件方法。
  • 手動型(Manually)
EventBus.Register<TaskCompletedEventData>(eventData =>
{
   WriteActivity("A task is completed by id = " + eventData.TaskId);
});

取消註冊事件

//註冊一個事件

EventBus.Unregister<TaskCompletedEventData>(handler);

博主GitHub地址
https://github.com/yuyue5945

關注公眾號不迷路

  • 自動型Automatically
    ABP框架掃描所有繼承了IEventHandler的介面實現類,並註冊到事件匯流排中,當事件發生時,可以通過DI來例項化物件,並呼叫事件方法。
  • 手動型(Manually)
EventBus.Register<TaskCompletedEventData>(eventData =>
{
   WriteActivity("A task is completed by id = " + eventData.TaskId);
});

取消註冊事件

//註冊一個事件

EventBus.Unregister<TaskCompletedEventData>(handler);

博主GitHub地址
https://github.com/yuyue5945

關注公眾號不迷路

ABP 框架實戰系列(三)-領域層深入篇