2023年2月

在设计界面的时候,不管是在Web端,还是在Winform端,或者是WPF或者移动界面等应用上,我们对界面的组织布局,一直是比较有趣的话题,而组织界面的好坏从用户的感受来看,可以提供程序可使用性高低,也体现了开发者的开发界面的功力。良好的界面布局,可以使得我们组织内容更加合理,也更加美观。本篇随笔主要罗列我自己在开发Winform界面的时候,对界面的组织布局的一些小小案例和心得。

界面的组织布局,我这里分为了三个部分,一个是主界面组织布局、列表界面组织布局、新增编辑界面组织布局。

1、主界面组织布局

主界面的组织布局,主要就是应用多窗体的布局方式,一般可以考虑使用多文档容器界面,这种也是常规的方式;或者可以考虑基于用户控件的变化实现同一个面板显示不同的控件内容。

如早期的Winform,我使用默认样式的Winform界面,布局采用控件"WeifenLuo.WinFormsUI.Docking"来承载窗体界面内容,如下所示。

鼠标单击可查看大图

前者也主要是使用多文档的方式来处理不同窗体的界面显示,随着时间的推移,后来逐渐引入了DevExpress的界面套件的方式来做界面,这个界面控件可以实现更加丰富的界面效果,而以界面代码生成工具(代码生成工具Database2Sharp)生成列表和编辑界面后,调整更加方便,因此主要的Winform界面前端就是使用DevExpress的方式来处理,使用DevExpress可以使用Ribbon窗体,也能够很好的支持多文档的界面,因此主界面的效果还是更胜一筹。

会员管理系统

这些主界面,后来基本上都是沿袭一致的界面呈现方式,使用了RibbonForm和树列表等展示内容,列表界面部分可以使用分页控件统一呈现,实现比较统一的界面效果。

2、列表界面组织布局

上面的内容,我们主要介绍了主界面的组织方式,一般主界面是多文档界面,因此列表界面也会最终展示在主界面框架上的。一般来说,我们展示常规内容的时候,分为列表内容和明细显示界面,也就是列表内容分页展示数据库里面的记录信息,而我们查看明细界面内容的时候,就是显示单条记录的详细内容。

如对于列表内容的展示,一般展示效果如下所示,主要展示数据库里面的二维表信息。

有时候列表查询的字段不够的时候,我们通过列表的高级查询进行补充这个短板,以便对于表有大量字段的时候的,对一些不常见的字段进行高级查询操作。

但是一旦列表内容比较多,我们如果需要定位不同的筛选信息的时候,如果仅仅通过一些查询字段的查询,也不是那么友好。因此可能引入一些树形列表来区分不同的内容。



通过这些列表展示的方式,我们可以进一步细化内容的展示,如权限系统的内容展示,通过对用户组织架构和角色的区分,我们可以快速定位系统用户,也可以通过查询字段进行查询,两相结合更加方便使用。

字典模块内容展示,通过不同字典大类的树形列表展示,可以使得我们过滤字典信息更加方便。

而角色管理者摒弃了传统二维表的方式展示数据,通过树列表控件、表单信息和选项卡信息,更好维护角色数据的处理。

对于客户关系管理系统的客户信息的列表界面,这个界面对客户信息的不同属性进行树形列表展示,可以使得定位客户信息更加精确化,更加友好。

以及对一些特定范畴的数据统一管理,可以结合树结构的控件展示,处理起来更方便合理。

另外,有些情况下,我们需要考虑在列表界面上展示主从表信息,那么也可以利用DevExpress的列表控件来实现效果,如下所示。

3、新增、编辑界面组织布局

除了列表界面,另外一个就是对数据的明细展示界面了,这个界面内容可以更加多元化,如在里面承载各种控件,如文本、日期、图片、下拉列表、树形列表,以及我们自定义的一些控件,如对于附件的通过管理展示控件。

有时候,明细内容还涉及到一些主从表明细的录入,如下所示是主从表明细内容直接录入的案例。

如果明细内容较多,我们可以考虑使用选项卡的方式来维护数据,通过选项卡把不同类别的数据区分管理,如下面客户基本资料就是一个案例。

后者系统用户信息的展示也是基于同样的理念

不过如果对于需要集中展示明细信息外,以及整合相关的记录信息,那么一两个选项卡肯定不够,如下面案例的人员基本信息的展示,除了展示自身的基础信息外,也把一些人员关联的信息一并展示,那么需要分为很多选项卡来容纳这些信息,但是太多又会导致我们的界面拥挤,因此可以在选项卡后面添加一个配置按钮,使得用户可以方便维护自己感兴趣的选项卡内容。

不过如果内容还是很多,我们如果还需要精确的分开不同部分的内容,我们也可以考虑下面一种方案。

如对于胶质瘤的病人资料,我们基础信息这么展示,虽然内容不少,但是还是比较合理。

但是,如果对于信息病理内容很多,如果一股脑的展示在一个内容页面上,肯定比较难看,而且也不好维护数据,如下是不好的案例展示。

