欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-04-08 23:02:29
在前面随笔《ABP开发框架前后端开发系列---(2)框架的初步介绍》中,我介绍了ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以便基于数据库应用的简化处理。本篇随笔进一步对ABP框架原有基础项目进行一定的改进,减少领域业务层的处理,同时抽离领域对象的AutoMapper标记并使用配置... ......

在前面随笔《abp开发框架前后端开发系列---(2)框架的初步介绍》中,我介绍了abp应用框架的项目组织情况,以及项目中领域层各个类代码组织,以便基于数据库应用的简化处理。本篇随笔进一步对abp框架原有基础项目进行一定的改进,减少领域业务层的处理,同时抽离领域对象的automapper标记并使用配置文件代替,剥离应用服务层的dto和接口定义,以便我们使用更加方便和简化,为后续使用代码生成工具结合相应分层代码的快速生成做一个铺垫。

1)abp项目的改进结构

abp官网文档里面,对自定义仓储类是不推荐的(除非找到合适的借口需要做),同时对领域对象的业务管理类,也是持保留态度,认为如果只有一个应用入口的情况(我主要考虑web api优先),因此领域业务对象也可以不用自定义,因此我们整个abp应用框架的思路就很清晰了,同时使用标准的仓储类,基本上可以解决绝大多数的数据操作。减少自定义业务管理类的目的是降低复杂度,同时我们把dto对象和领域对象的映射关系抽离到应有服务层的automapper的profile文件中定义,这样可以简化dto不依赖领域对象,因此dto和应用服务层的接口可以共享给类似winform、uwp/wpf、控制台程序等使用,避免重复定义,这点类似我们传统的entity层。这里我强调一点,这样改进abp框架,并没有改变整个abp应用框架的分层和调用规则,只是尽可能的简化和保持公用的内容。

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

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

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

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

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

 

2)项目分层的代码

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

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

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

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

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

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

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

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

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

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

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

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

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

由于篇幅的关系,后面在另起篇章介绍如何封装web api的调用类,并在控制台程序和winform程序中对web api接口服务层的调用,以后还会考虑在ant-design(react)和iview(vue)里面进行web界面的封装调用。

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