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应用框架的分层和调用规则,只是尽可能的简化和保持公用的内容。

改进后的解决方案项目结构如下所示。

image

以上是VS里面解决方案的项目结构,我根据项目之间的关系,整理了一个架构的图形,如下所示。

image

上图中,其中橘红色部分就是我们为各个层添加的类或者接口,分层上的序号是我们需要逐步处理的内容,我们来逐一解读一下各个类或者接口的内容。

2)项目分层的代码

我们介绍的基于领域驱动处理,第一步就是定义领域实体和数据库表之间的关系,我这里以字典模块的表来进行举例介绍。

首先我们创建字典模块里面两个表,两个表的字段设计如下所示。

image

而其中我们Id是业务对象的主键,所有表都是统一的,两个表之间都有一部分重复的字段,是用来做操作记录的。

image

这个里面我们可以记录创建的用户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项目里面,使用它需要引入对应的项目依赖

image

而基于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项目几乎不需要进行修改,因此应用服务层就是一个非常关键的部分,需要考虑对用户登录的验证、接口权限的认证、以及对审计日志的记录处理,以及异常的跟踪和传递,基本上应用服务层就是一个大内总管的角色,重要性不言而喻。

image

应用服务层只需要根据应用服务通用层的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接口测试页面里面,就是需要先登录的。

image

这样我们测试字典类型或者字典数据的接口,才能返回响应的数据。

image

由于篇幅的关系,后面在另起篇章介绍如何封装Web API的调用类,并在控制台程序和Winform程序中对Web API接口服务层的调用,以后还会考虑在Ant-Design(React)和IVIew(Vue)里面进行Web界面的封装调用。

这两天把这一个月来研究ABP的心得体会都尽量写出来和大家探讨,同时也希望大家不要认为我这些是灌水之作即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容