2023年2月

在我较早的时候,就开始研究和介绍ABP框架,ABP框架相对一些其他的框架,它整合了很多.net core的新技术和相关应用场景,虽然最早开始ABP框架是基于.net framework,后来也全部转向拥抱.net core,而另一条线的ABP VNext则是定位从.net core开始的,基础类库以Volo.Abp开始。ABP框架和ABP VNext框架两者在基类和分层上,都很多几乎是一样的,不过ABP VNext框架是后来者,规避了很多前辈ABP框架的一些缺点,最明显的就是更加模块化(弊端就是管理的独立项目暴增),不过对于大项目来说,功能模块的切分也是必要的。ABP VNext是一个内容很丰富的架构体系,因此准备做一个系列介绍ABP VNext相关架构的知识,本篇随笔介绍它的一些框架基础类继承关系。

1、ABP VNext技术概述

ABP VNext框架如果不考虑在微服务上的应用,其集成使用的难度会降低一个层级,不过ABP VNext和ABP框架一样,基础内容都会设计很多内容,如数据库都支持Oracle、SQLServer、MySql、PostgreSQL、SQLite,都有利用Redis作为分布式缓存,使用RabbitMQ作为事件总线的消息处理方式,使用MongoDB的NoSQL类型数据库作为特殊数据的存储服务,使用Quartz/HangFire作为定时任务的处理等。如果考虑引入微服务的话,会更需要了解
IdentityServer
服务,以及了解
Ocelot
库管理网关,使用
Elasticsearch
&
Kibana
来存储和可视化日志 (使用
Serilog
写日志),有时候感觉引入框架并非一件轻松的事情,各种知识点一股脑的涌来

我们开发复杂的系统,也是和建筑工人一样,一步步盖起房子来的,不同的是,有些人一块砖一块砖的盖,有些人采用预构件来构建,我们回到孩童的时候的思路,就是搭建积木的方式。

ABP VNext框架沿袭这种好习惯,把一些都简单化了,做起大项目来就更加方便了,类似搞一个乐高积木项目一样,不过我们约定了每个项目的基础分层部分,这样一来组装就标准化了。

如下面的一个项目,也可以当成它就是一个模块,和一个麻雀一样,五脏俱全,各个项目代表不同的功能,大家都这样做即可。

应用服务层:

Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO)。
Application,包含应用服务实现,依赖于 Domain 包和 Application.Contracts 包。

领域层:
Domain.Shared,包含常量,枚举和其他类型.
Domain 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象,依赖于 Domain.Shared 包.

基础设施层:

EntityFrameworkCore,包含EF的ORM处理,使用仓储模式,实现数据的存储功能。

HTTP 层
HttpApi项目, 为模块开发REST风格的HTTP API。
HttpApi.Client项目,它将应用服务接口实现远程端点的客户端调用,提供的动态代理HTTP C#客户端的功能。

各个层的依赖关系如下图所示。

我们把这些项目组成一个模块,即使这个模块只有一个表的处理功能,也是一个模块,它们构建成一个完整的模块内部生态层。

这样我们在以模块为基础单位,就可以单独开发,统一整合了,如下图所示。

这样,我们以相关的模块组合,以及一些辅助工具,就构成了整套框架的一个生态基础。

针对ABP VNext的前后端完全分离模式,我们给BS的前端,只需要提供API服务,以及接入详细说明即可,而给Winform、WPF、Console等基于.net的终端,则可以利用HTTP层的HttpApi.Client项目的动态客户端方式,避免编写API的客户端代理即可。

我详细参考了ABP VNext的基础框架类,以及一些应用模块项目的代码,它们基本上是提供了很多底层的支持,上层模块的支持,很多是在其商业版中的功能,并没有出现在应用模块中,如我们常见的权限系统的实现,它模块里面只是提供了简单的的角色和用户信息管理(而且很不完善),而我们往往需要扩展开来实现详细的用户、组织机构、角色、岗位、菜单、权限等功能的管理,才能算是一个完整的权限系统,另外还需要封装字典模块、附件管理模块等一些基础模块应用,这些就需要我们自己实现它的功能了。

以权限管理模块为例,它们虽然提供及基础的DTO和领域对象,没有提供完整应用层的实现,作为一个完整的应用系统,肯定不行,需要利用框架进一步实现才可以整合在项目中使用。

2、框架基础类继承关系

前面介绍了,本篇随笔作为系列的开篇,主要想介绍一下ABP VNext框架的一些基础类关系。

ABP VNext和ABP框架的基础类,虽然它们在项目管理上有所不同,不过它们的类关系层次继承关系,几乎没有太多的变化,有些一些层次上的调整而已。因此对于学习ABP或者ABP VNext框架来说,它们很多地方是共通的。

