【ABP】项目示例(15)——静态Api客户端

静态Api客户端

在上一章节中,实现了动态Api客户端功能,在这一章节中,实现静态Api客户端功能
静态Api客户端是ABP框架提供的一种远程HTTP调用机制,它可以根据应用服务依赖层中的接口定义,在开发时通过 ABP CLI生成HTTP代理类,使调用方可以像调用本地服务一样调用远程Api,而无需手动编写HttpClient、拼接URL和处理序列化等底层细节
静态Api客户端与动态Api客户端的比较:

  • 静态Api客户端代理生成是在开发时使用ABP CLI进行生成,动态Api客户端代理是运行时反射生成
  • 静态Api客户端在运行时不需要获取Api定义,动态Api客户端在首次调用时获取Api定义
  • 静态Api客户端代理在Api变更后需要重新生成,动态Api客户端则不需要

总之,静态Api客户端有性能优势,动态Api客户端开发体验更好
静态Api客户端分为两种模式:With Contracts和Without Contracts

  • With Contracts模式的客户端不依赖服务端的Application.Contracts,使用generate-proxy命令生成完整类型(包括接口、DTO、枚举、代理实现类和app-generate-proxy.json文件)
  • Without Contracts模式的客户端依赖服务端的Application.Contracts,使用generate-proxy命令只会生成代理实现类和app-generate-proxy.json文件

接下来分别对这两种模式的静态Api客户端进行实现

With Contracts模式

项目结构设计如下:

  • StaticClientWithContracts项目结构如下(客户端)
    Modules/StaticClientWithContracts/
    ├── General.StaticClient.WithContracts/ // 启动项目(待添加)
    ├── General.StaticClient.WithContracts.HttpApi.Client/ // Http代理客户端(待添加)

  • General.Backend.Web项目结构如下(服务端)
    ├── General.Backend.Application/
    ├── General.Backend.Application.Contracts/
    ├── General.Backend.DbMigrator/
    ├── General.Backend.Domain/
    ├── General.Backend.Domain.Shared/
    ├── General.Backend.EntityFrameworkCore/
    ├── General.Backend.HttpApi.StaticClient.WithContracts/ // Http代理客户端(待添加)
    ├── General.Backend.Web/

服务端

现有的服务端项目中已存在可公开为Restful Api端点的应用服务接口,例如应用服务层中的用户应用服务、角色应用服务、菜单应用服务、认证应用服务等
在服务端项目中,新建名称为General.Backend.HttpApi.StaticClient.WithContracts的类库,添加General.Backend.Domain.Shared项目依赖,因为生成的完整类型中枚举是在该项目中定义的。在程序包管理控制台选中General.Backend.HttpApi.StaticClient.WithContracts,执行以下命令安装ABP静态Api客户端代理和虚拟文件系统相关的Nuget包

Install-Package Volo.Abp.Http.Client -v 8.3.0
Install-Package Volo.Abp.Ddd.Application.Contracts -v 8.3.0
Install-Package Microsoft.Extensions.FileProviders.Embedded -v 8.0.25

新建名称为GeneralHttpApiClientWithContractsModule的模块类,添加AbpHttpClientModule和AbpVirtualFileSystemModule模块依赖,并注册静态Api客户端代理和配置当前项目的嵌入文件到虚拟文件系统中

[DependsOn(typeof(AbpHttpClientModule),typeof(AbpVirtualFileSystemModule)
)]
public class GeneralHttpApiClientWithContractsModule : AbpModule
{public const string RemoteServiceName = "GeneralBackend";public override void ConfigureServices(ServiceConfigurationContext context){context.Services.AddStaticHttpClientProxies(typeof(GeneralHttpApiClientWithContractsModule).Assembly,RemoteServiceName);Configure<AbpVirtualFileSystemOptions>(options =>{options.FileSets.AddEmbedded<GeneralHttpApiClientWithContractsModule>();});}
}

在类库General.Backend.HttpApi.StaticClient.WithContracts中打开终端,执行以下命令生成静态Api客户端的完整类型,http://localhost:5025/ 是服务端项目运行的实际地址,需要提前把服务端运行起来,再执行以下命令

abp generate-proxy -t csharp -u http://localhost:5025/

执行命令后会生成ClientProxies文件夹和app-generate-proxy.json接口定义文件,ClientProxies文件夹中包括了接口、DTO、枚举、代理实现类等文件
在类库General.Backend.HttpApi.StaticClient.WithContracts的项目文件中,开启生成嵌入资源清单,以及将生成的app-generate-proxy.json文件的属性复制到输出目录设置为如果较新则复制和属性生成操作设置为嵌入的资源

<PropertyGroup><GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup><ItemGroup><None Remove="ClientProxies\app-generate-proxy.json" />
</ItemGroup><ItemGroup><EmbeddedResource Include="ClientProxies\app-generate-proxy.json"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></EmbeddedResource>
</ItemGroup>

客户端

新建名称为General.StaticClient.WithContracts.HttpApi.Client的类库,添加General.Backend.HttpApi.StaticClient.WithContracts项目依赖,在程序包管理控制台选中General.StaticClient.WithContracts.HttpApi.Client,执行以下命令安装ABP静态Api客户端代理相关的Nuget包