合理的方式是引入选项卡或其他区分方式,我这里使用了一种不一样的方式,就是通过结合树列表和用户控件的方式,实现一个选项卡里面,对不同区域内容的展示和维护,这样录入界面和定位不同部分的数据也显得更加方便得体。

下面是对于病人资料内容展示的较为合理的案例。

对于一个病人,不同的属性对应不同的图片附件信息,我们可以统一区分管理,如下所示。

以上就是对于主界面界面、列表界面、明细编辑界面的几种界面案例的分析,我们一般在开发的时候,往往都是先使用代码生成工具(我的代码生成工具Database2Sharp),统一生成标准的列表界面,和明细展示的编辑界面,然后在对它进行一定的润色和修改,如调整字典下拉列表类型,增加一些树形列表,调整不同的选项卡展示内容,增加附件管理控件,增加一些特殊的界面效果,只有在一些特殊的情况下,我们需要彻底重新构建明细界面展示。

以上所有案例都是我在Winform开发的时候,逐步形成一些经验或者布局改变的思路,当然有些也是一而再,再而三的优化界面,从而达到更好的显示和处理效果,希望大家喜欢,也欢迎多提意见。

ABP
是ASP.NET Boilerplate的简称,ABP是一个
开源
且文档友好的
应用程序框架
。ABP不仅仅是一个框架,它还提供了一个
最徍实践
的基于
领域驱动设计(DDD)
的体系结构模型。学习使用ABP框架也有一段时间了,一直想全面了解下这个框架的整个来龙去脉,并把想把它使用历程整理成一个系列出来,不过一直没有下笔来写这篇文章的开篇,就是希望能够深入了解,再深入了解一些,希望自己能够理解透彻一些,不能误人子弟,也不想和网上千篇一律的翻译官网的内容,官网的英文介绍也已经很详细了,于是我觉得还是以实际使用的过程进行一定的整理会更好。

初次了解ABP框架,对它还是非常惊艳的,它基本上是.NET 领域架构的集大成者,几乎囊括了我们.NET领域排的上名的各种技术应用,而且它本身可以支持.net framework和.net core两种技术流派,对它的介绍也是非常感兴趣。

1)ABP框架的特点

我们来大概了解下ABP框架涉及到的内容。

  • 依赖注入
    ,这个部分使用 Castle windsor (依赖注入容器)来实现依赖注入,这个也是我们经常使用IOC来处理的方式;
  • Repository仓储模式
    ,已实现了Entity Framework、NHibernate、MangoDB、内存数据库等,仓储模式可以快速实现对数据接口的调用;
  • 身份验证与授权管理
    ,可以使用声明特性的方式对用户是否登录,或者接口的权限进行验证,可以通过一个很细粒度的方式,对各个接口的调用权限进行设置;
  • 数据有效性验证
    ,ABP自动对接口的输入参数对象进行非空判断,并且可以根据属性的申请信息对属性的有效性进行校验;
  • 审计日志记录
    ,也就是记录我们对每个接口的调用记录,以及对记录的创建、修改、删除人员进行记录等处理;
  • Unit Of Work工作单元模式
    ,为应用层和仓储层的方法自动实现数据库事务,默认所有应用服务层的接口,都是以工作单元方式运行,即使它们调用了不同的存储对象处理,都是处于一个事务的逻辑里面;
  • 异常处理
    ,ABP框架提供了一整套比较完善的流程处理操作,可以很方便的对异常进行进行记录和传递;
  • 日志记录
    ,我么可以利用Log4Net进行常规的日志记录,方便我们跟踪程序处理信息和错误信息;
  • 多语言/本地化支持
    ,ABP框架对多语言的处理也是比较友好的,提供了对XML、JSON语言信息的配置处理;
  • Auto Mapping自动映射
    ,这个是ABP的很重要的对象隔离概念,通过使用AutoMaper来实现域对象和DTO对象的属性映射,可以隔离两者的逻辑关系,但是又能轻松实现属性信息的赋值;
  • 动态Web API层
    ,利用这个动态处理,可以把Application Service 直接发布为Web API层,而不需要在累赘的为每个业务对象手工创建一个Web API的控制器,非常方便;
  • 动态JavaScript的AJax代理处理
    ,可以自动创建Javascript 的代理层来更方便使用Web Api,这个在Web层使用。