对于Application层来说,它是承接UI和领域层的中间层,因此它接收用户DTO对象,并且这些DTO对象为了和领域层的Entity层有映射关系,我们定义了一些基础类关系来协助它们,以方便DTO和Entity层之间的Mapping映射关系,从而通过约定方式承载系统的基础属性。

如ApplicationService层的相关DTO基类对象定义及继承关系如下图所示,其中右边是它们继承的接口,以及接口需要实现的属性信息。

注:上图ABP和ABP VNext框架,它们的基类定义和关系都是一样的。

而应用层有时候,需要对数据进行分页,并返回列表记录,那么下面的一些基类对象就是它的应用场景,通过定义分页信息和排序信息,可以让应用服务层获得相应的记录过滤,然后返回基于特定DTO对象的泛型列表,如下图所示。

注:上图ABP和ABP VNext框架,它们的基类定义和关系都是一样的。

ABP VNext框架的应用服务层类,提供了相关CRUD操作的基类,虽然我们有时候可以继承顶层ApplicationService进行开发,但是,为了方便,我们往往使用子类继承自CrudAppService,如下图所示。

首先定义相关自定义接口

public interfaceIBookAppService : 
ICrudAppService
< //定义CRUD方法 BookDto, //显示DTO Guid, //实体主键 PagedAndSortedResultRequestDto, //用于分页排序获取列表 CreateUpdateBookDto, //创建对象DTO CreateUpdateBookDto> //更新对象DTO {
}

然后实现该接口即可,如下所示。

public classBookAppService : 
CrudAppService
<Book, BookDto, Guid, PagedAndSortedResultRequestDto,CreateUpdateBookDto, CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid>repository)
:
base(repository)
{
}
}

CrudAppService实现了ICrudAppService接口中声明的所有方法. 然后,你可以添加自己的自定义方法或覆盖和自定义实现.

相对于ABP VNext的应用服务层基类,它们ABP框架的基类有所差异 ,它们分离了同步和异步的基类,不过基本上都使用异步基类居多,继承关系图如下所示。


它的服务层接口定义和接口实现的处理方式和ABP VNext的操作类似,就不再赘述了。

相对于前面介绍DTO层的基类定义,我们在框架的领域层也定义了类似的类和它的继承关系,和DTO一一对应,这样通过AutoMapping 的方式就可以自动处理他们的属性映射了,减少了很多繁琐的代码处理。

领域层的实体类关系和前面DTO关系类似,如下所示。

"聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的。

如果是聚合根,如商品、订单和订单明细的关系场景,就可以应用到,ABP不强制你使用聚合根,实际上你可以使用上面定义的
Entity
类。

它们和领域的实体关系整合起来是一张关系图,如下所示。

这个在基类部分,和ABP框架有所差异,ABP VNext框架中的聚合根增加了扩展属性的接口定义和实现,以及领域事件的处理接口,如下所示。

它们的部分基类代码如下所示

namespaceVolo.Abp.Domain.Entities
{
[Serializable]
public abstract classAggregateRoot: BasicAggregateRoot,
IHasExtraProperties,
IHasConcurrencyStamp
{
public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }

[DisableAuditing]
public virtual string ConcurrencyStamp { get; set; }protectedAggregateRoot()
{
ConcurrencyStamp
= Guid.NewGuid().ToString("N");
ExtraProperties
= newExtraPropertyDictionary();this.SetDefaultsForExtraProperties();
}
public virtual IEnumerable<ValidationResult>Validate(ValidationContext validationContext)
{
returnExtensibleObjectValidator.GetValidationErrors(this,
validationContext
);
}
}

而ABP 框架的聚合根部分,除了基类有所差异,处理一些特殊的信息外,基本上也是类似的。

以上就是ABP VNext的一些基类和关系图,希望能够促进我们了解ABP VNext框架的神秘之处,解开它的面纱。

如果你对ABP框架的知识点有兴趣,可以参考《
ABP框架使用
》,如果对于ABP框架VUE&Element前端开发有兴趣,可以参考《
循序渐进VUE+Element
》部分内容。

ABP VNext框架如果不考虑在微服务上的应用,也就是开发单体应用解决方案,虽然也是模块化开发,但其集成使用的难度会降低一个层级,不过ABP VNext和ABP框架一样,基础内容都会设计很多内容,如数据库都支持Oracle、SQLServer、MySql、PostgreSQL、SQLite,都有利用Redis作为分布式缓存,使用RabbitMQ作为事件总线的消息处理方式,使用MongoDB的NoSQL类型数据库作为特殊数据的存储服务,使用Quartz/HangFire作为定时任务的处理等。如果考虑引入微服务的话,会更需要了解
IdentityServer
服务,以及了解
Ocelot
库管理网关,使用
Elasticsearch
&
Kibana
来存储和可视化日志 (使用
Serilog
写日志),有时候感觉引入框架并非一件轻松的事情,各种知识点一股脑的涌来。

