在前面随笔《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<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 > )
{
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的心得体会都尽量写出来和大家探讨,同时也希望大家不要认为我这些是灌水之作即可。

ABP开发框架前后端开发系列---(3)框架的分层和文件组织的更多相关文章

  1. ABP开发框架前后端开发系列---(2)框架的初步介绍

    在前面随笔<ABP开发框架前后端开发系列---(1)框架的总体介绍>大概介绍了这个ABP框架的主要特点,以及介绍了我对这框架的Web API应用优先的一些看法,本篇继续探讨ABP框架的初步 ...

  2. ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理

    在前面两篇随笔<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>和<ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程>开始 ...

  3. ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程

    在前面随笔介绍的<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进 ...

  4. ABP开发框架前后端开发系列---(16)ABP框架升级最新版本的经验总结

    有一小段时间没有持续升级ABP框架了,最近就因应客户的需要,把ABP框架进行全面的更新,由于我们应用的ABP框架,基础部分还是会使用官方的内容,因此升级的时候需要把官方基础ABP的DLL进行全面的更新 ...

  5. ABP开发框架前后端开发系列---(10)Web API调用类的简化处理

    在较早期的随笔<ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用>已经介绍了Web API调用类的封装处理,虽然这些调用类我们可以使用代码生成工具快 ...

  6. ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用

    在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开 ...

  7. ABP开发框架前后端开发系列---(4)Web API调用类的封装和使用

    在前面随笔介绍ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以及简化了ABP框架的各个层的内容,使得我们项目结构更加清晰.上篇随笔已经介绍了字典模块中应用服务层接口的实现情况,并且通过 ...

  8. ABP开发框架前后端开发系列---(11)菜单的动态管理

    在前面随笔<ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理>中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现 ...

  9. ABP开发框架前后端开发系列---(12)配置模块的管理

    一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查 ...

随机推荐

  1. 新手用WPF山寨QQ管家7.6(三)

    由于一直忙工作,没有更新完博客,更可恨的是...在清理资料的时候不小心删除了之前自己做的各种效果的DEMO....好在项目中用到了大部分,也算有所保留,以后可不敢随便删东西了....太可怕了! 在 新 ...

  2. Androd安全——反编译技术完全解析

    )第二步成功后我们会发现在当前目录下多了一个<APKName>文件夹,这个文件夹中存放的就是反编译的结果了.我们可以打开AndroidManifest.xml.res/layout即可查看 ...

  3. golang echo livereload

    echo on port 1323 gin -a 1323 run server.go go get github.com/codegangsta/gin gin -h

  4. java线程安全问题原因及解决办法

    1.为什么会出现线程安全问题 计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址.当多个线程并发访问同一个内存地址并且内存地址保存的值是 ...

  5. Wordpress 通过 post id 获取文章 url

    global $post; echo get_permalink($post->ID); 函数详解: Codex - get_permalink() 注意:有些链接是通过 SEO 重定向的,比如 ...

  6. http状态码相关知识(复制)

    HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的.HTTP有两类报文:请求报文和响应报文. HTTP请求报文 一个HTTP请求报文由请求行(request ...

  7. 编译php 转

    LAOGAO added 20140907: configure: error: mcrypt.h not found. Please reinstall libmcrypt.复制代码 代码如下:wg ...

  8. ssh公钥私钥

    ssh配置文件vi /etc/ssh/sshd_config putty下载http://www.chiark.greenend.org.uk/~sgtatham/putty/download.htm ...

  9. C#的一些基本问题

    静态类和静态变量静态类的定义:static class 类名 静态方法和变量必须使用类名来引用,而不能使用实例化后的对象,因为,静态变量不属于任何实例,而是共有的. 非静态类里面既可以定义静态方法也可 ...

  10. 笔记:CS231n+assignment2(作业二)(一)

    第二个作业难度很高,但做(抄)完之后收获还是很大的.... 一.Fully-Connected Neural Nets 首先是对之前的神经网络的程序进行重构,目的是可以构建任意大小的全连接的neura ...