除了这些重要特性外,ABP框架还有很多一些特别的功能或者概念。

  • 多租户支持(每个租户的数据自动隔离,业务模块开发者不需要在保存和查询数据时写相应代码;
  • 软删除支持(继承相应的基类或实现相应接口,会自动实现软删除)
  • 系统设置存取管理(系统级、租户级、用户级,作用范围自动管理)
  • EventBus实现领域事件(Domain Events)
  • 模块以及模块的依赖关系实现插件化的模块处理等等

ABP框架主要还是基于领域驱动的理念来构建整个架构的,其中领域驱动包含的概念有 域对象Entities、仓储对象Repositories、域服务接口层Domain Services、域事件Domain Events、应用服务接口Application Services、数据传输对象DTOs等。一般简化来说,我们可以只需要保留域对象,标准仓储对象(不用自定义仓储接口)、应用服务接口和DTO对象即可,域服务层接口层和自定义的仓储对象一般情况下可以省略,后面我会介绍这个内容,也就是利用这些对象及关系,快速构建一个易于使用的ABP框架分层。

ABP官方网站:http://www.aspnetboilerplate.com,从里面可以查看很详细的案例和文档说明,可以根据需要下载不同类型的基础框架。

ABP GitHub源码地址:https://github.com/aspnetboilerplate,可以下载整个基础的框架内容,以及相关的样板案例代码。

下面是一个比较直观的ABP框架分层架构图。

上图只是一个大概的介绍,其实客户端部分,还应该包括Winform客户端、控制台客户端、WPF客户端等内容,而浏览器的前端-Web前端,还可以包含使用Ant-Design(React)、IView(VUE)、Angular等不同的前端技术来承载界面呈现层。而底层的数据库支持,还可以接入更多的,包括MS Sqlserver、Oracle、Mysql、PostgreSQL、SQLite等数据库。

我们可以看到展现层、应用层、领域层、持久化层等几个不同的分层,每个分层似乎都很好,但是可能需要落实到实处进行进一步的了解,由于目前.net core的技术应用逐渐走向主流,我们就以它的.net core方向进行介绍解读。

2) Web API优先的架构

纵观整个ABP框架,它的核心还是主要以 .NET 的后端技术为主线,也是着重笔墨的部分,在其展现层中,虽然Asp.NET MVC(包括.net Core部分)和Web API作为两个部分,但它的动态发布Web API,更为Web API优先的架构提供了很好的便利。

在当今流行的展现层中,越来越不依赖于后端的技术实现,而侧重于Web API标准化的对接,基于JSON数据的交互处理。不管是以Ant-Design(React)、IView(VUE)、Angular等技术应用的Web前端,我们可以看到这些架构很容易实现对Web API的标准接口对接,在我较早提供的Winform混合框架里面,也是以Web API优先的策略进行云端应用的部署。如下图是我在博客《
Web API应用架构设计分析(1)
》、《
Web API应用架构设计分析(2)
》、《
Web API接口设计经验总结
》、《
Winform混合式开发框架访问Web API接口的处理
》、《
Web API应用架构在Winform混合框架中的应用(3)--Winform界面调用WebAPI的过程分解
》等文章中的阐述。

作为ABP框架的核心、Web API动态发布,为其展现层提供了非常方便的途径,使得我们可以在利用其强大的后端架构的基础上,整合了很多.NET的很多技术应用,如前面介绍的很多ABP框架的特性。

前面介绍了基于Web API优先应用的特点,可以为我们产品线的快速扩展提供了很好的技术支撑,而ABP框架是一个比较强大、健壮,而且是集众多.NET优秀技术应用的集大成者,虽然整合使用ABP框架会比较一般的框架需要花费多一些时间,不过在构建比较大型,又需要强大的后台的需求下,这种应用场景是非常不错的,也是一个很好的投资。

3)ABP 框架的项目结构

ABP框架,包含了两个部分,一个基础的ABP框架实现(地址
https://github.com/aspnetboilerplate/aspnetboilerplate
),这个是我们所说的ABP框架的核心实现;

一个是基于这个基础上扩展应用的ABP框架,它整合了框架核心部分,并提供了一些基础处理模块,如人员、角色、权限、会话、身份验证、多租户、日志记录等等内容,我们一般指的ABP框架应用就是这个基础上扩展自己的业务项目。这个部分,我们可以根据官网上进行一定的选项配置,然后下载使用。

下载.net core 项目后,其中后端部分的项目视图如下所示。

我们从这个项目里面可以看到,它主要是分为下面几个项目分层。

Application应用层
:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。

Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。

EntityFrameworkCore
实体框架核心层,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。

Migrator数据迁移层
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。

Web.Core Web核心层
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。

Web.Core.Host Web API的宿主层
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。

Tests 单元测试层
,这个提供了一些应用层对象的模拟测试,其中测试的数据库使用的是Entity Framework 的内存数据库,不影响实际数据库内容。

以上是ABP框架的总体情况,我们到现在还没有正式深入介绍其中的各个部分,以及如果对这些内容进行优化处理,主要就是介绍一个整体性的ABP框架特性,以及ABP框架侧重的Web API方向,后续我继续对它进行深入的介绍和项目改造,以便适应我们实际的ABP项目开发。

在前面随笔《
ABP开发框架前后端开发系列---(1)框架的总体介绍
》大概介绍了这个ABP框架的主要特点,以及介绍了我对这框架的Web API应用优先的一些看法,本篇继续探讨ABP框架的初步使用,也就是我们下载到的ABP框架项目(基于ABP基础项目的扩展项目),如果理解各个组件模块,以及如何使用。

1)ABP框架应用项目的介绍

整个基础的ABP框架看似非常庞大,其实很多项目也很少内容,主要是独立封装不同的组件进行使用,如Automaper、SignalR、MongoDB、Quartz。。。等等内容,基本上我们主要关注的内容就是Abp这个主要的项目里面,其他的是针对不同的组件应用做的封装。

