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的心得体会都尽可能写出来和你们探讨,同时也但愿你们不要认为我这些是灌水之做便可。

相关文章
相关标签/搜索