Install-Package Volo.Abp.Http.Client -v 8.3.0

新建名称为GeneralStaticClientWithContractsModule的模块类,添加AbpHttpClientModule和GeneralHttpApiClientWithContractsModule模块依赖

[DependsOn(typeof(AbpHttpClientModule),typeof(GeneralHttpApiClientWithContractsModule))]
public class GeneralStaticClientWithContractsModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){}
}

WebApi启动项目的实现可参考动态Api客户端 中的实现,只是需要把对General.DynamicClient.HttpApi.Client项目依赖替换成General.StaticClient.WithContracts.HttpApi.Client项目,这里就不多赘述了

远程调用

这里模拟的是对远程服务的用户接口进行调用

[Route("api/[Controller]")]
public class ClientWithContractsController : AbpController
{private readonly IUserAppService _userAppService;public ClientWithContractsController(IUserAppService userAppService){_userAppService = userAppService;}[HttpGet("user/{id}")]public async Task<UserDto> GetUserByIdAsync(Guid id){var user = await _userAppService.GetAsync(id);return user;}[HttpGet("users")]public async Task<List<UserDto>> GetUsersAsync(){var users = await _userAppService.GetListAsync(new UserQueryDto{SortType = "asc",SortField = "Id",PageIndex = 1,PageSize = 10});return users.Items.ToList();}
}

同时把客户端和服务端项目运行起来,在客户端的Swagger页面访问GetUsersAsync方法,进而对服务端GetListAsync方法的调用。但是实际调用时出现了以下的错误
The API description of the General.Backend.Application.IUserAppService.GetListAsync method was not found!

提示General.Backend.Application.IUserAppService.GetListAsync这个接口方法的API描述没有找到
首先我去生成的app-generate-proxy.json接口定义文件中找这个接口方法,发现这个方法的类型定义为General.Backend.Application.Contracts.IUserAppService,与服务端中这个方法的类型定义一致,但是与程序实际调用的不符。客户端调用的接口方法实际是General.Backend.HttpApi.StaticClient.WithContracts类库中通过ABP命令generate-proxy生成的接口方法,生成的IUserAppService接口类的命名空间是General.Backend.Application,与服务端和app-generate-proxy.json接口定义文件对应接口类的命名空间General.Backend.Application.Contracts不一致,所以需要给生成的应用服务接口类的命名空间加上.Contracts
重新运行客户端和服务端项目,调用时又出现了一个错误
The API description of the General.Backend.Application.Contracts.IUserAppService.GetListAsync method was not found!

提示General.Backend.Application.Contracts.IUserAppService.GetListAsync这个接口方法的API描述没有找到
这个错误与先前的错误类似,但是现在两边的应用服务接口类的命名空间已经对上了,然而还是没有找到这个接口方法的API描述。通过查阅ABP8.3.0客户端代理接口API描述生成的源码 发现当app-generate-proxy.json在 ClientProxies/子目录时,根路径""扫描到ClientProxies目录,ClientProxies目录不是PhysicalDirectoryInfo,于是走fileInfo.PhysicalPath!,而嵌入资源的PhysicalPath为null,导致递归子目录失败 ,JSON从未被加载,结果就是找不到接口方法的API描述