而基于基础ABP框架扩展出来的ABP应用项目,则简单很多,我们也是在需要用到不同组件的时候,才考虑引入对应的基础模块进行使用,一般来说,主要还是基于仓储管理实现基于数据库的应用,因此我们主要对微软的实体框架的相关内容了解清楚即可。

这个项目是一个除了包含基础的人员、角色、权限、认证、配置信息的基础项目外,而如果你从这里开始,对于其中的一些继承关系的了解,会增加很多困难,因为它们基础的用户、角色等对象关系实在是很复杂。

我建议从一个简单的项目开始,也就是基于一两个特定的应用表开始的项目,因此可以参考案例项目:
eventcloud
或者
sample-blog-module
项目,我们入门理解起来可能更加清楚。这里我以
eventcloud
项目来进行分析项目中各个层的类之间的关系。

我们先从一个关系图来了解下框架下的领域驱动模块中的各个类之间的关系。

先以领域层,也就是项目中的EventCloud.Core里面的内容进行分析。

2)领域对象层的代码分析

首先,我们需要了解领域对象和数据库之间的关系的类,也就是领域实体信息,这个类非常关键,它是构建仓储模式和数据库表之间的关系的。

    [Table("AppEvents")]public class Event : FullAuditedEntity<Guid>, IMustHaveTenant
{
public virtual int TenantId { get; set; }

[Required]
[StringLength(MaxTitleLength)]
public virtual string Title { get; protected set; }

[StringLength(MaxDescriptionLength)]
public virtual string Description { get; protected set; }public virtual DateTime Date { get; protected set; }public virtual bool IsCancelled { get; protected set; }

......
}

这个里面定义了领域实体和表名之间的关系,其他属性也就是对应数据库的字段了

[Table("AppEvents")]

然后在EventCloud.EntityFrameworkCore项目里面,加入这个表的DbSet对象,如下代码所示。

namespaceEventCloud.EntityFrameworkCore
{
public class EventCloudDbContext : AbpZeroDbContext<Tenant, Role, User, EventCloudDbContext>{public virtual DbSet<Event> Events { get; set; }public virtual DbSet<EventRegistration> EventRegistrations { get; set; }public EventCloudDbContext(DbContextOptions<EventCloudDbContext>options)
:
base(options)
{
}
}
}

简单的话,仓储模式就可以跑起来了,我们利用 IRepository<Event, Guid> 接口就可以获取对应表的很多处理接口,包括增删改查、分页等等接口,不过为了进行业务逻辑的隔离,我们引入了Application Service应用层,同时也引入了DTO(数据传输对象)的概念,以便向应用层隐藏我们的领域对象信息,实现更加弹性化的处理。一般和领域对象对应的DTO对象定义如下所示。

    [AutoMapFrom(typeof(Event))]public class EventListDto : FullAuditedEntityDto<Guid>{public string Title { get; set; }public string Description { get; set; }public DateTime Date { get; set; }public bool IsCancelled { get; set; }public virtual int MaxRegistrationCount { get; protected set; }public int RegistrationsCount { get; set; }
}

其中我们需要注意实体类继承自FullAuditedEntityDto<Guid>,它标记这个领域对象会记录创建、修改、删除的标记、时间和人员信息,如果需要深入了解这个部分,可以参考下ABP官网关于领域实体对象的介绍内容(
Entities
)。

通过在类增加标记性的特性处理,我们可以从Event领域对象到EventListDto的对象实现了自动化的映射。这样的定义处理,一般来说没有什么问题,但是如果我们需要把DTO(如EventListDto)隔离和领域对象(如Event)的关系,把DTO单独抽取来方便公用,那么我们可以在应用服务层定义一个领域对象的映射文件来替代这种声明式的映射关系,AutoMaper的映射文件定义如下所示。

    public classEventMapProfile : Profile
{
publicEventMapProfile()
{
CreateMap
<EventListDto, Event>();
CreateMap
<EventDetailOutput, Event>();
CreateMap
<EventRegistrationDto, EventRegistration>();
}
}

这样抽取独立的映射文件,可以为我们单独抽取DTO对象和应用层接口作为一个独立项目提供方便,因为不需要依赖领域实体。如我改造项目的DTO层实例如下所示。

刚才介绍了领域实体和DTO对象的映射关系,就是为了给应用服务层提供数据的承载。

如果领域对象的逻辑处理比较复杂一些,还可以定义一个类似业务逻辑类(类似我们说说的BLL),一般ABP框架里面以Manager结尾的就是这个概念,如对于案例里面,业务逻辑接口和逻辑类定义如下所示,这里注意接口继承自IDomainService接口。

    /// <summary>
    ///Event的业务逻辑类/// </summary>
    public interfaceIEventManager: IDomainService
{
Task
<Event>GetAsync(Guid id);
Task CreateAsync(Event @event);
voidCancel(Event @event);
Task
<EventRegistration>RegisterAsync(Event @event, User user);
Task CancelRegistrationAsync(Event @event, User user);
Task
<IReadOnlyList<User>>GetRegisteredUsersAsync(Event @event);
}

