ABP開發框架前後端開發系列---(3)框架的分層和檔案組織
在前面隨筆《ABP開發框架前後端開發系列---(2)框架的初步介紹》中,我介紹了ABP應用框架的專案組織情況,以及專案中領域層各個類程式碼組織,以便基於資料庫應用的簡化處理。本篇隨筆進一步對ABP框架原有基礎專案進行一定的改進,減少領域業務層的處理,同時抽離領域物件的AutoMapper標記並使用配置檔案代替,剝離應用服務層的DTO和介面定義,以便我們使用更加方便和簡化,為後續使用程式碼生成工具結合相應分層程式碼的快速生成做一個鋪墊。
1)ABP專案的改進結構
ABP官網文件裡面,對自定義倉儲類是不推薦的(除非找到合適的藉口需要做),同時對領域物件的業務管理類,也是持保留態度,認為如果只有一個應用入口的情況(我主要考慮Web API優先),因此領域業務物件也可以不用自定義,因此我們整個ABP應用框架的思路就很清晰了,同時使用標準的倉儲類,基本上可以解決絕大多數的資料操作。減少自定義業務管理類的目的是降低複雜度,同時我們把DTO物件和領域物件的對映關係抽離到應有服務層的AutoMapper的Profile檔案中定義,這樣可以簡化DTO不依賴領域物件,因此DTO和應用服務層的介面可以共享給類似Winform、UWP/WPF、控制檯程式等使用,避免重複定義,這點類似我們傳統的Entity層。這裡我強調一點,這樣改進ABP框架,並沒有改變整個ABP應用框架的分層和呼叫規則,只是儘可能的簡化和保持公用的內容。
改進後的解決方案專案結構如下所示。
以上是VS裡面解決方案的專案結構,我根據專案之間的關係,整理了一個架構的圖形,如下所示。
上圖中,其中橘紅色部分就是我們為各個層新增的類或者介面,分層上的序號是我們需要逐步處理的內容,我們來逐一解讀一下各個類或者介面的內容。
2)專案分層的程式碼
我們介紹的基於領域驅動處理,第一步就是定義領域實體和資料庫表之間的關係,我這裡以字典模組的表來進行舉例介紹。
首先我們建立字典模組裡面兩個表,兩個表的欄位設計如下所示。
而其中我們Id是業務物件的主鍵,所有表都是統一的,兩個表之間都有一部分重複的欄位,是用來做操作記錄的。
這個裡面我們可以記錄建立的使用者ID、建立時間、修改的使用者ID、修改時間、刪除的資訊等。
1)領域物件
例如我們定義字典型別的領域物件,如下程式碼所示。
[Table("TB_DictType")] public class DictType : FullAuditedEntity<string> { /// <summary> /// 型別名稱 /// </summary> [Required] public virtual string Name { get; set; } /// <summary> /// 字典程式碼 /// </summary> public virtual string Code { get; set; } /// <summary> /// 父ID /// </summary> public virtual string PID { get; set; } /// <summary> /// 備註 /// </summary> public virtual string Remark { get; set; } /// <summary> /// 排序 /// </summary> public virtual string Seq { get; set; } }
其中FullAuditedEntity<string>代表我需要記錄物件的增刪改時間和使用者資訊,當然還有AuditedEntity和CreationAuditedEntity基類物件,來標識記錄資訊的不同。
字典資料的領域物件定義如下所示。
[Table("TB_DictData")] public class DictData : FullAuditedEntity<string> { /// <summary> /// 字典型別ID /// </summary> [Required] public virtual string DictType_ID { get; set; } /// <summary> /// 字典大類 /// </summary> [ForeignKey("DictType_ID")] public virtual DictType DictType { get; set; } /// <summary> /// 字典名稱 /// </summary> [Required] public virtual string Name { get; set; } /// <summary> /// 字典值 /// </summary> public virtual string Value { get; set; } /// <summary> /// 備註 /// </summary> public virtual string Remark { get; set; } /// <summary> /// 排序 /// </summary> public virtual string Seq { get; set; } }
這裡注意我們有一個外來鍵DictType_ID,同時有一個DictType物件的資訊,這個我們使用倉儲物件操作就很方便獲取到對應的字典型別物件了。
[ForeignKey("DictType_ID")] public virtual DictType DictType { get; set; }
2)EF的倉儲核心層
這個部分我們基本上不需要什麼改動,我們只需要加入我們定義好的倉儲物件DbSet即可,如下所示。
public class MyProjectDbContext : AbpZeroDbContext<Tenant, Role, User, MyProjectDbContext> { //字典內容 public virtual DbSet<DictType> DictType { get; set; } public virtual DbSet<DictData> DictData { get; set; } public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options) : base(options) { } }
通過上面程式碼,我們可以看到,我們每加入一個領域物件實體,在這裡就需要增加一個DbSet的物件屬性,至於它們是如何協同處理倉儲模式的,我們可以暫不關心它的機制。
3)應用服務通用層
這個專案分層裡面,我們主要放置在各個模組裡面公用的DTO和應用服務介面類。
例如我們定義字典型別的DTO物件,如下所示,這裡涉及的DTO,沒有使用AutoMapper的標記。
/// <summary> /// 字典物件DTO /// </summary> public class DictTypeDto : EntityDto<string> { /// <summary> /// 型別名稱 /// </summary> [Required] public virtual string Name { get; set; } /// <summary> /// 字典程式碼 /// </summary> public virtual string Code { get; set; } /// <summary> /// 父ID /// </summary> public virtual string PID { get; set; } /// <summary> /// 備註 /// </summary> public virtual string Remark { get; set; } /// <summary> /// 排序 /// </summary> public virtual string Seq { get; set; } }
字典型別的應用服務層介面定義如下所示。
public interface IDictTypeAppService : IAsyncCrudAppService<DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto> { /// <summary> /// 獲取所有字典型別的列表集合(Key為名稱,Value為ID值) /// </summary> /// <param name="dictTypeId">字典型別ID,為空則返回所有</param> /// <returns></returns> Task<Dictionary<string, string>> GetAllType(string dictTypeId); /// <summary> /// 獲取字典型別一級列表及其下面的內容 /// </summary> /// <param name="pid">如果指定PID,那麼找它下面的記錄,否則獲取所有</param> /// <returns></returns> Task<IList<DictTypeNodeDto>> GetTree(string pid); }
從上面的介面程式碼,我們可以看到,字典型別的介面基類是基於非同步CRUD操作的基類介面IAsyncCrudAppService,這個是在ABP核心專案的Abp.ZeroCore專案裡面,使用它需要引入對應的專案依賴
而基於IAsyncCrudAppService的介面定義,我們往往還需要多定義幾個DTO物件,如建立物件、更新物件、刪除物件、分頁物件等等。
如字典型別的建立物件DTO類定義如下所示,由於操作內容沒有太多差異,我們可以簡單的繼承自DictTypeDto即可。
/// <summary> /// 字典型別建立物件 /// </summary> public class CreateDictTypeDto : DictTypeDto { }
IAsyncCrudAppService定義了幾個通用的建立、更新、刪除、獲取單個物件和獲取所有物件列表的介面,介面定義如下所示。
namespace Abp.Application.Services { public interface IAsyncCrudAppService<TEntityDto, TPrimaryKey, in TGetAllInput, in TCreateInput, in TUpdateInput, in TGetInput, in TDeleteInput> : IApplicationService, ITransientDependency where TEntityDto : IEntityDto<TPrimaryKey> where TUpdateInput : IEntityDto<TPrimaryKey> where TGetInput : IEntityDto<TPrimaryKey> where TDeleteInput : IEntityDto<TPrimaryKey> { Task<TEntityDto> Create(TCreateInput input); Task Delete(TDeleteInput input); Task<TEntityDto> Get(TGetInput input); Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input); Task<TEntityDto> Update(TUpdateInput input); } }
而由於這個介面定義了這些通用處理介面,我們在做應用服務類的實現的時候,都往往基於基類AsyncCrudAppService,預設具有以上介面的實現。
同理,對於字典資料物件的操作類似,我們建立相關的DTO物件和應用服務層介面。
/// <summary> /// 字典資料的DTO /// </summary> public class DictDataDto : EntityDto<string> { /// <summary> /// 字典型別ID /// </summary> [Required] public virtual string DictType_ID { get; set; } /// <summary> /// 字典名稱 /// </summary> [Required] public virtual string Name { get; set; } /// <summary> /// 指定值 /// </summary> public virtual string Value { get; set; } /// <summary> /// 備註 /// </summary> public virtual string Remark { get; set; } /// <summary> /// 排序 /// </summary> public virtual string Seq { get; set; } } /// <summary> /// 建立字典資料的DTO /// </summary> public class CreateDictDataDto : DictDataDto { }
/// <summary> /// 字典資料的應用服務層介面 /// </summary> public interface IDictDataAppService : IAsyncCrudAppService<DictDataDto, string, PagedResultRequestDto, CreateDictDataDto, DictDataDto> { /// <summary> /// 根據字典型別ID獲取所有該型別的字典列表集合(Key為名稱,Value為值) /// </summary> /// <param name="dictTypeId">字典型別ID</param> /// <returns></returns> Task<Dictionary<string, string>> GetDictByTypeID(string dictTypeId); /// <summary> /// 根據字典型別名稱獲取所有該型別的字典列表集合(Key為名稱,Value為值) /// </summary> /// <param name="dictType">字典型別名稱</param> /// <returns></returns> Task<Dictionary<string, string>> GetDictByDictType(string dictTypeName); }
4)應用服務層實現
應用服務層是整個ABP框架的靈魂所在,對內協同倉儲物件實現資料的處理,對外配合Web.Core、Web.Host專案提供Web API的服務,而Web.Core、Web.Host專案幾乎不需要進行修改,因此應用服務層就是一個非常關鍵的部分,需要考慮對使用者登入的驗證、介面許可權的認證、以及對審計日誌的記錄處理,以及異常的跟蹤和傳遞,基本上應用服務層就是一個大內總管的角色,重要性不言而喻。
應用服務層只需要根據應用服務通用層的DTO和服務介面,利用標準的倉儲物件進行資料的處理呼叫即可。
如對於字典型別的應用服務層實現類程式碼如下所示。
/// <summary> /// 字典型別應用服務層實現 /// </summary> [AbpAuthorize] public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService { /// <summary> /// 標準的倉儲物件 /// </summary> private readonly IRepository<DictType, string> _repository; public DictTypeAppService(IRepository<DictType, string> repository) : base(repository) { _repository = repository; } /// <summary> /// 獲取所有字典型別的列表集合(Key為名稱,Value為ID值) /// </summary> /// <returns></returns> public async Task<Dictionary<string, string>> GetAllType(string dictTypeId) { IList<DictType> list = null; if (!string.IsNullOrWhiteSpace(dictTypeId)) { list = await Repository.GetAllListAsync(p => p.PID == dictTypeId); } else { list = await Repository.GetAllListAsync(); } Dictionary<string, string> dict = new Dictionary<string, string>(); foreach (var info in list) { if (!dict.ContainsKey(info.Name)) { dict.Add(info.Name, info.Id); } } return dict; } /// <summary> /// 獲取字典型別一級列表及其下面的內容 /// </summary> /// <param name="pid">如果指定PID,那麼找它下面的記錄,否則獲取所有</param> /// <returns></returns> public async Task<IList<DictTypeNodeDto>> GetTree(string pid) { //確保PID非空 pid = string.IsNullOrWhiteSpace(pid) ? "-1" : pid; List<DictTypeNodeDto> typeNodeList = new List<DictTypeNodeDto>(); var topList = Repository.GetAllList(s => s.PID == pid).MapTo<List<DictTypeNodeDto>>();//頂級內容 foreach(var dto in topList) { var subList = Repository.GetAllList(s => s.PID == dto.Id).MapTo<List<DictTypeNodeDto>>(); if (subList != null && subList.Count > 0) { dto.Children.AddRange(subList); } } return await Task.FromResult(topList); } }
我們可以看到,標準的增刪改查操作,我們不需要實現,因為已經在基類應用服務類AsyncCrudAppService,預設具有這些介面的實現。
而我們在類的時候,看到一個宣告的標籤[AbpAuthorize],就是對這個服務層的訪問,需要使用者的授權登入才可以訪問。
5)Web.Host Web API宿主層
如我們在Web.Host專案裡面啟動的Swagger介面測試頁面裡面,就是需要先登入的。
這樣我們測試字典型別或者字典資料的介面,才能返回響應的資料。
由於篇幅的關係,後面在另起篇章介紹如何封裝Web API的呼叫類,並在控制檯程式和Winform程式中對Web API介面服務層的呼叫,以後還會考慮在Ant-Design(React)和IVIew(Vue)裡面進行Web介面的封裝呼叫。
這兩天把這一個月來研究ABP的心得體會都儘量寫出來和大家探討,同時也希望大家不要認為我這些是灌水之作即可。