"作为面向服务架构(SOA)的一个变体,微服务是一种将应用程序分解成松散耦合服务的新型架构风格. 通过细粒度的服务和轻量级的协议,微服务提供了更多的模块化,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够开发,部署和扩展各自的服务,实现开发的并行化.它还允许通过连续重构形成单个服务的架构. 基于微服务架构可以实现持续交付和部署."

ABP VNext 框架引入微服务后,就需要使用API网关来,ABP框架可以使用Ocelot来做网关统一处理上游的HTTP请求,并在内部网络上使用内部网关,处理微服务之间的调用,从而把微服务的调用接口统一为一个固定的模式处理。本篇随笔介绍一下网关的基本智知识,以及ABP VNext 框在引入Ocelot来做网关后的架构图场景,介绍一下ABP VNext 微服务的案例的基本情况。

1、网关和认证服务的介绍

API网关是系统暴露在外部的一个访问入口。就像一个公司的门卫承担着寻址、限制进入、安全检查、位置引导、等等功能。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理等等。
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。

Ocelot是一个用.NET Core技术实现并且开源的API网关技术,它的功能包括了:路由、请求聚合、服务发现、认证、鉴权、限流熔断、并内置了负载均衡器、Service Fabric、Butterfly Tracing等的集成。而且这些功能都只需要简单的配置即可完成。

Ocelot首先通过配置将HttpRequest对象保存到一个指定的状态直到它到达用来创建HttpRequestMessage对象并将创建的HttpRequestMessage对象发送到下游服务中的请求构造中间件。通过中间件来发出请求是Ocelot管道中做的最后一件事。它不会再调用下一个中间件。下游服务的响应会存储在每个请求 scoped repository中,并作为一个请求返回到Ocelot管道中。有一个中间件将HttpResponseMessage映射到HttpResponse对象并返回给客户端。

单网关服务示意图如下所示。


API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口。这样就可以明显的简化客户端实现和微服务应用程序之间的沟通方式。

上游和下游描述消息流:所有 消息从上游流动到下游。

网关作为上游会接收所有的客户端请求,并路由到对应的下游服务器进行处理,再将请求结果返回。而这个上下游请求的对应关系也被称之为路由。

我们的下游服务接口都是公开的,没有经过任何的认证,只要知道接口的调用方法,任何人都可以随意调用,因此,很容易就造成信息泄露或者服务被攻击。

正如,我要找Wlling干活之前,我得先到 HR 部门那里登记并且拿到属于我自己的工卡,然后我带着我的工卡去找Wlling,亮出我是公司员工的身份,并且有权利要求他帮我完成一个任务。

IdentityServer4认证服务器有多种认证模式,包括用户密码、客户端等等。客户端需要先想IdentityServer4 请求认证,获得一个token,然后再带着这个token向下游服务发出请求。

ApiResources 为数组类型,表示identityserver管理的所有的下游服务列表。

  • Name: 下游服务名称
  • DisplayName: 下游服务别名

Clients
为数组类型,表示identityserver管理的所有的上游客户端列表

  • ClientId: 客户端id
  • ClientSecret: 客户端对应的密钥
  • GrantType: 该客户端支持的认证模式
  • Scope: 该客户端支持访问的下游服务列表,必须是在
    apiresources
    列表中登记的

当接入ocelot网关时,我们要达到内外互隔的特性,于是就把identityserver服务也托管到ocelot网关中,这样我们就能统一认证和服务请求时的入口。

如ABP案例中的微服务网关【PublicWebSiteGateway.Host】项目中的配置内容,配置服务器上下游的信息如下所示。

  "Routes": [
{
"DownstreamPathTemplate": "/api/productManagement/{everything}","DownstreamScheme": "https","DownstreamHostAndPorts": [
{
"Host": "localhost","Port": 44344}
],
"UpstreamPathTemplate": "/api/productManagement/{everything}","UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post"]
},
{
"DownstreamPathTemplate": "/api/blogging/{everything}","DownstreamScheme": "https","DownstreamHostAndPorts": [
{
"Host": "localhost","Port": 44357}
],
"UpstreamPathTemplate": "/api/blogging/{everything}","UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post"]
}
],
"GlobalConfiguration": {"BaseUrl": "https://localhost:44397"},

多网关服务示意图如下所示,这种模式是针对不同的客户端来实现一个不同的API网关。

ABP VNext 框架里面也采用了多网关的应用,其微服务的整体架构图如下所示。

其中网关包含了后台管理应用网关【BackendAdminAppGateway.Host】,以及公开的应用接入网关【PublicWebSiteGateway.Host】,而内部网关服务【InternalGateway.Host】,则是用于内部微服务之间调用的统一网关解析。

ABP VNext框架中的微服务,有各个模块的微服务组成一个集合,一起为各个应用提供不同的数据处理服务。