业务逻辑类的实现如下所示。

我们看到这个类的构造函数里面,带入了几个接口对象的参数,这个就是DI,依赖注入的概念,这些通过IOC容易进行构造函数的注入,我们只需要知道,在模块启动后,这些接口都可以使用就可以了,如果需要了解更深入的,可以参考ABP官网对于依赖注入的内容介绍(
Dependency Injection
)。

这样我们对应的Application Service里面,对于Event的应用服务层的类EventAppService ,如下所示。

[AbpAuthorize]public classEventAppService : EventCloudAppServiceBase, IEventAppService
{
private readonlyIEventManager _eventManager;private readonly IRepository<Event, Guid>_eventRepository;publicEventAppService(
IEventManager eventManager,
IRepository
<Event, Guid>eventRepository)
{
_eventManager
=eventManager;
_eventRepository
=eventRepository;
}

......

这里的服务层类提供了两个接口注入,一个是自定义的事件业务对象类,一个是标准的仓储对象。

大多数情况下如果是基于Web API的架构下,如果是基于数据库表的处理,我觉得领域的业务管理类也是不必要的,直接使用仓储的标准对象处理,已经可以满足大多数的需要了,一些逻辑我们可以在Application Service里面实现以下即可。

3)字典模块业务类的简化

我们以字典模块的字典类型表来介绍。

领域业务对象接口层定义如下所示(类似IBLL)

    /// <summary>
    ///领域业务管理接口/// </summary>
    public interfaceIDictTypeManager : IDomainService
{
/// <summary> ///获取所有字典类型的列表集合(Key为名称,Value为ID值)/// </summary> /// <param name="dictTypeId">字典类型ID,为空则返回所有</param> /// <returns></returns> Task<Dictionary<string, string>> GetAllType(stringdictTypeId);

}

领域业务对象管理类(类似BLL)

    /// <summary>
    ///领域业务管理类实现/// </summary>
    public classDictTypeManager : DomainService, IDictTypeManager
{
private readonly IRepository<DictType, string>_dictTypeRepository;public DictTypeManager(IRepository<DictType, string>dictTypeRepository)
{
this._dictTypeRepository =dictTypeRepository;
}
/// <summary> ///获取所有字典类型的列表集合(Key为名称,Value为ID值)/// </summary> /// <param name="dictTypeId">字典类型ID,为空则返回所有</param> /// <returns></returns> public async Task<Dictionary<string, string>> GetAllType(stringdictTypeId)
{
IList
<DictType> list = null;if (!string.IsNullOrWhiteSpace(dictTypeId))
{
list
= await _dictTypeRepository.GetAllListAsync(p => p.PID ==dictTypeId);
}
else{
list
= await_dictTypeRepository.GetAllListAsync();
}

Dictionary
<string, string> dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Id);
}
}
returndict;
}
}

然后领域对象的应用服务层接口实现如下所示

[AbpAuthorize]public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
private readonlyIDictTypeManager _manager;private readonly IRepository<DictType, string>_repository;publicDictTypeAppService(
IRepository
<DictType, string>repository,
IDictTypeManager manager) :
base(repository)
{
_repository
=repository;
_manager
=manager;
}
/// <summary> ///获取所有字典类型的列表集合(Key为名称,Value为ID值)/// </summary> /// <returns></returns> public async Task<Dictionary<string, string>> GetAllType(stringdictTypeId)
{
var result = await _manager.GetAllType(dictTypeId);returnresult;
}
......

这样就在应用服务层里面,就整合了业务逻辑类的处理,不过这样的做法,对于常规数据库的处理来说,显得有点累赘,还需要多定义一个业务对象接口和一个业务对象实现,同时在应用层接口里面,也需要多增加一个接口参数,总体感觉有点多余,因此我把它改为使用标准的仓储对象来处理就可以达到同样的目的了。

在项目其中对应位置,删除字典类型的一个业务对象接口和一个业务对象实现,改为标准仓储对象的接口处理,相当于把业务逻辑里面的代码提出来放在服务层而已,那么在应用服务层的处理代码如下所示。

[AbpAuthorize]public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, PagedResultRequestDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
private readonly IRepository<DictType, string>_repository;publicDictTypeAppService(
IRepository
<DictType, string> repository) : base(repository)
{
_repository
=repository;
}
/// <summary> ///获取所有字典类型的列表集合(Key为名称,Value为ID值)/// </summary> /// <returns></returns> public async Task<Dictionary<string, string>> GetAllType(stringdictTypeId)
{
IList
<DictType> list = null;if (!string.IsNullOrWhiteSpace(dictTypeId))
{
list
= await Repository.GetAllListAsync(p => p.PID ==dictTypeId);
}
else{
list
= awaitRepository.GetAllListAsync();
}

Dictionary
<string, string> dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Id);
}
}
returndict;
}

......

