在前面隨筆《ABP開發(fā)框架前后端開發(fā)系列---(2)框架的初步介紹》中,我介紹了ABP應(yīng)用框架的項(xiàng)目組織情況,以及項(xiàng)目中領(lǐng)域?qū)痈鱾€(gè)類代碼組織,以便基于數(shù)據(jù)庫(kù)應(yīng)用的簡(jiǎn)化處理。本篇隨筆進(jìn)一步對(duì)ABP框架原有基礎(chǔ)項(xiàng)目進(jìn)行一定的改進(jìn),減少領(lǐng)域業(yè)務(wù)層的處理,同時(shí)抽離領(lǐng)域?qū)ο蟮腁utoMapper標(biāo)記并使用配置文件代替,剝離應(yīng)用服務(wù)層的DTO和接口定義,以便我們使用更加方便和簡(jiǎn)化,為后續(xù)使用代碼生成工具結(jié)合相應(yīng)分層代碼的快速生成做一個(gè)鋪墊。
1)ABP項(xiàng)目的改進(jìn)結(jié)構(gòu)
ABP官網(wǎng)文檔里面,對(duì)自定義倉(cāng)儲(chǔ)類是不推薦的(除非找到合適的借口需要做),同時(shí)對(duì)領(lǐng)域?qū)ο蟮臉I(yè)務(wù)管理類,也是持保留態(tài)度,認(rèn)為如果只有一個(gè)應(yīng)用入口的情況(我主要考慮Web API優(yōu)先),因此領(lǐng)域業(yè)務(wù)對(duì)象也可以不用自定義,因此我們整個(gè)ABP應(yīng)用框架的思路就很清晰了,同時(shí)使用標(biāo)準(zhǔn)的倉(cāng)儲(chǔ)類,基本上可以解決絕大多數(shù)的數(shù)據(jù)操作。減少自定義業(yè)務(wù)管理類的目的是降低復(fù)雜度,同時(shí)我們把DTO對(duì)象和領(lǐng)域?qū)ο蟮挠成潢P(guān)系抽離到應(yīng)有服務(wù)層的AutoMapper的Profile文件中定義,這樣可以簡(jiǎn)化DTO不依賴領(lǐng)域?qū)ο螅虼薉TO和應(yīng)用服務(wù)層的接口可以共享給類似Winform、UWP/WPF、控制臺(tái)程序等使用,避免重復(fù)定義,這點(diǎn)類似我們傳統(tǒng)的Entity層。這里我強(qiáng)調(diào)一點(diǎn),這樣改進(jìn)ABP框架,并沒有改變整個(gè)ABP應(yīng)用框架的分層和調(diào)用規(guī)則,只是盡可能的簡(jiǎn)化和保持公用的內(nèi)容。
改進(jìn)后的解決方案項(xiàng)目結(jié)構(gòu)如下所示。

以上是VS里面解決方案的項(xiàng)目結(jié)構(gòu),我根據(jù)項(xiàng)目之間的關(guān)系,整理了一個(gè)架構(gòu)的圖形,如下所示。

上圖中,其中橘紅色部分就是我們?yōu)楦鱾€(gè)層添加的類或者接口,分層上的序號(hào)是我們需要逐步處理的內(nèi)容,我們來逐一解讀一下各個(gè)類或者接口的內(nèi)容。
2)項(xiàng)目分層的代碼
我們介紹的基于領(lǐng)域驅(qū)動(dòng)處理,第一步就是定義領(lǐng)域?qū)嶓w和數(shù)據(jù)庫(kù)表之間的關(guān)系,我這里以字典模塊的表來進(jìn)行舉例介紹。
首先我們創(chuàng)建字典模塊里面兩個(gè)表,兩個(gè)表的字段設(shè)計(jì)如下所示。

而其中我們Id是業(yè)務(wù)對(duì)象的主鍵,所有表都是統(tǒng)一的,兩個(gè)表之間都有一部分重復(fù)的字段,是用來做操作記錄的。