2、ABP VNext项目的微服务项目

前面说到,ABP VNext 框架里面也采用了多网关的应用,其中网关包含了后台管理应用网关【BackendAdminAppGateway.Host】,以及公开的应用接入网关【PublicWebSiteGateway.Host】,而内部网关服务【InternalGateway.Host】,则是用于内部微服务之间调用的统一网关解析。

ABP VNext的微服务项目如下所示。

生成的数据库包含两个部分,其中基础数据库包含IdentityServer4所需的基础表,以及用户、角色、租户、日志、组织机构、权限等权限模块的基础表;另外一个部分就是业务模块的数据库了,如下所示。

我们通过AuthServer.Host和ProductService.Host项目,初始化相关的数据库。

最后获得两个初始数据库,包含基础的表信息。

之前随笔也提到过,虽然ABP VNext的官方提供了构建权限系统的相关表信息,但是组织机构、用户、角色业务表和中间表的管理没有在其对应的Identity项目中提供,官方提供的Identity项目如下所示。

这部分完善的应用接口及管理,他们是在ABP VNext商业版中进行开发并提供的,因此我们开发具体的应用所需的权限基础内容,需要自己进行项目模块的扩展,然后完善组织机构、角色、用户、菜单、日志(审计日志、对象修改日志)、权限点的管理和维护等内容。

3、微软的eShopOnContainer微服务架构

eShopOnContainer是基于Docker技术微服务架构demo,由微软架构师利用.net core技术实现并在github上开源,同时发布的还有关于微服务架构的白皮书(
点这里
),微服务架构是一个比较新的架构模式,通读白皮书并结合该demo代码,可以做到按图索骥的作用,对理解.net core技术实现微服务架构可以做到事半功倍。

在Github中的微软eShopOnContainer 项目地址:
https://github.com/dotnet-architecture/eShopOnContainers

eShopOnContainer 的开发架构示意图如下所示。

包含网关的架构架构图如下图所示,其中包含多个网关服务处理客户端的请求。

4、微服务的模块拆分

微服务根据功能或者应用场景进行拆分,如把一个大型复杂的系统应用,拆分为多个微服务应用模块,然后进行整合使用。

或者按下面界限上下文进行划分

不过微服务也不是拆分的越细越好,一般根据实际情况进行度量,引入微服务虽然能够解决一些技术上和性能上的问题,不过拆分过多可能会导致开发和维护上灾难。

在我们常规的调试或者测试的时候,喜欢把一些测试信息打印在控制台或者记录在文件中,对于.netframework项目来说,我们输出控制台的日志信息习惯的用Console.WriteLine来输出查看,不过对于.netcore的项目来说,这种输出看不到任何信息了,不过即使这样,我们建议还是把一些测试的日志记录在文件中,供查看对比。本篇随笔介绍使用Serilog 来记录日志信息,包括应用于.netframework项目和.netcore项目中都适合。

1、在Winform项目中整合Serilog 记录日志

在.NET使用日志框架第一时间会想到NLog或是Log4Net,Serilog 是这几年快速崛起的Log框架之一,Serilog是以Structured logging 为基础进行设计,通过logging API 可以轻松的记录应用程序中对象属性,方便快速进行logging 内容进行查询与分析,并将其纪录内容通过json (可指定) 的方式输出。

不管对于.netframwork或者.netcore的Winform项目来说,使用Serilog 来记录日志信息的操作都很类似,它都支持的。

官网地址:
https://serilog.net/
,或者参考它的Github地址:
https://github.com/serilog/serilog

我们只需要引入对应的几个Serilog 相关DLL即可,如下所示。