这样我们少定义两个文件,以及减少协调业务类的代码,代码更加简洁和容易理解,反正最终实现都是基于仓储对象的接口调用。

另外,我们继续了解项目,知道在Web.Host项目是我们Web API层启动,且动态构建Web API层的服务层。它整合了Swagger对接口的测试使用。

            //Swagger - Enable this line and the related lines in Configure method to enable swagger UI
            services.AddSwaggerGen(options =>{
options.SwaggerDoc(
"v1", new Info { Title = "MyProject API", Version = "v1"});
options.DocInclusionPredicate((docName, description)
=> true);//Define the BearerAuth scheme that's in use options.AddSecurityDefinition("bearerAuth", newApiKeyScheme()
{
Description
= "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name
= "Authorization",
In
= "header",
Type
= "apiKey"});//Assign scope requirements to operations based on AuthorizeAttribute options.OperationFilter<SecurityRequirementsOperationFilter>();
});

启动项目,我们可以看到Swagger的管理界面如下所示。

在前面随笔《
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(stringdictTypeId);/// <summary>
        ///获取字典类型一级列表及其下面的内容/// </summary>
        /// <param name="pid">如果指定PID,那么找它下面的记录,否则获取所有</param>
        /// <returns></returns>
        Task<IList<DictTypeNodeDto>> GetTree(stringpid);
}

从上面的接口代码,我们可以看到,字典类型的接口基类是基于异步CRUD操作的基类接口IAsyncCrudAppService,这个是在ABP核心项目的Abp.ZeroCore项目里面,使用它需要引入对应的项目依赖

而基于IAsyncCrudAppService的接口定义,我们往往还需要多定义几个DTO对象,如创建对象、更新对象、删除对象、分页对象等等。

如字典类型的创建对象DTO类定义如下所示,由于操作内容没有太多差异,我们可以简单的继承自DictTypeDto即可。

    /// <summary>
    ///字典类型创建对象/// </summary>
    public classCreateDictTypeDto : DictTypeDto
{
}

IAsyncCrudAppService定义了几个通用的创建、更新、删除、获取单个对象和获取所有对象列表的接口,接口定义如下所示。

namespaceAbp.Application.Services
{
public interface IAsyncCrudAppService<TEntityDto, TPrimaryKey, in TGetAllInput, in TCreateInput, in TUpdateInput, in TGetInput, in TDeleteInput>: IApplicationService, ITransientDependencywhere 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 classCreateDictDataDto : 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(stringdictTypeId);/// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictType">字典类型名称</param>
        /// <returns></returns>
        Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName);
}

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(stringdictTypeId)
{
IList
<DictType> list = null;if (!string.IsNullOrWhiteSpace(dictTypeId))
{
list
= await Repository.GetAllListAsync(p => p.PID ==dictTypeId);
}
else{
list
= awaitRepository.GetAllListAsync();
}

Dictionary
<string, string> dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Id);
}
}
returndict;
}
/// <summary> ///获取字典类型一级列表及其下面的内容/// </summary> /// <param name="pid">如果指定PID,那么找它下面的记录,否则获取所有</param> /// <returns></returns> public async Task<IList<DictTypeNodeDto>> GetTree(stringpid)
{
//确保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 intopList)
{
var subList = Repository.GetAllList(s => s.PID == dto.Id).MapTo<List<DictTypeNodeDto>>();if (subList != null && subList.Count > 0)
{
dto.Children.AddRange(subList);
}
}
return awaitTask.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应用框架的项目组织情况,以及项目中领域层各个类代码组织,以及简化了ABP框架的各个层的内容,使得我们项目结构更加清晰。上篇随笔已经介绍了字典模块中应用服务层接口的实现情况,并且通过运行Web API的宿主程序,可以在界面上进行接口测试了,本篇随笔基于前面介绍的基础上,介绍Web API调用类的封装和使用,使用包括控制台和Winform中对调用封装类的使用。

在上篇随笔《
ABP开发框架前后端开发系列---(3)框架的分层和文件组织
》中我绘制了改进后的ABP框架的架构图示,如下图所示。

这个项目分层里面的 03-Application.Common 应用服务通用层,我们主要放置在各个模块里面公用的DTO和应用服务接口类。有了这些DTO文件和接口类,我们就不用在客户端(如Winform客户、控制台、WPF/UWP等)重复编写这部分的内容,直接使用即可。

这些DTO文件和接口类文件,我们的主要用途是用来封装客户端调用Web API的调用类,使得我们在界面使用的时候,调用更加方便。

1)Web API调用类封装

为了更方便在控制台客户端、Winform客户端等场景下调用Web API的功能,我们需要对应用服务层抛出的Web API接口进行封装,然后结合DTO类实现一个标准的接口实现。

由于这些调用类可能在多个客户端中进行共享,因此根据我们在混合框架中积累的经验,我们把它们独立为一个项目进行管理,如下项目视图所示。

其中DictDataApiCaller 就是对应领域对象 <领域对象>ApiCaller的命名规则。

