为 ASP.NET Core Web API 生成 C# 客户端 API
通过 Code First 方法为 ASP.NET Core Web API 生成 C# 客户端 API,并为 jQuery、Aurelia、Axios 和 Angular 2+ 生成 TypeScript 客户端 API。
引言
为了开发 ASP.NET Web API 或 ASP.NET Core Web API 的客户端程序,强类型客户端 API 生成器 可生成 C# 和 TypeScript 强类型客户端 API。该工具包旨在最大限度地减少重复任务,简化后端开发和前端开发之间的协调,并通过更少的投入、工作和压力来提高开发团队的生产力和产品质量。
这个开源项目提供以下产品:
- C# 强类型客户端 API 代码生成器,支持桌面、通用 Windows、Android 和 iOS。
- TypeScript 强类型客户端 API 代码生成器,适用于 jQuery、Angular 2+ 和 Aurelia,以及使用 Axios 的 TypeScript/JavaScript 应用程序。
- TypeScript CodeDOM,一个用于 TypeScript 的 CodeDOM 组件,派生自 .NET 的 CodeDOM。
- POCO2TS.exe,一个从 POCO 类生成 TypeScript 接口的命令行程序。
- Fonlow.Poco2Ts,一个组件,用于从 POCO 类生成 TypeScript 接口
本文重点介绍为 ASP.NET Core 2.0+ 生成 C# 客户端 API 库。如果您仍在 .NET Framework 上工作,请查看 "为 ASP.NET Web API 生成 C# 客户端 API"。有关 TypeScript 客户端 API 库,请查看 "ASP.NET Web API, Angular 2, TypeScript and WebApiClientGen"。
背景
在开发需要高抽象和语义数据类型的 Web 应用程序时,您希望 API 和客户端 API 都能利用强数据类型来提高开发生产力和数据约束。
在 2015 年开发 WebApiClientGen
之前,我曾搜索并尝试找到一些现有解决方案,这些解决方案可以让我免于编写重复代码,从而专注于在客户端构建业务逻辑。以下是协助客户端程序开发的一些开源项目:
NSwag 在 2015 年 11 月 WebApiClientGen
初次发布后于 2016 年浮出水面,并得到了微软的认可。NSwag "将 Swashbuckle 和 AutoRest 的功能结合到一个工具链中",然而,它在架构上与 Swagger/OpenApi 耦合,这有一些固有的架构限制。
尽管这些解决方案可以在一定程度上生成强类型客户端代码并减少重复任务,但我发现它们都无法提供我所期望的所有流畅高效的编程体验:
- 与 ASP.NET Web 服务的数据模型对应的强类型客户端数据模型
- 与
ApiController
派生类的函数对应的强类型函数原型 - 像 WCF SvcUtils 那样批量生成代码,从而在 SDLC 期间最大限度地减少开销
- 通过使用流行的 .NET 属性(如
DataContractAttribute
和JsonObjectAttribute
等)进行数据批注来精选数据模型。 - 设计时和编译时类型检查
- 客户端数据模型、函数原型和文档注释的智能感知
这里是 WebApiClientGen。
Using the Code
假设
- 您一直在开发 ASP.NET Web API 应用程序,并将使用 C# 作为主要编程语言开发运行在 Windows 桌面、通用 Windows、Android 或 iOS 上的客户端应用程序。
- 您和您的同事都喜欢通过服务器端和客户端的强类型函数来实现高抽象。
- POCO 类被 Web API 和 Entity Framework Code First 使用,您可能不想将所有数据类和类成员发布到客户端程序。
步骤 0:将 NuGet 包 WebApiClientGenCore 安装到 ASP.NET Core 2.0+ Web MVC/API 项目中
安装还将把依赖的 NuGet 包 Fonlow.TypeScriptCodeDOMCore
和 Fonlow.Poco2TsCore
安装到项目引用中。
步骤 1:NuGet 安装后
步骤 1.1 创建 CodeGenController
在您的 Web API 项目中,添加 以下控制器(复制 Github 中最新的)
#if DEBUG //This controller is not needed in production release,
#since the client API should be generated during development of the Web API.
using Fonlow.CodeDom.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using System.Linq;
using System.Net;
namespace Fonlow.WebApiClientGen
{
[ApiExplorerSettings(IgnoreApi = true)]
[Route("api/[controller]")]
public class CodeGenController : ControllerBase
{
private readonly IApiDescriptionGroupCollectionProvider apiExplorer;
private readonly string webRootPath;
/// <summary>
/// For injecting some environment config by the run time.
/// </summary>
/// <param name="apiExplorer"></param>
/// <param name="hostingEnvironment"></param>
public CodeGenController(IApiDescriptionGroupCollectionProvider apiExplorer,
IWebHostEnvironment hostingEnvironment)
{
this.apiExplorer = apiExplorer;
this.webRootPath = hostingEnvironment.WebRootPath;
}
/// <summary>
/// Trigger the API to generate WebApiClientAuto.cs
/// for an established client API project.
/// </summary>
/// <param name="settings"></param>
/// <returns>OK if OK</returns>
[HttpPost]
public ActionResult TriggerCodeGen([FromBody] CodeGenSettings settings)
{
if (settings == null)
return BadRequest("No settings");
if (settings.ClientApiOutputs == null)
return BadRequest("No settings/ClientApiOutputs");
Fonlow.Web.Meta.WebApiDescription[] apiDescriptions;
try
{
var descriptions = ApiExplorerHelper.GetApiDescriptions(apiExplorer);
apiDescriptions = descriptions.Select
(d => Fonlow.Web.Meta.MetaTransform.GetWebApiDescription(d)).OrderBy
(d => d.ActionDescriptor.ActionName).ToArray();
}
catch (System.InvalidOperationException e)
{
System.Diagnostics.Trace.TraceWarning(e.Message);
return StatusCode((int)HttpStatusCode.InternalServerError, e.Message);
}
if (!settings.ClientApiOutputs.CamelCase.HasValue)
{
settings.ClientApiOutputs.CamelCase = true;
}
try
{
CodeGen.GenerateClientAPIs(this.webRootPath, settings, apiDescriptions);
}
catch (Fonlow.Web.Meta.CodeGenException e)
{
var msg = e.Message + " : " + e.Description;
System.Diagnostics.Trace.TraceError(msg);
return BadRequest(msg);
}
return Ok("Done");
}
}
}
#endif
备注
CodeGenController
应仅在调试构建的开发期间可用,因为客户端 API 应针对每个版本的 Web API 仅生成一次。
步骤 1.2 使 ApiExplorer 可见
这是为了告诉 WebApiClientGen
哪些控制器将进行客户端代码生成。
选择退出方法
在 Startup.cs 中,添加下面突出显示的行
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc( // or AddControllers, or AddControllersWithViews,
options =>
{
#if DEBUG
options.Conventions.Add
(new Fonlow.CodeDom.Web.
ApiExplorerVisibilityEnabledConvention());//To make ApiExplorer
//be visible to WebApiClientGen
#endif
}
);
使用 ApiExplorerVisibilityEnabledConvention
是一种选择退出方法,它将包含所有控制器,除了那些用 ApiExplorerSettingsAttribute
或 ApiControllerAttribute
装饰的控制器。如果您的大多数控制器都将生成强类型客户端 API,则此方法更适用。
选择加入方法
或者,如果您更喜欢选择加入方法,可以使用 ApiExplorerSettingsAttribute
来装饰 Web API 控制器,如下所示:
[ApiExplorerSettings(IgnoreApi = false)] // or [ApiController]
[Route("api/[controller]")]
public class HeroesController : ControllerBase
{
那么就不需要添加 ApiExplorerVisibilityEnabledConvention
了。如果您的大多数控制器都不需要生成强类型客户端 API,则此方法更适用。
步骤 2:创建 .NET Core 客户端 API 项目
提示
如果您确信 System.Text.Json 可以处理服务器和 .NET 客户端上所有强数据类型场景,您可以将 codegen.json 中的 UseSystemTextJson 设置为 true
,这样您就不需要 Newtonsoft.Json 了。
步骤 3:准备 JSON 配置数据
您的 Web API 项目可能具有如下 POCO 类和 API 函数
namespace DemoWebApi.DemoData
{
public sealed class Constants
{
public const string DataNamespace = "http://fonlow.com/DemoData/2014/02";
}
[DataContract(Namespace = Constants.DataNamespace)]
public enum AddressType
{
[EnumMember]
Postal,
[EnumMember]
Residential,
};
[DataContract(Namespace = Constants.DataNamespace)]
public enum Days
{
[EnumMember]
Sat = 1,
[EnumMember]
Sun,
[EnumMember]
Mon,
[EnumMember]
Tue,
[EnumMember]
Wed,
[EnumMember]
Thu,
[EnumMember]
Fri
};
...
[DataContract(Namespace = Constants.DataNamespace)]
public class Entity
{
public Entity()
{
Addresses = new List<Address>();
}
[DataMember]
public Guid Id { get; set; }
[DataMember(IsRequired =true)]//MVC and Web API does not care
[System.ComponentModel.DataAnnotations.Required]//MVC and Web API care
//about only this
public string Name { get; set; }
[DataMember]
public IList<Address> Addresses { get; set; }
public override string ToString()
{
return Name;
}
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Person : Entity
{
[DataMember]
public string Surname { get; set; }
[DataMember]
public string GivenName { get; set; }
[DataMember]
public DateTime? BirthDate { get; set; }
public override string ToString()
{
return Surname + ", " + GivenName;
}
}
...
namespace DemoWebApi.Controllers
{
[Route("api/[controller]")]
public class EntitiesController : Controller
{
/// <summary>
/// Get a person
/// so to know the person
/// </summary>
/// <param name="id">unique id of that guy</param>
/// <returns>person in db</returns>
[HttpGet]
[Route("getPerson/{id}")]
public Person GetPerson(long id)
{
return new Person()
{
Surname = "Huang",
GivenName = "Z",
Name = "Z Huang",
DOB = DateTime.Now.AddYears(-20),
};
}
[HttpPost]
[Route("createPerson")]
public long CreatePerson([FromBody] Person p)
{
Debug.WriteLine("CreatePerson: " + p.Name);
if (p.Name == "Exception")
throw new InvalidOperationException("It is exception");
Debug.WriteLine("Create " + p);
return 1000;
}
[HttpPut]
[Route("updatePerson")]
public void UpdatePerson([FromBody] Person person)
{
Debug.WriteLine("Update " + person);
}
JSON 配置数据如下所示:
{
"ApiSelections": {
"ExcludedControllerNames": [
"DemoWebApi.Controllers.Home",
"DemoWebApi.Controllers.FileUpload"
],
"DataModelAssemblyNames": [
"DemoWebApi.DemoDataCore",
"DemoCoreWeb"
],
"CherryPickingMethods": 3
},
"ClientApiOutputs": {
"ClientLibraryProjectFolderName": "..\\..\\..\\..\\..\\DemoCoreWeb.ClientApi",
"GenerateBothAsyncAndSync": true,
"StringAsString": true,
"CamelCase": true,
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGenCore.NG2",
"TargetDir": "..\\..\\..\\..\\..\\DemoNGCli\\NGSource\\src\\ClientApi",
"TSFile": "WebApiCoreNG2ClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
}
]
}
}
建议将 JSON 有效负载保存到文件中,如本截图所示
提示
ExcludedControllerNames
属性将 排除那些已对 ApiExplorer 可见的控制器,或者被 [ApiExplorerSettings(IgnoreApi = true)]
装饰的控制器将不会对 ApiExplorer
可见。
StringAsString
是 一个用于 .NET Core Web API 的选项,它默认返回 text/plain string
,而不是 application/json JSON 对象,因此生成的客户端代码不会反序列化相应 Web API 函数的响应体。
步骤 4:运行 Web API 项目的 DEBUG 构建,并 POST JSON 配置数据以触发客户端 API 代码的生成
在开发过程中,您有两种在 VS 解决方案文件夹中启动 Web API 的方式。
DotNet
在命令提示符中,CD 到一个文件夹,例如 C:\VSProjects\MySln\DemoCoreWeb\bin\Debug\netcoreapp3.0,然后运行
dotnet democoreweb.dll
或直接运行 democoreweb.exe。
IIS Express
在 VS IDE 中运行 Web 项目,IIS Express 将启动以托管 Web 应用程序。
备注
Web 应用程序的不同托管方式可能会导致不同的 Web 根路径,因此您可能需要相应地调整 JSON 配置数据以适应文件夹。
您可以创建并运行一个 PowerShell 文件来启动 Web 服务并 POST
cd $PSScriptRoot
<#
Make sure CodeGen.json is saved in format ANSI or UTF-8 without BOM,
since ASP.NET Core 2.0 Web API will fail to deserialize POST Body that contains BOM.
#>
$path = "$PSScriptRoot\DemoCoreWeb\bin\Debug\netcoreapp3.0"
$procArgs = @{
FilePath = "dotnet.exe"
ArgumentList = "$path\DemoCoreWeb.dll"
WorkingDirectory = $path
PassThru = $true
}
$process = Start-Process @procArgs
$restArgs = @{
Uri = 'https://:5000/api/codegen'
Method = 'Post'
InFile = "$PSScriptRoot\DemoCoreWeb\CodeGen.json"
ContentType = 'application/json'
}
Invoke-RestMethod @restArgs
Stop-Process $process
发布客户端 API 库
完成这些步骤后,您现在已经将 C# 客户端 API 生成到一个名为 WebApiClientAuto.cs 的文件中,类似于这个示例:
public partial class Entities
{
private System.Net.Http.HttpClient client;
private System.Uri baseUri;
public Entities(System.Net.Http.HttpClient client, System.Uri baseUri)
{
if (client == null)
throw new ArgumentNullException("client", "Null HttpClient.");
if (baseUri == null)
throw new ArgumentNullException("baseUri", "Null baseUri");
this.client = client;
this.baseUri = baseUri;
}
/// <summary>
/// Get a person
/// so to know the person
/// GET api/Entities/getPerson/{id}
/// </summary>
/// <param name="id">unique id of that guy</param>
/// <returns>person in db</returns>
public async Task<DemoWebApi.DemoData.Client.Person> GetPersonAsync(long id)
{
var requestUri = new Uri(this.baseUri, "api/Entities/getPerson/"+id);
var responseMessage = await client.GetAsync(requestUri);
responseMessage.EnsureSuccessStatusCode();
var stream = await responseMessage.Content.ReadAsStreamAsync();
using (JsonReader jsonReader = new JsonTextReader
(new System.IO.StreamReader(stream)))
{
var serializer = new JsonSerializer();
return serializer.Deserialize<DemoWebApi.DemoData.Client.Person>
(jsonReader);
}
}
/// <summary>
/// Get a person
/// so to know the person
/// GET api/Entities/getPerson/{id}
/// </summary>
/// <param name="id">unique id of that guy</param>
/// <returns>person in db</returns>
public DemoWebApi.DemoData.Client.Person GetPerson(long id)
{
var requestUri = new Uri(this.baseUri, "api/Entities/getPerson/"+id);
var responseMessage = this.client.GetAsync(requestUri).Result;
responseMessage.EnsureSuccessStatusCode();
var stream = responseMessage.Content.ReadAsStreamAsync().Result;
using (JsonReader jsonReader =
new JsonTextReader(new System.IO.StreamReader(stream)))
{
var serializer = new JsonSerializer();
return serializer.Deserialize<DemoWebApi.DemoData.Client.Person>
(jsonReader);
}
}
/// <summary>
/// POST api/Entities/createPerson
/// </summary>
public async Task<long> CreatePersonAsync(DemoWebApi.DemoData.Client.Person p)
{
var requestUri = new Uri(this.baseUri, "api/Entities/createPerson");
using (var requestWriter = new System.IO.StringWriter())
{
var requestSerializer = JsonSerializer.Create();
requestSerializer.Serialize(requestWriter, p);
var content = new StringContent(requestWriter.ToString(),
System.Text.Encoding.UTF8, "application/json");
var responseMessage = await client.PostAsync(requestUri, content);
responseMessage.EnsureSuccessStatusCode();
var stream = await responseMessage.Content.ReadAsStreamAsync();
using (JsonReader jsonReader =
new JsonTextReader(new System.IO.StreamReader(stream)))
{
var serializer = new JsonSerializer();
return System.Int64.Parse(jsonReader.ReadAsString());
}
}
}
SDLC。
初始设置后,每次您对 Web API 的接口进行更改时,只需:
- 构建 Web API 的 DEBUG 版本。
- 运行 CreateClientApi.ps1,它将启动 dotnet Kestrel Web 服务器或 IIS Express。
- 构建并运行您的客户端集成测试。
以下时序图说明了程序员和自动化步骤之间的交互。
团队协作
本节介绍了一些基本的团队协作场景。不同公司和团队的情况和背景可能不同,因此您应相应地调整您的团队实践。
您的团队有一个后端开发人员 Brenda 负责 Web API,一个前端开发人员 Frank 负责前端。每个开发机器都正确设置了集成测试环境,因此大多数 CI 工作都可以在每个开发机器上完成,而无需团队 CI 服务器。主干开发是默认的分支实践。如果您不使用 TBD,而是 Git Flow 或其他分支策略,那么调整起来应该不难。
1 个包含后端代码和前端代码的存储库
- Brenda 编写了一些新的 Web API 代码并进行了构建。
- Brenda 执行 CreateClientApi.ps1 来生成客户端代码。
- Brenda 编写并运行了一些基本的集成测试用例来测试 Web API。
- Brenda 将更改提交/推送到主开发分支或主干。
- Frank 更新/拉取更改,进行构建,并运行测试用例。
- Frank 基于新的 Web API 和客户端 API 开发新的前端功能。
1 个后端存储库和 1 个前端存储库
Brenda 调整了 CodeGen.json,该文件会将生成的代码定向到前端存储库工作目录中的客户端 API 文件夹。
- Brenda 编写了一些新的 Web API 代码并进行了构建。
- Brenda 执行 CreateClientApi.ps1 来生成客户端代码。
- Brenda 编写并运行了一些基本的集成测试用例来测试 Web API。
- Brenda 将更改提交/推送到两个存储库的主开发分支或主干。
- Frank 使用两个存储库更新/拉取更改,进行构建,并运行测试用例。
- Frank 基于新的 Web API 和客户端 API 开发新的前端功能。
兴趣点
ASP.NET 的 Controller 和 ApiController,以及 ASP.NET Core 的 Controller 和 ControllerBase
在 ASP.NET Web API 之前的旧时代,程序员必须使用 MVC 控制器来创建基于 JSON 的 Web API。后来微软创建了 ASP.NET Web API,从那时起程序员一直使用 System.Web.Http.ApiController
。现在有了 ASP.NET Core,程序员使用 Microsoft.AspNetCore.Mvc.ControllerBase
或 Microsoft.AspNetCore.Mvc.Controller
来创建 Web API,其中 ControllerBase
仅支持 Web API,而 Controller
同时支持 Web API 和 MVC 视图。
尽管如此,在一个 Controller
派生类中混合 API 函数和 View
函数可能是不明智的。
处理 HTTP 响应中的字符串
在 ASP.NET Web API 中,如果 Web API 函数返回 string
,则响应体始终是 JSON 对象,除非您提供一个自定义格式化程序,将 string
作为 string
返回。在 .NET Core Web API 中,此类 API 函数将 默认在响应体中将字符串作为字符串返回,除非客户端 HTTP 请求提供接受头 "application/json
"。当在 CodeGen JSON 配置中提供 "StringAsString" : true
时,生成的客户端代码将不会反序列化相应 Web API 函数的响应体,显然,如果 Web API 函数将返回一个大型 string
,这将更高效。
关于 .NET Core 的 NuGet
据推测,您已经阅读了 "为 ASP.NET Web API 生成 C# 客户端 API"。导入 NuGet 包 Fonlow.WebApiClientGen 时,安装 NuGet 包可以将 CodeGenController
和其他文件复制到 Web 项目中。然而,对于 .NET Core Web 项目,Fonlow.WebApiClientGenCore 只能复制程序集。Rick Strahl 在此解释得很好:
WebApiClientGen vs Swagger
- GitHub 上的代码示例,SwaggerDemo 分支用于比较 WebApiClientGen 和 Swagger。
OpenApiClientGen
OpenApiClientGen
基于 Fonlow.TypeScriptCodeDomCore
和 Fonlow.Poco2TsCore
,它们是 WebApiClientGen 的核心组件,因此生成的代码具有相似的特性。
与 NSwag 的比较
当使用其他供应商提供且具有 Swagger/OpenAPI 定义的 Web 服务时,您可以尝试使用 OpenApiClientGen
。
如果您正在进行 Web 服务和客户端程序的完整栈开发,您不需要 Swagger/OpenAPI,除非您想向其他公司提供客户端 API,其中一些公司正在使用 WebApiClientGen
不支持的技术栈。
参考文献
- Swagger/AutoRest vs WebApiClientGen
- Visual Acuity Charts,一个视力表移动应用程序,使用为 Xamarin 生成的客户端 API 代码,支持 Android 和 iOS