在前面隨筆《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的心得體會都盡量寫出來和大家探討,同時也希望大家不要認為我這些是灌水之作即可。