如对于字典模块的API封装类,它们继承一个相同的基类,然后实现特殊的自定义接口即可,这样可以减少常规的Create、Get、GetAll、Update、Delete等操作的代码,这些全部由调用基类进行处理,而只需要实现自定义的接口调用即可。如下是字典模块DictType和DictData两个业务对象的API封装关系。

如对于字典类型的API封装类定义代码如下所示。

    /// <summary>
    ///字典类型对象的Web API调用处理/// </summary>
    public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticDictTypeApiCaller Instance
{
get{return Singleton<DictTypeApiCaller>.Instance;
}
}

......

这里我们可以通过单件的方式来使用字典类型API的封装类实例 DictTypeApiCaller.Instance

对于Web API的调用,我们知道,一般需要使用WebClient或者HttpRequest的底层类进行Url的访问处理,通过提供相应的数据,获取对应的返回结果。

而对于操作方法的类型,是使用POST、GET、INPUT、DELETE的不同,需要看具体的接口,我们可以通过Swagger UI 呈现出来的进行处理即可,如下所示的动作类型。

如果处理动作不匹配,如本来是Post的用Get方法,或者是Delete的用Post方法,都会出错。

在Abp.Web.Api项目里面有一个AbpWebApiClient的封装方法,里面实现了POST方法,可以参考来做对应的WebClient的封装调用。

我在它的基础上扩展了实现方法,包括了Get、Put、Delete方法的调用。

我们使用的时候,初始化它就可以了。

apiClient = new AbpWebApiClient();

例如,我们对于常规的用户登录处理,它的API调用封装的操作代码如下所示,这个是一个POST方法。

        /// <summary>
        ///对用户身份进行认证/// </summary>
        /// <param name="username">用户名</param>
        /// <param name="password">用户密码</param>
        /// <returns></returns>
        public async virtual Task<AuthenticateResult> Authenticate(string username, stringpassword)
{
var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);var input = new{
UsernameOrEmailAddress
=username,
Password
=password
};
var result = await apiClient.PostAsync<AuthenticateResult>(url, input);returnresult;
}

对于业务接口来说,我们都是基于约定的规则来命名接口名称和地址的,如对于GetAll这个方法来说,字典类型的地址如下所示。

/api/services/app/DictData/GetAll

另外还包括服务器的基础地址,从而构建一个完整的调用地址如下所示。

http://localhost:21021/api/services/app/DictData/GetAll

由于这些规则确定,因此我们可以通过动态构建这个API地址即可。

            string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数)
            url += string.Format("?SkipCount={0}&MaxResultCount={1}", dto.SkipCount, dto.MaxResultCount);

而对于GetAll函数来说,这个定义如下所示。

Task<PagedResultDto<TEntityDto>> GetAll(TGetAllInput input)

它是需要根据一定的条件进行查询的,不仅仅是 SkipCount 和 MaxResultCount两个属性,因此我们需要动态组合它的url参数,因此建立一个辅助类来动态构建这些输入参数地址。

        /// <summary>
        ///获取所有对象列表/// </summary>
        /// <param name="input">获取所有条件</param>
        /// <returns></returns>
        public async virtual Task<PagedResultDto<TEntityDto>>GetAll(TGetAllInput input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url =GetUrlParam(input, url);var result = await apiClient.GetAsync<PagedResultDto<TEntityDto>>(url);returnresult;
}

这样我们这个API的调用封装类的基类就实现了常规的功能了。效果如下所示。

而字典类型的API封装类,我们只需要实现特定的自定义接口即可,省却我们很多的工作量。

namespaceMyProject.Caller
{
/// <summary> ///字典类型对象的Web API调用处理/// </summary> public class DictTypeApiCaller : AsyncCrudApiCaller<DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticDictTypeApiCaller Instance
{
get{return Singleton<DictTypeApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicDictTypeApiCaller()
{
this.DomainName = "DictType";//指定域对象名称,用于组装接口地址 }public async Task<Dictionary<string, string>> GetAllType(stringdictTypeId)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url += string.Format("?dictTypeId={0}", dictTypeId);var result = await apiClient.GetAsync<Dictionary<string, string>>(url);returnresult;
}
public async Task<IList<DictTypeNodeDto>> GetTree(stringpid)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url += string.Format("?pid={0}", pid);var result = await apiClient.GetAsync<IList<DictTypeNodeDto>>(url);returnresult;
}
}
}

2)API封装类的调用

前面小节介绍了针对Web API接口的封装,以适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

到这里为止,我们还没有测试过具体的调用,还没有了解实际调用过程中是否有问题,当然我们在开发的时候,一般都是一步步来的,但也是确保整个路线没有问题的。

实际情况如何,是骡是马拉出来溜溜就知道了。

首先我们创建一个基于.net Core的控制台程序,项目情况如下所示。

在其中我们定义这个项目的模块信息,它是依赖于APICaller层的模块。