然后在Main函数中添加对应的处理代码,如下所示。

    static classProgram
{
/// <summary> ///应用程序的主入口点。/// </summary> [STAThread]static voidMain()
{
Log.Logger
= newLoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
"logs/log.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();

在类代码或者界面代码中,直接通过方法输出日志信息即可。

   //使用Serilog记录日志信息
   Serilog.Log.Information(json);

它和其他Log组件,如Log4net一样,它的使用区分类别来输出不同类型的日志,支持Verbose、Debug、Information、Error、Warning、Fatal 几个接口的输出。

Serilog.Log.Verbose("verbose"); //用于跟踪信息和调试细节,不会输出显示出来//以下为常用记录日志方法
Serilog.Log.Information("info");
Serilog.Log.Debug(
"debug");
Serilog.Log.Warning(
"warning");
Serilog.Log.Error(
"err");
Serilog.Log.Fatal(
"fatal"); 

输出日志信息如下所示。

Serilog 如果想个性化设置日志输出到什么地方,需要引用一些serilog的接收器。Serilog的输出对象称之为Sink

Github上提供了大量的Serilog 第三方的可用
sinks
,这里简单的列举几个常用的:

2、在.NetCore项目的Web API中整合Serilog

在.netcore的Web API项目中配置和上面的有一些差异,引入相关的Serilog组件类库,如下所示。

其中需要在IHostBuilder或者IWebHostBuilder中注入Serilog接口,如下初始化代码所示。

在Web API的控制器中使用代码如下所示

    /// <summary>
    ///用于接收长语音信息回调的处理/// </summary>
[ApiController]
[Route(
"[controller]")]public classLongVoiceController : ControllerBase
{
private readonly ILogger<LongVoiceController>_logger;public LongVoiceController(ILogger<LongVoiceController>logger)
{
_logger
=logger;
}
/// <summary> ///测试控制器,路由:get /longvoice/// </summary> /// <returns></returns> [HttpGet]public List<string>TestList()
{
return new List<string>()
{
"测试列表接口","检查控制器正常"};
}
/// <summary> ///回调写入结果, 路由:post /longvoice/// </summary> /// <param name="input"></param> /// <returns></returns> [HttpPost]public async Task<CommonResultDto>CallBack(LongVoiceResultDto input)
{
CommonResultDto result
= null;var json =JsonConvert.SerializeObject(input, Formatting.Indented);
_logger.LogInformation(json);

result
= new CommonResultDto(0, "操作成功");return awaitTask.FromResult(result);
}
}

我们注意到直接使用web API 的 IIogger<T>接口调用写日志即可。

_logger.LogInformation(json);

这样我们可以看到相关的日志输出

在ABP VNext框架中,即使在它提供的所有案例中,都没有涉及到Winform程序的案例介绍,不过微服务解决方案中提供了一个控制台的程序供了解其IDS4的调用和处理,由于我开发过很多Winform项目,以前基于ABP框架基础上开发的《
ABP快速开发框架
》中就包含了Winform客户端,因此我对于ABP VNext在Winform上的使用也比较关心,花了不少时间来研究框架的相关的授权和窗体构建处理上,因此整理了该随笔内容,主要用于介绍ABP VNext框架中Winform终端的开发和客户端授权信息的处理。

1、ABP VNext框架中Winform终端的开发

不管对于那种终端项目,需要应用ABP VNext模块的,都需要创建一个模块类,继承于AbpModule,然后引入相关的依赖模块,并配置Servcie信息,如下是Winform项目中的Module类,如下所示。

namespaceWinform.TestApp
{
[DependsOn(
typeof(MicroBookStoreHttpApiClientModule),typeof(AbpHttpClientIdentityModelModule)
)]
public classWinformApiClientModule : AbpModule
{
public override voidConfigureServices(ServiceConfigurationContext context)
{
}
}
}

ABP VNext模块的初始化,根据依赖关系进行相关的初始化,我们在创建Winform项目(基于.net Core开发)的时候,需要在Main函数中创建一个应用接口,如下所示。

 //使用 AbpApplicationFactory 创建一个应用
 var app = AbpApplicationFactory.Create<WinformApiClientModule>();//初始化应用
 app.Initialize();

这个app接口对象非常重要,需要用它创建一些接口服务,如下所示。

var service = app.ServiceProvider.GetService<IService1>();

不过由于这个app对象需要在整个应用程序的生命周期中都可能会用到,用来构建一些用到的接口对象等,那么我们就需要创建一个静态类对象用来存储相关的应用接口信息,需要用到它的时候就可以直接使用了,否则丢掉了就没法构建接口使用了。

首先我们创建一个用于存储全局信息类GlobalControl,如下所示。

    /// <summary>
    ///应用程序全局对象/// </summary>
    public classGlobalControl
{
public MainForm? MainDialog { get; set; } = null;public IAbpApplicationWithInternalServiceProvider? app { get; set; }/// <summary> ///创建指定类型窗口实例/// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T CreateForm<T>() whereT :Form
{
if (app == null) return null;else{var form = app.ServiceProvider.GetService<T>();returnform;
}
}
/// <summary> ///创建服务类的接口实例/// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T GetService<T>() where T : class{if (app == null) return null;else{var service = app.ServiceProvider.GetService<T>();returnservice;
}
}

这样我们在Main方法中创建的时候,构建一个静态的类对象,用于存储我们所需要的信息,这样上面提到的应用接口对象,就可以存储起来,

    public static classPortal
{
/// <summary> ///应用程序的全局静态对象/// </summary> public static GlobalControl gc = new GlobalControl();/// <summary> ///The main entry point for the application./// </summary> [STAThread]static voidMain()
{
Log.Logger
= newLoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
"logs/myapp.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
//使用 AbpApplicationFactory 创建一个应用 var app = AbpApplicationFactory.Create<WinformApiClientModule>();//初始化应用 app.Initialize();
gc.app = app;
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(
false);var form = app.ServiceProvider.GetService<MainForm>();
gc.MainDialog
=form;
Application.Run(gc.MainDialog);
}
}

上面标注红色的部分就是把这个重要的app存放起来,便于后期的使用。

而我们注意到,我们创建窗体的时候,不是使用

var form = new MainForm();

的方式构建,而是使用接口构建的方式。

 var form = app.ServiceProvider.GetService<MainForm>();

和我们前面提到的方式构建接口是一样的。

var service = app.ServiceProvider.GetService<IService1>();

这个是为什么呢?因为我们需要通过构造函数注入接口方式,在窗体中引用相关的接口服务。

由于没有默认构造函数,因此不能再通过new的方式构建了,需要使用ABP VNext的常规接口解析的方式获得对应的窗体对象了。

注意:这里窗体需要继承自
ITransientDependency
接口,这样才可以通过接口的方式构建,否则是不行的。

如果我们在主窗体或者其他界面事件中调用其他窗口,也是类似,如下调用所示。

        private void button2_Click(objectsender, EventArgs e)
{
var form2 = Portal.gc.CreateForm<SecondForm>();
form2.ShowDialog();
}

这个地方就是用到了静态对象GlobalControl里面的方法构建,因为里面在程序启动的时候,已经存储了app应用接口对象了,可以用它来构建相关的接口或者窗体对象。

当然,这里的SecondForm也是不能使用New的方式构建窗体对象,也需要使用服务构建的标准方式来处理,毕竟它的默认构造函数用于接口的注入处理了。

程序看起来效果如下所示,可以正常打开窗体了。

2、Winform客户端授权信息的处理

在ABP VNext微服务的解决方案中,有一个控制台调用服务接口的测试项目,如下所示。

它主要就是介绍如何配置IdentityServer4(也叫IDS4)的授权规则来获得动态客户端的接口调用服务的。

它的配置是通过appsettings.json中配置好IdentityServer4终端的节点信息,用来在客户端调用类中进行相关的授权处理(获得令牌)的,因为我们调用服务接口需要令牌信息,而这些都是封装在内部里面的。

appsettings.json的配置信息如下所示,这个IDS4认证是采用client_credentials方式认证的。

而在构建ABP VNext项目模板的时候,也提供了一个类似控制台的测试项目,如下所示。

这个里面的appsettings.json是使用用户名密码的方式进行认证的,授权方式是密码方式。

看到这些信息,你可能注意到了用户名密码都在里面。

我在想,如果每次让用户使用Winform程序的时候,来修改一下这个appsettings.json,那肯定是不友好的,如果把IDS4信息动态构建,传入接口使用,是不是就可以不用配置文件了呢?

通过分析ABP VNExt框架的类库,你可以看到IDS的授权认证处理是在IdentityModelAuthenticationService 接口实现类里面,它通过下面接口获得通信令牌信息。

public async Task<string> GetAccessTokenAsync(IdentityClientConfiguration configuration)

我们传入对应的IDS4的配置对象即可获得接口的令牌信息。

我们通过IIdentityModelAuthenticationService 接口获得令牌信息,缓存起来可以,但是每次调用的时候,如何设定HttpClient的令牌头部信息呢,通过分析 IdentityModelAuthenticationService 类的代码知道,如果我们在appsetting.json配置了IDS4的标准配置,它就可以根据配置信息获得令牌信息的缓存,并设置到调用的HttpClient里面,如果我们采用刚才说的动态配置对象的传入获得token,没有IDS4配置文件信息它是没法提取出令牌缓存信息的。

        public async Task<bool> TryAuthenticateAsync(HttpClient client, string identityClientName = null)
{
var accessToken = awaitGetAccessTokenOrNullAsync(identityClientName);if (accessToken == null)
{
return false;
}

SetAccessToken(client, accessToken);
return true;
}

那有没有其他方式可以动态设定令牌信息或者类似的操作呢?

有!我们注意到,IRemoteServiceHttpClientAuthenticator 接口就是用来解决终端授权处理的接口,它的接口定义如下所示。

namespaceVolo.Abp.Http.Client.Authentication
{
public interfaceIRemoteServiceHttpClientAuthenticator
{
Task Authenticate(RemoteServiceHttpClientAuthenticateContext context);
}
}

我们参考项目Volo.Abp.Http.Client.IdentityModel.Web的思路

这个项目使用了自定义的接口实现类HttpContextIdentityModelRemoteServiceHttpClientAuthenticator,替换默认的IdentityModelRemoteServiceHttpClientAuthenticator类,我们来看看它的具体实现

namespaceVolo.Abp.Http.Client.IdentityModel.Web
{
[Dependency(ReplaceServices
= true)]public classHttpContextIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator
{
public IHttpContextAccessor HttpContextAccessor { get; set; }publicHttpContextIdentityModelRemoteServiceHttpClientAuthenticator(
IIdentityModelAuthenticationService identityModelAuthenticationService)
:
base(identityModelAuthenticationService)
{
}
public override asyncTask Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
if (context.RemoteService.GetUseCurrentAccessToken() != false)
{
var accessToken = awaitGetAccessTokenFromHttpContextOrNullAsync();if (accessToken != null)
{
context.Request.SetBearerToken(accessToken);
return;
}
}
await base.Authenticate(context);
}
protected virtual async Task<string>GetAccessTokenFromHttpContextOrNullAsync()
{
var httpContext = HttpContextAccessor?.HttpContext;if (httpContext == null)
{
return null;
}
return await httpContext.GetTokenAsync("access_token");
}
}
}

