为 ASP.NET Core Web API 生成 TypeScript 客户端 API
如何为 ASP.NET Core 生成强类型 TypeScript 客户端 API
引言
为了开发 ASP. NET Core Web API 的客户端程序,强类型客户端 API 生成器 会生成 C# 和 TypeScript 代码中的强类型客户端 API,以最大限度地减少重复性任务,提高应用程序开发人员的生产力和产品的质量。然后,您可以将生成的源代码或编译后的客户端 API 库提供或发布给自己、团队中的其他开发人员或 B2B 合作伙伴。
本文重点介绍为各种 JavaScript 库和 TypeScript 框架生成 TypeScript 客户端 API。
备注
即使您进行 JavaScript 编程,仍然可以使用 WebApiClientGen
,因为生成的 TypeScript 文件可以编译成 JavaScript 文件。虽然您将无法获得设计时类型检查和编译时类型检查,但只要您的 JS 编辑器支持此类功能,您仍然可以通过源代码文档享受设计时智能提示。
背景
WebApiClientGen
的开发始于 2015 年,用于 WPF 应用程序中的 C# .NET 客户端 API。Swagger (现已更名为 OpenApi) 存在一些固有的不足
- 数据类型数量有限
- 不支持泛型
- ...
WebApiClientGen
最初是为 .NET Framework 和 .NET (Core) 设计的,它(95%)充分利用了 .NET 丰富的数据类型。之后,在开发了 TypeScript CodeDOM 后,我开发了对 jQuery 的支持,起初是为了好玩,后来是为了生产。
作为一名主要从事复杂业务应用程序开发的应用程序开发人员,我期望获得这样的高效编程体验
- 强类型的客户端数据模型映射到服务的模型。
- 强类型函数原型映射到
ApiController
派生类的函数。 - 以 WCF 编程方式进行批量代码生成。
- 通过数据注释(使用
DataContractAttribute
和JsonObjectAttribute
等流行属性)来选择性地获取数据模型。 - 设计时和编译时进行类型检查。
- 客户端数据模型、函数原型和文档注释的智能提示。
- 跨编程平台的 API 原型一致:C#,用于 jQuery、Angular、Aurelia、Fetch API 和 AXIOS 的 TypeScript
隆重推出 WebApiClientGen。
假设
- 您正在开发 ASP.NET Core 应用程序,并将基于 AJAX 开发 Web 前端的 JavaScript 库,使用 jQuery 或 SPA(Angular2、Vue 或 React)。
- 您和您的同事更倾向于在服务器端和客户端都通过强类型函数实现高抽象,并且使用了 TypeScript。
- POCO 类由 Web API 和 Entity Framework Code First 共用,您可能不想将所有数据类和成员发布到客户端程序。
另外,如果您的团队推崇 Trunk Based Development,那就更好了,因为 WebApiClientGen
的设计以及使用 WebApiClientGen
的工作流程都考虑到了 Trunk Based Development,它比 Feature Branching 和 Gitflow 等其他分支策略更适合持续集成。
为了跟进这种开发客户端程序的新方式,最好拥有一个 ASP.NET Core Web API 项目,或者一个包含 Web API 的 MVC 项目。您可以使用现有项目,也可以创建一个演示项目。
Using the Code
本文重点介绍使用 jQuery 的代码示例。有关 Angular 2+ 的类似代码示例,请参阅文章 "ASP.NET Web API, Angular2, TypeScript and WebApiClientGen"。
步骤 0:在 Web API 项目中安装 NuGet 包 WebApiClientGenCore 和 WebApiClientGenCore.jQuery
安装还将向项目引用安装依赖的 NuGet 包 Fonlow.TypeScriptCodeDomCore
和 Fonlow.Poco2TsCore
。
一个 HttpClient 辅助库 应与生成的代码一起复制到 _Scripts_ 文件夹,并且每次执行 CodeGen 时都会更新。
此外,用于触发 CodeGen 的 CodeGenController.cs 已添加到项目的 _Controllers_ 文件夹。
CodeGenController
应仅在调试版本开发期间可用,因为客户端 API 应为每个 Web API 版本生成一次。
#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;
}
备注
- Nuget 包 WebApiClientGenCore 不安装 CodeGenController,您需要 复制该文件。
启用 Web API 的文档注释
在 ASP.NET Core 项目的“属性”中,勾选 _生成/输出/文档文件/生成包含 API 文档的文件_。对于复杂/企业级应用程序,您很可能会在其他程序集中建立数据模型。请检查这些程序集的相同设置,WebApiClientGen
将读取 XML 文档文件并将内容复制到生成的代码中。
步骤 1:准备 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 Address
{
[DataMember]
public Guid Id { get; set; }
public Entity Entity { get; set; }
/// <summary>
/// Foreign key to Entity
/// </summary>
public Guid EntityId { get; set; }
[DataMember]
public string Street1 { get; set; }
[DataMember]
public string Street2 { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string State { get; set; }
[DataMember]
public string PostalCode { get; set; }
[DataMember]
public string Country { get; set; }
[DataMember]
public AddressType Type { get; set; }
[DataMember]
public DemoWebApi.DemoData.Another.MyPoint Location;
}
[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;
}
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Company : Entity
{
[DataMember]
public string BusinessNumber { get; set; }
[DataMember]
public string BusinessNumberType { get; set; }
[DataMember]
public string[][] TextMatrix
{ get; set; }
[DataMember]
public int[][] Int2DJagged;
[DataMember]
public int[,] Int2D;
[DataMember]
public IEnumerable<string> Lines;
}
...
...
namespace DemoWebApi.Controllers
{
[RoutePrefix("api/SuperDemo")]
public class EntitiesController : ApiController
{
/// <summary>
/// Get a person
/// </summary>
/// <param name="id">unique id of that guy</param>
/// <returns>person in db</returns>
[HttpGet]
public Person GetPerson(long id)
{
return new Person()
{
Surname = "Huang",
GivenName = "Z",
Name = "Z Huang",
BirthDate = DateTime.Now.AddYears(-20),
};
}
[HttpPost]
public long CreatePerson(Person p)
{
Debug.WriteLine("CreatePerson: " + p.Name);
if (p.Name == "Exception")
throw new InvalidOperationException("It is exception");
Debug.WriteLine("Create " + p);
return 1000;
}
[HttpPut]
public void UpdatePerson(Person person)
{
Debug.WriteLine("Update " + person);
}
[HttpPut]
[Route("link")]
public bool LinkPerson(long id, string relationship, [FromBody] Person person)
{
return person != null && !String.IsNullOrEmpty(relationship);
}
[HttpDelete]
public void Delete(long id)
{
Debug.WriteLine("Delete " + id);
}
[Route("Company")]
[HttpGet]
public Company GetCompany(long id)
{
下面的 JSON 配置数据将 `POST` 到 CodeGen Web API
{
"ApiSelections": {
"ExcludedControllerNames": [
"DemoWebApi.Controllers.Account",
"DemoWebApi.Controllers.FileUpload"
],
"DataModelAssemblyNames": [
"DemoWebApi.DemoData",
"DemoWebApi"
],
"CherryPickingMethods": 3
},
"ClientApiOutputs": {
"ClientLibraryProjectFolderName": "..\\DemoWebApi.ClientApi",
"GenerateBothAsyncAndSync": true,
"CamelCase": true,
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGen.jQuery",
"TargetDir": "Scripts\\ClientApi",
"TSFile": "WebApiJQClientAuto.ts",
"AsModule": false,
"ContentType": "application/json;charset=UTF-8"
},
{
"AssemblyName": "Fonlow.WebApiClientGen.NG2",
"TargetDir": "..\\DemoNGCli\\NGSource\\src\\ClientApi",
"TSFile": "WebApiNG2ClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
}
]
}
}
如果您所有的 POCO 类都定义在 Web API 项目中,您应该将 Web API 项目的程序集名称放入 DataModelAssemblyNames
数组。如果您有一些专用的数据模型程序集以实现良好的关注点分离,您应该将相应的程序集名称放入数组。
TypeScriptNG2Folder 是指向 Angular2 项目的绝对路径或相对路径。例如,..\\DemoAngular2\\ClientApi 表示一个作为 Web API 项目的同级项目创建的 Angular 2 项目。
CodeGen 根据 CherryPickingMethods
生成 POCO 类的强类型 TypeScript 接口,该方法在下面的文档注释中进行了描述。
/// <summary>
/// Flagged options for cherry picking in various development processes.
/// </summary>
[Flags]
public enum CherryPickingMethods
{
/// <summary>
/// Include all public classes, properties and properties.
/// </summary>
All = 0,
/// <summary>
/// Include all public classes decorated by DataContractAttribute,
/// and public properties or fields decorated by DataMemberAttribute.
/// And use DataMemberAttribute.IsRequired
/// </summary>
DataContract =1,
/// <summary>
/// Include all public classes decorated by JsonObjectAttribute,
/// and public properties or fields decorated by JsonPropertyAttribute.
/// And use JsonPropertyAttribute.Required
/// </summary>
NewtonsoftJson = 2,
/// <summary>
/// Include all public classes decorated by SerializableAttribute,
/// and all public properties or fields
/// but excluding those decorated by NonSerializedAttribute.
/// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
/// </summary>
Serializable = 4,
/// <summary>
/// Include all public classes, properties and properties.
/// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
/// </summary>
AspNet = 8,
}
默认值为 DataContract
,表示选择加入。您可以使用任何一种或组合方法。
步骤 2:运行 Web API 项目的 DEBUG 版本,并 POST JSON 配置数据以触发客户端 API 代码的生成
在 IDE 中使用 IIS Express 运行 Web 项目。
然后,您可以使用 Curl 或 Poster 或任何您喜欢的客户端工具 `POST` 到 _https://:10965/api/CodeGen_,并设置 `content-type=application/json`。
提示
因此,基本上,您只需要执行步骤 2 即可在 Web API 更新时生成客户端 API,因为您不必每次都安装 NuGet 包或创建新的 JSON 配置数据。
编写一些批处理脚本来启动 Web API 和 POST JSON 配置数据应该不难。事实上,我已经为您起草了一个:一个 **PowerShell 脚本文件,该脚本在 DotNet Kestrel 上启动 Web (API) 项目,然后 POST JSON 配置数据以触发代码生成**。
发布客户端 API 库
现在您已生成了 TypeScript 客户端 API,类似于此示例
///<reference path="../typings/jquery/jquery.d.ts" />
///<reference path="HttpClient.ts" />
namespace DemoWebApi_DemoData_Client {
export interface Address {
city?: string;
country?: string;
id?: string;
postalCode?: string;
state?: string;
street1?: string;
street2?: string;
type?: DemoWebApi_DemoData_Client.AddressType;
/**
* It is a field
*/
location?: DemoWebApi_DemoData_Another_Client.MyPoint;
}
export enum AddressType { Postal, Residential }
export interface Company extends DemoWebApi_DemoData_Client.Entity {
/**
* BusinessNumber to be serialized as BusinessNum
*/
BusinessNum?: string;
businessNumberType?: string;
foundDate?: Date;
registerDate?: Date;
textMatrix?: Array<Array<string>>;
int2D?: number[][];
int2DJagged?: Array<Array<number>>;
lines?: Array<string>;
}
export enum Days {
Sat = 1,
Sun = 2,
Mon = 3,
Tue = 4,
Wed = 5,
/**
* Thursday
*/
Thu = 6,
Fri = 7
}
/**
* Base class of company and person
*/
export interface Entity {
/**
* Multiple addresses
*/
addresses?: Array<DemoWebApi_DemoData_Client.Address>;
id?: string;
/**
* Name of the entity.
*/
name: string;
phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
web?: string;
}
...
namespace DemoWebApi_Controllers_Client {
export class Entities {
constructor(private baseUri: string = HttpClient.locationOrigin,
private httpClient: HttpClientBase = new HttpClient(),
private error?: (xhr: JQueryXHR, ajaxOptions: string,
thrown: string) => any, private statusCode?: { [key: string]: any; }) {
}
/**
* POST api/Entities/createCompany
*/
createCompany(p: DemoWebApi_DemoData_Client.Company,
callback: (data : DemoWebApi_DemoData_Client.Company) => any,
headersHandler?: () => {[header: string]: string}) {
this.httpClient.post(this.baseUri + 'api/Entities/createCompany',
p, callback, this.error, this.statusCode,
'application/json;charset=UTF-8', headersHandler);
}
/**
* POST api/Entities/createPerson
*/
createPerson(p: DemoWebApi_DemoData_Client.Person, callback: (data : number)
=> any, headersHandler?: () => {[header: string]: string}) {
this.httpClient.post(this.baseUri + 'api/Entities/createPerson',
p, callback, this.error, this.statusCode,
'application/json;charset=UTF-8', headersHandler);
}
/**
* DELETE api/Entities/{id}
*/
delete(id: number, callback: (data : void) => any,
headersHandler?: () => {[header: string]: string}) {
this.httpClient.delete(this.baseUri + 'api/Entities/' + id,
callback, this.error, this.statusCode, headersHandler);
}
/**
* GET api/Entities/Company/{id}
*/
getCompany(id: number, callback:
(data : DemoWebApi_DemoData_Client.Company) => any,
headersHandler?: () => {[header: string]: string}) {
this.httpClient.get(this.baseUri + 'api/Entities/Company/' + id,
callback, this.error, this.statusCode, headersHandler);
}
提示
- PowerShell 脚本还将 TS 文件编译为用于 jQuery 的 JS 文件。
内部使用
在使用 Visual Studio 等不错的文本编辑器编写客户端代码时,您可能会获得很好的智能提示。
外部使用
如果您希望外部开发人员通过 JavaScript 使用您的 Web API,您可以发布生成的 TypeScript 客户端 API 文件或编译后的 JavaScript 文件。
对于 Angular
必备组件
步骤 1:在 Web API 项目中,导入 NuGet 包:Fonlow.WebApiClientGenCore.NG2
步骤 2:在将 POST 到 Web 服务的 _CodeGen.json_ 中,添加以下内容
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGenCore.NG2",
"TargetDir": "..\\..\\..\\..\\..\\HeroesDemo\\src\\ClientApi",
"TSFile": "WebApiCoreNG2ClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8",
"ClientNamespaceSuffix": ".Client",
"ContainerNameSuffix": "",
"DataAnnotationsToComments": true,
"HelpStrictMode": true
},
生成的代码
...
export namespace DemoWebApi_Controllers_Client {
@Injectable()
export class Entities {
constructor(@Inject('baseUri') private baseUri: string = location.protocol +
'//' + location.hostname + (location.port ? ':' + location.port : '') +
'/', private http: HttpClient) {
}
/**
* POST api/Entities/createCompany
*/
createCompany(p?: DemoWebApi_DemoData_Client.Company, headersHandler?:
() => HttpHeaders): Observable<DemoWebApi_DemoData_Client.Company> {
return this.http.post<DemoWebApi_DemoData_Client.Company>
(this.baseUri + 'api/Entities/createCompany', JSON.stringify(p),
{ headers: headersHandler ? headersHandler().append('Content-Type',
'application/json;charset=UTF-8') : new HttpHeaders
({ 'Content-Type': 'application/json;charset=UTF-8' }) });
}
/**
* POST api/Entities/createPerson
*/
createPerson(p?: DemoWebApi_DemoData_Client.Person, headersHandler?:
() => HttpHeaders): Observable<number> {
return this.http.post<number>(this.baseUri + 'api/Entities/createPerson',
JSON.stringify(p), { headers: headersHandler ? headersHandler().append
('Content-Type', 'application/json;charset=UTF-8') :
new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) });
}
/**
* DELETE api/Entities/{id}
*/
delete(id?: number, headersHandler?: () => HttpHeaders):
Observable<HttpResponse<string>> {
return this.http.delete(this.baseUri + 'api/Entities/' + id,
{ headers: headersHandler ? headersHandler() : undefined,
observe: 'response', responseType: 'text' });
}
/**
* GET api/Entities/Company/{id}
*/
getCompany(id?: number, headersHandler?: () => HttpHeaders):
Observable<DemoWebApi_DemoData_Client.Company> {
return this.http.get<DemoWebApi_DemoData_Client.Company>
(this.baseUri + 'api/Entities/Company/' + id,
{ headers: headersHandler ? headersHandler() : undefined });
}
备注
自 WebApiClientGen
v1.9.0-beta(2016 年 6 月,当时 Angular 2 仍处于 RC1 版本)起,已提供对 Angular2 的支持。自 WebApiClientGen
v2.0 起,已提供对 Angular 2 正式版的支持。请参阅文章 "ASP.NET Web API, Angular2, TypeScript and WebApiClientGen"。
对于 Fetch API
必备组件
步骤 1:添加 NuGet 包:Fonlow.WebApiClientGenCore.Fetch
步骤 2:POST 此载荷
{
"AssemblyName": "Fonlow.WebApiClientGenCore.Fetch",
"TargetDir": "..\\..\\..\\..\\..\\fetchapi\\src\\clientapi",
"TSFile": "WebApiCoreFetchClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8",
"HelpStrictMode": true
}
生成的代码
export namespace DemoWebApi_Controllers_Client {
export class Entities {
constructor(private baseUri: string = location.protocol + '//' +
location.hostname + (location.port ? ':' + location.port : '') + '/') {
}
/**
* POST api/Entities/createCompany
*/
createCompany(p?: DemoWebApi_DemoData_Client.Company,
headersHandler?: () => {[header: string]: string}):
Promise<DemoWebApi_DemoData_Client.Company> {
return fetch(this.baseUri + 'api/Entities/createCompany',
{ method: 'post', headers: headersHandler ?
Object.assign(headersHandler(), { 'Content-Type':
'application/json;charset=UTF-8' }): { 'Content-Type':
'application/json;charset=UTF-8' },
body: JSON.stringify(p) }).then(d => d.json());
}
/**
* POST api/Entities/createPerson
*/
createPerson(p?: DemoWebApi_DemoData_Client.Person,
headersHandler?: () => {[header: string]: string}): Promise<number> {
return fetch(this.baseUri + 'api/Entities/createPerson',
{ method: 'post', headers: headersHandler ? Object.assign(headersHandler(),
{ 'Content-Type': 'application/json;charset=UTF-8' }):
{ 'Content-Type': 'application/json;charset=UTF-8' },
body: JSON.stringify(p) }).then(d => d.json());
}
/**
* DELETE api/Entities/{id}
*/
delete(id?: number, headersHandler?: () => {[header: string]: string}):
Promise<Response> {
return fetch(this.baseUri + 'api/Entities/' + id,
{ method: 'delete', headers: headersHandler ? headersHandler() : undefined });
}
/**
* GET api/Entities/Company/{id}
*/
getCompany(id?: number, headersHandler?: () =>
{[header: string]: string}): Promise<DemoWebApi_DemoData_Client.Company> {
return fetch(this.baseUri + 'api/Entities/Company/' + id,
{ method: 'get', headers: headersHandler ? headersHandler() :
undefined }).then(d => d.json());
}
兴趣点
简洁性和一致性
如您所见,为 jQuery、Angular 和 Aurelia 生成的代码的 API 几乎相同。除非您使用回调,否则应用程序代码看起来是相同的。
Angular 和 Aurelia 是 TypeScript 框架,自带 HttpClient
组件。jQuery 作为一个库,自带 jQuery.ajax
,而 辅助库 HttpClient.ts 使 API 和生成代码的实现变得简单而一致。
许多 JavaScript 库,如 React 和 Vue.js,不自带内置的 HTTP 请求库或组件,JavaScript 程序员通常使用 AXIOS 或 Fetch API。您可以找到一些关于 React 和 Vue.js 如何利用生成的代码来使用 AXIOS 的示例。
备注
React 背后的 Babel 团队在 2019 年 3 月改变了主意,开始支持命名空间,如 issue 60 中所述。
服务实现细节
虽然 ASP.NET Core MVC 和 Web API 可以使用 _System.Text.Json_ 或 _NewtonSoft.Json_ 进行 JSON 应用程序,但 _NewtonSoft.Json_ 可以 很好地处理由 DataContractAttribute 装饰的 POCO 类。
CLR 命名空间将通过将点替换为下划线并添加 Client
作为后缀来转换为 TypeScript 命名空间。例如,namespace My.Name.space
将转换为 My_Name_space_Client
。
从某种角度来看,服务命名空间/函数名与客户端命名空间/函数名之间的一一对应关系暴露了服务的实现细节,这通常不被推荐。然而,传统的 RESTful 客户端编程要求程序员了解服务函数的 URL 查询模板,而查询模板是服务的实现细节。因此,这两种方法在某种程度上都暴露了服务的实现细节,但后果不同。
对客户端开发人员来说,经典的函数原型如
ReturnType DoSomething(Type1 t1, Type2 t2 ...)
是 API 函数,其余的是传输的技术实现细节:TCP/IP、HTTP、SOAP、面向资源、CRUD 式 URI、RESTful、XML 和 JSON 等。函数原型和一段 API 文档应该足以调用 API 函数。客户端开发人员不应关心这些传输的技术细节,至少在操作成功时是这样。只有在出现错误时,开发人员才需要关心技术细节。例如,在基于 SOAP 的 Web 服务中,您需要了解 SOAP 故障;在 RESTful Web 服务中,您可能需要处理 HTTP 状态码和响应。
而查询模板对 API 函数的语义含义几乎没有帮助。相比之下,WebApiClientGen
以服务函数命名客户端函数,就像 WCF 中的 SvcUtil.exe 默认做的那样,因此生成的客户端函数具有良好的语义含义,只要您作为服务开发人员已经为服务函数起了有意义的名称。
在涵盖服务开发和客户端开发的整个 SDLC 大图中,服务开发人员了解服务函数的语义含义,并且通常将函数命名为功能描述是一个良好的编程实践。面向资源的 CRUD 可能具有语义含义,或者只是从功能描述的技术翻译。
WebApiClientGen
将您的 Web API 的文档注释复制到生成的 TypeScript 代码的 JsDoc3
注释中,因此您几乎不需要阅读 MVC 生成的帮助页面,并且您与服务的客户端编程将变得更加无缝。
持续集成
提示
编写脚本来自动化一些步骤以实现持续集成应该不难。您可以在以下位置找到示例:
- WebApiClientGen
- WebApiClientGen Examples(针对 .NET Framework、.NET Standard、Xamarin 和 vue TS)。
- .NET Core Demo(针对 ASP.NET Core MVC、Web API、ASP.NET Core + Angular、MAUI、fetchAPI、vue TS 和 React TS)。
备注
Web 服务和客户端的开发格局一直在迅速变化。自 WebApiClientGen
于 2015 年 9 月首次发布以来,由 Open API Initiative 主导的 Open API Definition Format 应运而生,并于 2015 年 11 月 成立。希望该倡议能够解决 Swagger Specification 2.0 的一些不足,特别是在处理 decimal / monetary 类型方面。尽管如此,利用 WebApiClientGen
的 SDLC 经过优化,可用于开发基于 ASP.NET Web API 和 ASP.NET Core Web API 的客户端程序。
SDLC。
因此,基本上,您只需编写 Web API 代码(包括 API 控制器和数据模型),然后执行 _CreateClientApi.ps1_。就这些了。WebApiClientGen
和 _CreateClientApi.ps1_ 会为您处理其余的事情。
团队协作
本节介绍了一些基本的团队协作场景。不同公司和团队的情况和背景可能不同,因此您应相应地调整您的团队实践。
您的团队有一位后端开发人员 Brenda 负责 Web API,一位前端开发人员 Frank 负责前端。每个开发机器都已正确设置了集成测试环境,因此大部分 CI 工作可以在每台开发机器上完成,而无需团队 CI 服务器。Trunk-based development 是默认的分支实践。
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 开发新的前端功能。
参考文献
历史
- 2023 年 10 月 17 日:初始版本