namespaceRemoteApiConsoleApp
{
[DependsOn(
typeof(CallerModule))]public classMyModule : AbpModule
{
public override voidInitialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
}

在ABP里面,模块是通过一定顺序启动的,如果我们通过AbpBootstrapper类来启动相关的模块,启动模块的代码如下所示。

//使用AbpBootstrapper创建类来处理
using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
{
bootstrapper.Initialize();

..........

模块启动后,系统的IOC容器会为我们注册好相关的接口对象,那么调用API封装类的代码如下所示。

                //使用AbpBootstrapper创建类来处理
                using (var bootstrapper = AbpBootstrapper.Create<MyModule>())
{
bootstrapper.Initialize();
#region Role using (var client = bootstrapper.IocManager.ResolveAsDisposable<RoleApiCaller>())
{
var caller =client.Object;

Console.WriteLine(
"Logging in with TOKEN based auth...");var token = caller.Authenticate("admin", "123qwe").Result;
Console.WriteLine(token.ToJson());

caller.RequestHeaders.Add(
new NameValue("Authorization", "Bearer" +token.AccessToken));

Console.WriteLine(
"Getting roles...");var pagerDto = new PagedResultRequestDto() { SkipCount = 0, MaxResultCount = 10};var result =caller.GetAll(pagerDto);
Console.WriteLine(result.ToJson());

Console.WriteLine(
"Create role...");
List
<string> permission = new List<string>() { "Pages.Roles"};var createRoleDto = new CreateRoleDto { DisplayName = "test", Name = "Test", Description = "test", Permissions =permission };var roleDto =caller.Create(createRoleDto).Result;
Console.WriteLine(roleDto.ToJson());
var singleDto = new EntityDto<int>() { Id =roleDto.Id };
Console.WriteLine(
"Getting role by id...");
roleDto
=caller.Get(singleDto).Result;
Console.WriteLine(roleDto);

Console.WriteLine(
"Delete role...");var delResult =caller.Delete(singleDto);
Console.WriteLine(delResult.ToJson());

Console.ReadLine();
}
#endregion

上面是对角色的相关接口操作,如果对于我们之前创建的字典模块,那么它的操作代码类似,如下所示。

    #region DictType

    using (var client = bootstrapper.IocManager.ResolveAsDisposable<DictTypeApiCaller>())
{
var caller =client.Object;

Console.WriteLine(
"Logging in with TOKEN based auth...");var token = caller.Authenticate("admin", "123qwe").Result;
Console.WriteLine(token.ToJson());

caller.RequestHeaders.Add(
new NameValue("Authorization", "Bearer" +token.AccessToken));

Console.WriteLine(
"Get All ...");var pagerDto = new DictTypePagedDto() { SkipCount = 0, MaxResultCount = 10};var result =caller.GetAll(pagerDto).Result;
Console.WriteLine(result.ToJson());

Console.WriteLine(
"Get All by condition ...");var pagerdictDto = new DictTypePagedDto() { Name = "民族"};
result
=caller.GetAll(pagerdictDto).Result;
Console.WriteLine(result.ToJson());

Console.WriteLine(
"Get count by condition ...");
pagerdictDto
= newDictTypePagedDto() {};var count =caller.Count(pagerdictDto).Result;
Console.WriteLine(count);
Console.WriteLine();

Console.WriteLine(
"Create DictType...");var createDto = new CreateDictTypeDto { Id = Guid.NewGuid().ToString(), Name = "Test", Code = "Test"};var dictDto =caller.Create(createDto).Result;
Console.WriteLine(dictDto.ToJson());

Console.WriteLine(
"Update DictType...");
dictDto.Code
= "testcode";var updateDto =caller.Update(dictDto).Result;
Console.WriteLine(updateDto.ToJson());
if (updateDto != null)
{
Console.WriteLine(
"Delete DictType...");
caller.Delete(
new EntityDto<string>() { Id =dictDto.Id });
}

}
#endregion

测试字典模块的处理,执行效果如下所示。

删除内容,我们是配置为软删除的,因此可以通过数据库记录查看是否标记为删除了。

同时,我们可以看到审计日志里面,有对相关应用层接口的调用记录。

以上就是.net core控制台程序中对于API封装接口的调用,上面代码如果需要在.net framework里面跑,也是一样的,我同样也做了一个基于.net framework控制台程序,代码调用都差不多的,它的ApiCaller我们做成了 .net standard程序类库的,因此都是通用的。

前面我们提到,我们的APICaller的类,设计了单件的实例调用,因此我们调用起来更加方便,除了上面使用ABP的启动模块的方式调用外,我们可以用传统的方式进行调用,也就是创建一个ApiCaller的实例对象的方式进行调用,如下代码所示。

    string loginName = this.txtUserName.Text.Trim();string password = this.txtPassword.Text;
AuthenticateResult result
= null;try{
result
= awaitDictTypeApiCaller.Instance.Authenticate(loginName, password);
}
catch(AbpException ex)
{
MessageDxUtil.ShowTips(
"用户帐号密码不正确。\r\n错误信息:" +ex.Message);return;
}

由于篇幅的原因,基于winform界面模块的调用,我在后面随笔在另起一篇随笔进行介绍吧,毕竟那是毕竟漂亮的字典模块呈现了。