这里看到,它主要就是从httpContext中获得access_token的头部信息,然后通过SetBearerToken的接口设置到对应的HttpRequest请求中去的,也就是先获得令牌,然后设置请求对象的令牌,从而完成了授权令牌的信息处理。

我们如果是Winform或者控制台,那么调用请求类是HttpClient,我们可以模仿项目 Volo.Abp.Http.Client.IdentityModel.Web 这个方式创建一个项目,然后通过依赖方式来替换默认授权处理接口的实现;也可以通过在本地项目中创建一个IdentityModelRemoteServiceHttpClientAuthenticator的子类来替换默认的,如下所示。

namespaceWinform.TestApp
{
public classMyIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator
{

在ABP VNext框架类IdentityModelAuthenticationService中获得令牌的时候,就会设置获得的令牌到分布式缓存中,它的键是IdentityClientConfiguration对象的键值生成的,如下代码逻辑所示。

那么我们只需要在自定义的 MyIdentityModelRemoteServiceHttpClientAuthenticator 类中根据键获得缓存就可以设置令牌信息了。

通过上面的处理,我们就可以动态根据账号密码获得令牌,并根据配置信息的键从缓存中获得令牌,设置到对应的对象上去,完成了令牌的信息设置,这样ABP VNext动态客户端的代理接口类,就可以正常调用获得数据了。

数据记录展示如下。

这样,整个测试的例子就完成了多个Winform窗体的生成和调用展示,并通过令牌的处理,完成了客户端的IDS4授权,可以正常调用动态客户端的接口类,完美解决了相关的技术点了。

在ABP VNext框架中,HttpApi项目是我们作为Restful格式的控制器对象的封装项目,但往往很多案例都是简单的继承基类控制器AbpControllerBase,而需要在每个控制器里面重写很多类似的Create/Update/Delete/Get/GetList等常规Restful接口的调用,千篇一律的重复,本篇随笔介绍如何对这些内容通过基类的方式实现,子类无需重复代码,并且强类型所有的接口实现。

1、Restful接口的CRUD实现

在我们使用HttpApi项目进一步封装ABP VNext框架的Application项目中的应用服务,作为Restful格式的控制器对象,往往都需要实现基本的Create/Update/Delete/Get/GetList等常规Restful接口的实现调用,官方很多案例也都是把这部分代码进行重复在重复,如下所示。

例如对于客户对象Customer的HttpApi项目控制器的代码如下:

    /// <summary>
    ///客户信息控制器/// </summary>
    //[Area("crm")]
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController : AbpControllerBase, ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;publicCustomerController(ICustomerAppService customerAppService)
{
_customerAppService
=customerAppService;
}
/// <summary> ///创建对象/// </summary> [HttpPost]public Task<CustomerDto>CreateAsync(CreateCustomerDto input)
{
return_customerAppService.CreateAsync(input);
}
/// <summary> ///删除对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpDelete]
[Route(
"{id}")]public Task DeleteAsync(stringid)
{
return_customerAppService.DeleteAsync(id);
}
/// <summary> ///根据ID获取指定对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet]
[Route(
"{id}")]public Task<CustomerDto> GetAsync(stringid)
{
return_customerAppService.GetAsync(id);
}
/// <summary> ///分页获取列表记录/// </summary> /// <param name="input"></param> /// <returns></returns> [HttpGet]public Task<PagedResultDto<CustomerDto>>GetListAsync(CustomerPagedDto input)
{
return_customerAppService.GetListAsync(input);
}
/// <summary> ///更新对象/// </summary> [HttpPut]
[Route(
"{id}")]public Task<CustomerDto> UpdateAsync(stringid, CustomerDto input)
{
return_customerAppService.UpdateAsync(id, input);
}
/// <summary> ///获取字段列别名/// </summary> /// <returns></returns> [HttpGet]
[Route(
"columnalias")]public Task<Dictionary<string, string>>GetColumnNameAlias()
{
return_customerAppService.GetColumnNameAlias();
}
}

对于其他业务对象,这部分基本上千篇一律的重复一次,就是为了简单的封装一下CRUD的常规接口。

那么我们是否可以考虑通过基类的方式来抽取这部分代码,放到基类里面去实现,以后只需要继承该基类就完事了呢?

考虑到这些Restful的API接口实现,很多都是特定的业务对象,如上面的CustomerDto、CustomerPagedDto 等,那这些就需要通过泛型的方式指定类型给基类了。

而业务接口 ICustomerAppService 也是一个特定的业务接口,也需要传递给基类处理,这样才能进行调用的。

2、HttpApi 基类控制器的实现

我们注意到上面项目的ICustomerService的接口定义如下:

    /// <summary>
    ///客户信息,应用层服务接口定义/// </summary>
    public interfaceICustomerAppService : 
ICrudAppService
<CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>{/////<summary> ///// 获取指定条件的数量/////</summary> /////<param name="input">查找条件</param> /////<returns></returns> //Task<int> CountAsync(CustomerPagedDto input); /// <summary> ///获取字段中文别名(用于界面显示)的字典集合/// </summary> /// <returns></returns> Task<Dictionary<string, string>>GetColumnNameAlias();

}

它是继承自ICrudAppService接口(Abp的基类接口)并传递几个相关的实体类参数作为基类的接口强类型构建的。

那么我们的HttpApi 基类控制器也可以采用这种方式来传递对应的类型,作为基类接口的处理需要。

我们定义一个控制器基类MyAbpControllerBase,让它继承自常规的AbpControllerBase接口,并实现ICrudAppService接口,如下所示。

    /// <summary>
    ///自定义ABP控制器基类,用于实现通用的CRUD等方法/// </summary>
    public abstract class MyAbpControllerBase<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>: AbpControllerBase, 
IMyCrudAppService
<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput> where TEntityDto : IEntityDto<TKey> whereTGetListInput : IPagedAndSortedResultRequestwhere TCreateInput : IEntityDto<TKey> where TUpdateInput : IEntityDto<TKey>{protected IMyCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>_service;public MyAbpControllerBase(IMyCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>service)
{
_service
=service;
}

这样我们就定义好这个基类,并且通过让它传递相关的业务对象和对象外键类型,强类型相关的接口处理,并让它实现了相关的构造函数。

那么对应的接口实现,我们只需要调用 _service 的处理即可。

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[HttpPost]public Task<TEntityDto>CreateAsync(TCreateInput input)
{
return_service.CreateAsync(input);
}
/// <summary> ///删除对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpDelete]
[Route(
"{id}")]publicTask DeleteAsync(TKey id)
{
return_service.DeleteAsync(id);
}
/// <summary> ///获取指定id的记录/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet]
[Route(
"{id}")]public Task<TEntityDto>GetAsync(TKey id)
{
return_service.GetAsync(id);
}
/// <summary> ///获取条件的列表/// </summary> [HttpGet]public Task<PagedResultDto<TEntityDto>>GetListAsync(TGetListInput input)
{
return_service.GetListAsync(input);
}
/// <summary> ///更新对象/// </summary> [HttpPut]
[Route(
"{id}")]public Task<TEntityDto>UpdateAsync(TKey id, TUpdateInput input)
{
return_service.UpdateAsync(id, input);
}

我们还可以自己增加一些特殊的接口和基类的实现,这样我们对于常规的接口就不需要添加重复的实现代码了,只需要继承基类就可以了。

子类继承基类的代码如下所示。

    /// <summary>
    ///客户信息控制器/// </summary>
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController :
MyAbpControllerBase
<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;public CustomerController(ICustomerAppService customerAppService) : base(customerAppService)
{
_customerAppService
=customerAppService;
}
}

这样这个CustomerController默认就具有所有相关的常规接口了,不需要千篇一律的重写那些繁杂的代码,清爽了很多。

而如果我们需要额外增加一些接口的处理,那么在其接口定义增加,并实现即可,如下代码所示。

    /// <summary>
    ///客户信息,应用层服务接口定义/// </summary>
    public interfaceICustomerAppService : 
IMyCrudAppService
<CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>{/// <summary> ///增加的额外测试接口/// </summary> /// <returns></returns> Task<bool>TestExtra();
}

HttpApi项目的实现代码如下所示。

    /// <summary>
    ///客户信息控制器/// </summary>
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController :
MyAbpControllerBase
<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;public CustomerController(ICustomerAppService customerAppService) : base(customerAppService)
{
_customerAppService
=customerAppService;
}
/// <summary> ///测试额外的接口调用/// </summary> /// <returns></returns> [HttpGet]
[Route(
"test-extra")]public async Task<bool>TestExtra()
{
return await_customerAppService.TestExtra();
}
}

启动Swagger的查看接口界面,我们可以看到,Customer控制器所发布的接口信息,如下所示。

一切都是那么的美好,以后再也不用重复书写或看到那些重复的,没有技术含量的代码了。