這個(gè)里面我們可以記錄創(chuàng)建的用戶ID、創(chuàng)建時(shí)間、修改的用戶ID、修改時(shí)間、刪除的信息等。
1)領(lǐng)域?qū)ο?/strong>
例如我們定義字典類型的領(lǐng)域?qū)ο?,如下代碼所示。
[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>代表我需要記錄對(duì)象的增刪改時(shí)間和用戶信息,當(dāng)然還有AuditedEntity和CreationAuditedEntity基類對(duì)象,來標(biāo)識(shí)記錄信息的不同。
字典數(shù)據(jù)的領(lǐng)域?qū)ο蠖x如下所示。
[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; }
}
這里注意我們有一個(gè)外鍵DictType_ID,同時(shí)有一個(gè)DictType對(duì)象的信息,這個(gè)我們使用倉(cāng)儲(chǔ)對(duì)象操作就很方便獲取到對(duì)應(yīng)的字典類型對(duì)象了。
[ForeignKey("DictType_ID")]
public virtual DictType DictType { get; set; }
2)EF的倉(cāng)儲(chǔ)核心層
這個(gè)部分我們基本上不需要什么改動(dòng),我們只需要加入我們定義好的倉(cāng)儲(chǔ)對(duì)象DbSet即可,如下所示。
public class MyProjectDbContext : AbpZeroDbContext<Tenant, Role, User, MyProjectDbContext>
{
//字典內(nèi)容
public virtual DbSet<DictType> DictType { get; set; }
public virtual DbSet<DictData> DictData { get; set; }
public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options)
: base(options)
{
}
}
通過上面代碼,我們可以看到,我們每加入一個(gè)領(lǐng)域?qū)ο髮?shí)體,在這里就需要增加一個(gè)DbSet的對(duì)象屬性,至于它們是如何協(xié)同處理倉(cāng)儲(chǔ)模式的,我們可以暫不關(guān)心它的機(jī)制。
3)應(yīng)用服務(wù)通用層
這個(gè)項(xiàng)目分層里面,我們主要放置在各個(gè)模塊里面公用的DTO和應(yīng)用服務(wù)接口類。
例如我們定義字典類型的DTO對(duì)象,如下所示,這里涉及的DTO,沒有使用AutoMapper的標(biāo)記。
/// <summary>
/// 字典對(duì)象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; }
}
字典類型的應(yīng)用服務(wù)層接口定義如下所示。
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>
/// 獲取字典類型一級(jí)列表及其下面的內(nèi)容
/// </summary>
/// <param name="pid">如果指定PID,那么找它下面的記錄,否則獲取所有</param>
/// <returns></returns>
Task<IList<DictTypeNodeDto>> GetTree(string pid);
}
從上面的接口代碼,我們可以看到,字典類型的接口基類是基于異步CRUD操作的基類接口IAsyncCrudAppService,這個(gè)是在ABP核心項(xiàng)目的Abp.ZeroCore項(xiàng)目里面,使用它需要引入對(duì)應(yīng)的項(xiàng)目依賴