private void GetGenerateProxyFileInfos(List<IFileInfo> fileInfoList, string path = "")
{foreach (var directoryContent in VirtualFileProvider.GetDirectoryContents(path)){if (directoryContent.IsDirectory){// 进入子目录GetGenerateProxyFileInfos(fileInfoList, GetDirectoryContentPath(path, directoryContent));}else if (directoryContent.Name.EndsWith("generate-proxy.json")){fileInfoList.Add(...);}}
}
private string GetDirectoryContentPath(string rootPath, IFileInfo fileInfo)
{if (fileInfo is PhysicalDirectoryInfo physicalDirectoryInfo){return rootPath + physicalDirectoryInfo.Name.EnsureStartsWith('/');}return fileInfo.PhysicalPath!;  // ← 嵌入资源走这里
}

要规避递归查找路径的缺陷,可以将磁盘上的物理文件app-generate-proxy.json添加到虚拟文件系统的根目录下,这样该虚拟文件的物理路径指向磁盘上的文件路径
在GeneralHttpApiClientWithContractsModule模块类中,添加物理文件app-generate-proxy.json到虚拟文件系统中

Configure<AbpVirtualFileSystemOptions>(options =>
{options.FileSets.AddEmbedded<GeneralHttpApiClientWithContractsModule>();var assemblyDirectory = Path.GetDirectoryName(typeof(GeneralHttpApiClientWithContractsModule).Assembly.Location)!;options.FileSets.AddPhysical(Path.Combine(assemblyDirectory, "ClientProxies"));
});

再次运行客户端和服务端项目,调用客户端的GetUsersAsync方法,显示调用成功,获取到服务端的用户信息列表数据

Without Contracts模式

项目结构设计如下:

  • StaticClientWithoutContracts项目结构如下(客户端)
    Modules/StaticClientWithoutContracts/
    ├── General.StaticClient.WithoutContracts/ // 启动项目(待添加)
    ├── General.StaticClient.WithoutContracts.HttpApi.Client/ // Http代理客户端(待添加)

  • General.Backend.Web项目结构如下(服务端)
    ├── General.Backend.Application/
    ├── General.Backend.Application.Contracts/
    ├── General.Backend.DbMigrator/
    ├── General.Backend.Domain/
    ├── General.Backend.Domain.Shared/
    ├── General.Backend.EntityFrameworkCore/
    ├── General.Backend.HttpApi.StaticClient.WithoutContracts/ // Http代理客户端(待添加)
    ├── General.Backend.Web/

服务端

在服务端项目中,新建名称为General.Backend.HttpApi.StaticClient.WithoutContracts的类库,添加General.Backend.Application.Contracts项目依赖,使用引用的接口、DTO、枚举等类型。在程序包管理控制台选中General.Backend.HttpApi.StaticClient.WithoutContracts,执行以下命令安装ABP静态Api客户端代理和虚拟文件系统相关的Nuget包

Install-Package Volo.Abp.Http.Client -v 8.3.0
Install-Package Microsoft.Extensions.FileProviders.Embedded -v 8.0.25

新建名称为GeneralHttpApiClientWithoutContractsModule的模块类,添加AbpHttpClientModule、GeneralApplicationContractsModule和AbpVirtualFileSystemModule模块依赖,并注册静态Api客户端代理和配置当前项目的嵌入文件到虚拟文件系统中

[DependsOn(typeof(AbpHttpClientModule),typeof(AbpVirtualFileSystemModule),typeof(GeneralApplicationContractsModule)
)]
public class GeneralHttpApiClientWithoutContractsModule : AbpModule
{public const string RemoteServiceName = "GeneralBackend";public override void ConfigureServices(ServiceConfigurationContext context){context.Services.AddStaticHttpClientProxies(typeof(GeneralApplicationContractsModule).Assembly,RemoteServiceName);Configure<AbpVirtualFileSystemOptions>(options =>{options.FileSets.AddEmbedded<GeneralHttpApiClientWithoutContractsModule>();var assemblyDirectory = Path.GetDirectoryName(typeof(GeneralHttpApiClientWithoutContractsModule).Assembly.Location)!;options.FileSets.AddPhysical(Path.Combine(assemblyDirectory, "ClientProxies"));});}
}

同样的,也需要添加物理文件app-generate-proxy.json到虚拟文件系统中
在类库General.Backend.HttpApi.StaticClient.WithoutContracts中打开终端,执行以下命令生成静态Api客户端的代理实现类和app-generate-proxy.json文件,http://localhost:5025/ 是服务端项目运行的实际地址,需要提前把服务端运行起来,再执行以下命令

abp generate-proxy -t csharp -u http://localhost:5025/ --without-contracts

执行命令后会生成ClientProxies文件夹和app-generate-proxy.json接口定义文件,ClientProxies文件夹中只包含代理实现类文件
同样的,也需要配置ClientProxies\app-generate-proxy.json文件属性

<PropertyGroup><GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup><ItemGroup><None Remove="ClientProxies\app-generate-proxy.json" />
</ItemGroup><ItemGroup><EmbeddedResource Include="ClientProxies\app-generate-proxy.json"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></EmbeddedResource>
</ItemGroup>

客户端

新建名称为General.StaticClient.WithoutContracts.HttpApi.Client的类库,添加General.Backend.HttpApi.StaticClient.WithoutContracts项目依赖,在程序包管理控制台选中General.StaticClient.WithoutContracts.HttpApi.Client,执行以下命令安装ABP静态Api客户端代理相关的Nuget包

Install-Package Volo.Abp.Http.Client -v 8.3.0

新建名称为GeneralStaticClientWithoutContractsModule的模块类,添加AbpHttpClientModule和GeneralHttpApiClientWithoutContractsModule模块依赖

[DependsOn(typeof(AbpHttpClientModule),typeof(GeneralHttpApiClientWithoutContractsModule))]
public class GeneralStaticClientWithoutContractsModule : AbpModule
{public override void ConfigureServices(ServiceConfigurationContext context){}
}

同样的,WebApi启动项目的实现可参考动态Api客户端中的实现

远程调用

这里模拟的也是对远程服务的用户接口进行调用

[Route("api/[Controller]")]
public class ClientWithoutContractsController : AbpController
{private readonly IUserAppService _userAppService;public ClientWithoutContractsController(IUserAppService userAppService){_userAppService = userAppService;}[HttpGet("user/{id}")]public async Task<UserDto> GetUserByIdAsync(Guid id){var user = await _userAppService.GetAsync(id);return user;}[HttpGet("users")]public async Task<List<UserDto>> GetUsersAsync(){var users = await _userAppService.GetListAsync(new UserQueryDto{SortType = "asc",SortField = "Id"});return users.Items.ToList();}
}

同时把客户端和服务端项目运行起来,在客户端的Swagger页面访问GetUsersAsync方法,显示调用成功,获取到服务端的用户信息列表数据