ABP開發框架先後端開發系列---(3)框架的分層和文件組織

在前面隨筆《ABP開發框架先後端開發系列---(2)框架的初步介紹》中,我介紹了ABP應用框架的項目組織狀況,以及項目中領域層各個類代碼組織,以便基於數據庫應用的簡化處理。本篇隨筆進一步對ABP框架原有基礎項目進行必定的改進,減小領域業務層的處理,同時抽離領域對象的AutoMapper標記並使用配置文件代替,剝離應用服務層的DTO和接口定義,以便咱們使用更加方便和簡化,爲後續使用代碼生成工具結合相應分層代碼的快速生成作一個鋪墊。html

1)ABP項目的改進結構

ABP官網文檔裏面,對自定義倉儲類是不推薦的(除非找到合適的藉口須要作),同時對領域對象的業務管理類,也是持保留態度,認爲若是隻有一個應用入口的狀況(我主要考慮Web API優先),所以領域業務對象也能夠不用自定義,所以咱們整個ABP應用框架的思路就很清晰了,同時使用標準的倉儲類,基本上能夠解決絕大多數的數據操做。減小自定義業務管理類的目的是下降複雜度,同時咱們把DTO對象和領域對象的映射關係抽離到應有服務層的AutoMapper的Profile文件中定義,這樣能夠簡化DTO不依賴領域對象,所以DTO和應用服務層的接口能夠共享給相似Winform、UWP/WPF、控制檯程序等使用,避免重複定義,這點相似咱們傳統的Entity層。這裏我強調一點,這樣改進ABP框架,並無改變整個ABP應用框架的分層和調用規則,只是儘量的簡化和保持公用的內容。數據庫

改進後的解決方案項目結構以下所示。後端

以上是VS裏面解決方案的項目結構,我根據項目之間的關係,整理了一個架構的圖形,以下所示。架構

上圖中,其中橘紅色部分就是咱們爲各個層添加的類或者接口,分層上的序號是咱們須要逐步處理的內容,咱們來逐一解讀一下各個類或者接口的內容。app

 

2)項目分層的代碼

咱們介紹的基於領域驅動處理,第一步就是定義領域實體和數據庫表之間的關係,我這裏以字典模塊的表來進行舉例介紹。框架

首先咱們建立字典模塊裏面兩個表,兩個表的字段設計以下所示。異步

而其中咱們Id是業務對象的主鍵,全部表都是統一的,兩個表之間都有一部分重複的字段,是用來作操做記錄的。async

這個裏面咱們能夠記錄建立的用戶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的心得體會都儘可能寫出來和你們探討,同時也但願你們不要認爲我這些是灌水之做便可。

相關文章
相關標籤/搜索