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 应用框架的分层和调用规则, 只是尽可能的简化和保持公用的内容.
改进后的解决方案项目结构如下所示.
以上是 VS 里面解决方案的项目结构, 我根据项目之间的关系, 整理了一个架构的图形, 如下所示.
上图中, 其中橘红色部分就是我们为各个层添加的类或者接口, 分层上的序号是我们需要逐步处理的内容, 我们来逐一解读一下各个类或者接口的内容.
2) 项目分层的代码
我们介绍的基于领域驱动处理, 第一步就是定义领域实体和数据库表之间的关系, 我这里以字典模块的表来进行举例介绍.
首先我们创建字典模块里面两个表, 两个表的字段设计如下所示.
而其中我们 Id 是业务对象的主键, 所有表都是统一的, 两个表之间都有一部分重复的字段, 是用来做操作记录的.
这个里面我们可以记录创建的用户 ID, 创建时间, 修改的用户 ID, 修改时间, 删除的信息等.
1) 领域对象
例如我们定义字典类型的领域对象, 如下代码所示.
- [Table("TB_DictType")]
- public class DictType : FullAuditedEntity
- {
- ///
- /// 类型名称
- ///
- [Required]
- public virtual string Name { get; set; }
- ///
- /// 字典代码
- ///
- public virtual string Code { get; set; }
- ///
- /// 父 ID
- ///
- public virtual string PID { get; set; }
- ///
- /// 备注
- ///
- public virtual string Remark { get; set; }
- ///
- /// 排序
- ///
- public virtual string Seq { get; set; }
- }
其中 FullAuditedEntity 代表我需要记录对象的增删改时间和用户信息, 当然还有 AuditedEntity 和 CreationAuditedEntity 基类对象, 来标识记录信息的不同.
字典数据的领域对象定义如下所示.
- [Table("TB_DictData")]
- public class DictData : FullAuditedEntity
- {
- ///
- /// 字典类型 ID
- ///
- [Required]
- public virtual string DictType_ID { get; set; }
- ///
- /// 字典大类
- ///
- [ForeignKey("DictType_ID")]
- public virtual DictType DictType { get; set; }
- ///
- /// 字典名称
- ///
- [Required]
- public virtual string Name { get; set; }
- ///
- /// 字典值
- ///
- public virtual string Value { get; set; }
- ///
- /// 备注
- ///
- public virtual string Remark { get; set; }
- ///
- /// 排序
- ///
- 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
- {
- // 字典内容
- public virtual DbSet DictType { get; set; }
- public virtual DbSet DictData { get; set; }
- public MyProjectDbContext(DbContextOptions options)
- : base(options)
- {
- }
- }
通过上面代码, 我们可以看到, 我们每加入一个领域对象实体, 在这里就需要增加一个 DbSet 的对象属性, 至于它们是如何协同处理仓储模式的, 我们可以暂不关心它的机制.
3) 应用服务通用层
这个项目分层里面, 我们主要放置在各个模块里面公用的 DTO 和应用服务接口类.
例如我们定义字典类型的 DTO 对象, 如下所示, 这里涉及的 DTO, 没有使用 AutoMapper 的标记.
- ///
- /// 字典对象 DTO
- ///
- public class DictTypeDto : EntityDto
- {
- ///
- /// 类型名称
- ///
- [Required]
- public virtual string Name { get; set; }
- ///
- /// 字典代码
- ///
- public virtual string Code { get; set; }
- ///
- /// 父 ID
- ///
- public virtual string PID { get; set; }
- ///
- /// 备注
- ///
- public virtual string Remark { get; set; }
- ///
- /// 排序
- ///
- public virtual string Seq { get; set; }
- }
字典类型的应用服务层接口定义如下所示.
- public interface IDictTypeAppService : IAsyncCrudAppService
- {
- ///
- /// 获取所有字典类型的列表集合 (Key 为名称, Value 为 ID 值)
- ///
- /// 字典类型 ID, 为空则返回所有
- ///
- Task> GetAllType(string dictTypeId);
- ///
- /// 获取字典类型一级列表及其下面的内容
- ///
- /// 如果指定 PID, 那么找它下面的记录, 否则获取所有
- ///
- Task> GetTree(string pid);
- }
从上面的接口代码, 我们可以看到, 字典类型的接口基类是基于异步 CRUD 操作的基类接口 IAsyncCrudAppService, 这个是在 ABP 核心项目的 Abp.ZeroCore 项目里面, 使用它需要引入对应的项目依赖
而基于 IAsyncCrudAppService 的接口定义, 我们往往还需要多定义几个 DTO 对象, 如创建对象, 更新对象, 删除对象, 分页对象等等.<喎"/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+yOfX1rXkwODQzbXEtLS9qLbUz/NEVE/A4Lao0uXI58/Cy/nKvqOs08nT2rLZ1/fE2sjdw7vT0MyrtuCy7tLso6zO0sPHv8nS1LzytaW1xLzMs9DX1ERpY3RUeXBlRHRvvLS/yaGjPC9wPgoKPHByZSBjbGFzcz0="brush:java;"> ///
- /// 字典类型创建对象 ///
- public class CreateDictTypeDto : DictTypeDto {
- }
IAsyncCrudAppService 定义了几个通用的创建, 更新, 删除, 获取单个对象和获取所有对象列表的接口, 接口定义如下所示.
- namespace Abp.Application.Services
- {
- public interface IAsyncCrudAppService : IApplicationService, ITransientDependency
- where TEntityDto : IEntityDto
- where TUpdateInput : IEntityDto
- where TGetInput : IEntityDto
- where TDeleteInput : IEntityDto
- {
- Task Create(TCreateInput input);
- Task Delete(TDeleteInput input);
- Task Get(TGetInput input);
- Task> GetAll(TGetAllInput input);
- Task Update(TUpdateInput input);
- }
- }
而由于这个接口定义了这些通用处理接口, 我们在做应用服务类的实现的时候, 都往往基于基类 AsyncCrudAppService, 默认具有以上接口的实现.
同理, 对于字典数据对象的操作类似, 我们创建相关的 DTO 对象和应用服务层接口.
- ///
- /// 字典数据的 DTO
- ///
- public class DictDataDto : EntityDto
- {
- ///
- /// 字典类型 ID
- ///
- [Required]
- public virtual string DictType_ID { get; set; }
- ///
- /// 字典名称
- ///
- [Required]
- public virtual string Name { get; set; }
- ///
- /// 指定值
- ///
- public virtual string Value { get; set; }
- ///
- /// 备注
- ///
- public virtual string Remark { get; set; }
- ///
- /// 排序
- ///
- public virtual string Seq { get; set; }
- }
- ///
- /// 创建字典数据的 DTO
- ///
- public class CreateDictDataDto : DictDataDto
- {
- }
- ///
- /// 字典数据的应用服务层接口
- ///
- public interface IDictDataAppService : IAsyncCrudAppService
- {
- ///
- /// 根据字典类型 ID 获取所有该类型的字典列表集合 (Key 为名称, Value 为值)
- ///
- /// 字典类型 ID
- ///
- Task> GetDictByTypeID(string dictTypeId);
- ///
- /// 根据字典类型名称获取所有该类型的字典列表集合 (Key 为名称, Value 为值)
- ///
- /// 字典类型名称
- ///
- Task> GetDictByDictType(string dictTypeName);
- }
4) 应用服务层实现
应用服务层是整个 ABP 框架的灵魂所在, 对内协同仓储对象实现数据的处理, 对外配合 Web.Core,Web.Host 项目提供 Web API 的服务, 而 Web.Core,Web.Host 项目几乎不需要进行修改, 因此应用服务层就是一个非常关键的部分, 需要考虑对用户登录的验证, 接口权限的认证, 以及对审计日志的记录处理, 以及异常的跟踪和传递, 基本上应用服务层就是一个大内总管的角色, 重要性不言而喻.
应用服务层只需要根据应用服务通用层的 DTO 和服务接口, 利用标准的仓储对象进行数据的处理调用即可.
如对于字典类型的应用服务层实现类代码如下所示.
- ///
- /// 字典类型应用服务层实现
- ///
- [AbpAuthorize]
- public class DictTypeAppService : MyAsyncServiceBase, IDictTypeAppService
- {
- ///
- /// 标准的仓储对象
- ///
- private readonly IRepository _repository;
- public DictTypeAppService(IRepository repository) : base(repository)
- {
- _repository = repository;
- }
- ///
- /// 获取所有字典类型的列表集合 (Key 为名称, Value 为 ID 值)
- ///
- ///
- public async Task> GetAllType(string dictTypeId)
- {
- IList list = null;
- if (!string.IsNullOrWhiteSpace(dictTypeId))
- {
- list = await Repository.GetAllListAsync(p => p.PID == dictTypeId);
- }
- else
- {
- list = await Repository.GetAllListAsync();
- }
- Dictionary dict = new Dictionary();
- foreach (var info in list)
- {
- if (!dict.ContainsKey(info.Name))
- {
- dict.Add(info.Name, info.Id);
- }
- }
- return dict;
- }
- ///
- /// 获取字典类型一级列表及其下面的内容
- ///
- /// 如果指定 PID, 那么找它下面的记录, 否则获取所有
- ///
- public async Task> GetTree(string pid)
- {
- // 确保 PID 非空
- pid = string.IsNullOrWhiteSpace(pid) ? "-1" : pid;
- List typeNodeList = new List();
- var topList = Repository.GetAllList(s => s.PID == pid).MapTo>();// 顶级内容
- foreach(var dto in topList)
- {
- var subList = Repository.GetAllList(s => s.PID == dto.Id).MapTo>();
- 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 的心得体会都尽量写出来和大家探讨, 同时也希望大家不要认为我这些是灌水之作即可.
来源: https://www.2cto.com/kf/201905/810130.html