而基于IAsyncCrudAppService的接口定義,我們往往還需要多定義幾個(gè)DTO對(duì)象,如創(chuàng)建對(duì)象、更新對(duì)象、刪除對(duì)象、分頁(yè)對(duì)象等等。
如字典類型的創(chuàng)建對(duì)象DTO類定義如下所示,由于操作內(nèi)容沒有太多差異,我們可以簡(jiǎn)單的繼承自DictTypeDto即可。
/// <summary>
/// 字典類型創(chuàng)建對(duì)象
/// </summary>
public class CreateDictTypeDto : DictTypeDto
{
}
IAsyncCrudAppService定義了幾個(gè)通用的創(chuàng)建、更新、刪除、獲取單個(gè)對(duì)象和獲取所有對(duì)象列表的接口,接口定義如下所示。
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);
}
}
而由于這個(gè)接口定義了這些通用處理接口,我們?cè)谧鰬?yīng)用服務(wù)類的實(shí)現(xiàn)的時(shí)候,都往往基于基類AsyncCrudAppService,默認(rèn)具有以上接口的實(shí)現(xiàn)。
同理,對(duì)于字典數(shù)據(jù)對(duì)象的操作類似,我們創(chuàng)建相關(guān)的DTO對(duì)象和應(yīng)用服務(wù)層接口。
/// <summary>
/// 字典數(shù)據(jù)的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>
/// 創(chuàng)建字典數(shù)據(jù)的DTO
/// </summary>
public class CreateDictDataDto : DictDataDto
{
}
/// <summary>
/// 字典數(shù)據(jù)的應(yīng)用服務(wù)層接口
/// </summary>
public interface IDictDataAppService : IAsyncCrudAppService<DictDataDto, string, PagedResultRequestDto, CreateDictDataDto, DictDataDto>
{
/// <summary>
/// 根據(jù)字典類型ID獲取所有該類型的字典列表集合(Key為名稱,Value為值)
/// </summary>
/// <param name="dictTypeId">字典類型ID</param>
/// <returns></returns>
Task<Dictionary<string, string>> GetDictByTypeID(string dictTypeId);
/// <summary>
/// 根據(jù)字典類型名稱獲取所有該類型的字典列表集合(Key為名稱,Value為值)
/// </summary>
/// <param name="dictType">字典類型名稱</param>
/// <returns></returns>
Task<Dictionary<string, string>> GetDictByDictType(string dictTypeName);
}
4)應(yīng)用服務(wù)層實(shí)現(xiàn)
應(yīng)用服務(wù)層是整個(gè)ABP框架的靈魂所在,對(duì)內(nèi)協(xié)同倉(cāng)儲(chǔ)對(duì)象實(shí)現(xiàn)數(shù)據(jù)的處理,對(duì)外配合Web.Core、Web.Host項(xiàng)目提供Web API的服務(wù),而Web.Core、Web.Host項(xiàng)目幾乎不需要進(jìn)行修改,因此應(yīng)用服務(wù)層就是一個(gè)非常關(guān)鍵的部分,需要考慮對(duì)用戶登錄的驗(yàn)證、接口權(quán)限的認(rèn)證、以及對(duì)審計(jì)日志的記錄處理,以及異常的跟蹤和傳遞,基本上應(yīng)用服務(wù)層就是一個(gè)大內(nèi)總管的角色,重要性不言而喻。

應(yīng)用服務(wù)層只需要根據(jù)應(yīng)用服務(wù)通用層的DTO和服務(wù)接口,利用標(biāo)準(zhǔn)的倉(cāng)儲(chǔ)對(duì)象進(jìn)行數(shù)據(jù)的處理調(diào)用即可。
如對(duì)于字典類型的應(yīng)用服務(wù)層實(shí)現(xiàn)類代碼如下所示。
/// <summary>
/// 字典類型應(yīng)用服務(wù)層實(shí)現(xiàn)
/// </summary>
[AbpAuthorize]
public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
/// <summary>
/// 標(biāo)準(zhǔn)的倉(cāng)儲(chǔ)對(duì)象
/// </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>
/// 獲取字典類型一級(jí)列表及其下面的內(nèi)容
/// </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>>();//頂級(jí)內(nèi)容
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);
}
}
我們可以看到,標(biāo)準(zhǔn)的增刪改查操作,我們不需要實(shí)現(xiàn),因?yàn)橐呀?jīng)在基類應(yīng)用服務(wù)類AsyncCrudAppService,默認(rèn)具有這些接口的實(shí)現(xiàn)。
而我們?cè)陬惖臅r(shí)候,看到一個(gè)聲明的標(biāo)簽[AbpAuthorize],就是對(duì)這個(gè)服務(wù)層的訪問,需要用戶的授權(quán)登錄才可以訪問。
5)Web.Host Web API宿主層
如我們?cè)赪eb.Host項(xiàng)目里面啟動(dòng)的Swagger接口測(cè)試頁(yè)面里面,就是需要先登錄的。

這樣我們測(cè)試字典類型或者字典數(shù)據(jù)的接口,才能返回響應(yīng)的數(shù)據(jù)。

由于篇幅的關(guān)系,后面在另起篇章介紹如何封裝Web API的調(diào)用類,并在控制臺(tái)程序和Winform程序中對(duì)Web API接口服務(wù)層的調(diào)用,以后還會(huì)考慮在Ant-Design(React)和IVIew(Vue)里面進(jìn)行Web界面的封裝調(diào)用。
這兩天把這一個(gè)月來研究ABP的心得體會(huì)都盡量寫出來和大家探討,同時(shí)也希望大家不要認(rèn)為我這些是灌水之作即可。