ABP開發(fā)框架前后端開發(fā)系列---(3)框架的分層和文件組織

在前面隨筆《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)如下所示。

image

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

image

上圖中,其中橘紅色部分就是我們?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ì)如下所示。

image

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

image

這個(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)目依賴

image

而基于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)總管的角色,重要性不言而喻。

image

應(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è)面里面,就是需要先登錄的。

image

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

image

由于篇幅的關(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)為我這些是灌水之作即可。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容