OpenApiClientGen,用于基于 Open API 定义生成强类型客户端 API 代码
引言
OpenAPI Client Generators 是一个.NET Core命令行程序,用于在.NET Frameworks和.NET Core中生成C#强类型客户端API代码,以及为Angular 5+、Aurelia、jQuery、AXIOS和Fetch API生成TypeScript代码。
假定您已经具备Swagger/Open API规范以及相应客户端应用程序开发的丰富知识和经验。
OpenApiClientGen 是.NET应用程序开发人员通常使用的以下工具之外的一个替代解决方案:
Swashbuckle.AspNetCore
加上NSwagStudio
,如 Microsoft Docs 中所述。- OpenAPITools / openapi-generator
- ...
这些都是不错的工具,覆盖的范围比OpenApiClientGen
更广。本文旨在概述它们之间的区别,以便您在合适的场景下选择正确的工具。
背景
OpenApiClientGen
的开发基于 WebApiClientGen 的一些核心组件,并共享相同的基本设计原则。因此,生成的客户端代码也具有相同的特性。
使用代码
如何生成
当不带参数运行Fonlow.OpenApiClientGen.exe时,您将看到以下提示:
Parameter 1: Open API YAML/JSON definition file
Parameter 2: Settings file in JSON format.
Example:
Fonlow.OpenApiClientGen.exe my.yaml
Fonlow.OpenApiClientGen.exe my.yaml myproj.json
Fonlow.OpenApiClientGen.exe my.yaml ..\myproj.json</code>
典型的CodeGen JSON文件如下所示: "DemoCodeGen.json"
{
"ClientNamespace": "My.Pet.Client",
"ClientLibraryProjectFolderName": "./Tests/DemoClientApi",
"ContainerClassName": "PetClient",
"ClientLibraryFileName": "PetAuto.cs",
"ActionNameStrategy": 4,
"UseEnsureSuccessStatusCodeEx": true,
"DecorateDataModelWithDataContract": true,
"DataContractNamespace": "http://pet.domain/2020/03",
"DataAnnotationsEnabled": true,
"DataAnnotationsToComments": true,
"HandleHttpRequestHeaders": true,
"Plugins": [
{
"AssemblyName": "Fonlow.OpenApiClientGen.NG2",
"TargetDir": "./ng2/src/clientapi",
"TSFile": "ClientApiAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
}
]
}
基于 pet.yaml 生成的 C# 代码示例
/// <summary>
/// A representation of a cat
/// </summary>
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Cat : Pet
{
/// <summary>
/// The measured skill for hunting
/// </summary>
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="huntingSkill")]
public CatHuntingSkill HuntingSkill { get; set; } = CatHuntingSkill.lazy;
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public enum CatHuntingSkill
{
[System.Runtime.Serialization.EnumMemberAttribute()]
clueless = 0,
[System.Runtime.Serialization.EnumMemberAttribute()]
lazy = 1,
[System.Runtime.Serialization.EnumMemberAttribute()]
adventurous = 2,
[System.Runtime.Serialization.EnumMemberAttribute()]
aggressive = 3,
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Category
{
/// <summary>
/// Category ID
/// </summary>
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
/// <summary>
/// Category name
/// Min length: 1
/// </summary>
[System.Runtime.Serialization.DataMember(Name="name")]
[System.ComponentModel.DataAnnotations.StringLength(int.MaxValue, MinimumLength=1)]
public string Name { get; set; }
/// <summary>
/// Test Sub Category
/// </summary>
[System.Runtime.Serialization.DataMember(Name="sub")]
public CategorySub Sub { get; set; }
}
public class CategorySub
{
/// <summary>
/// Dumb Property
/// </summary>
[System.Runtime.Serialization.DataMember(Name="prop1")]
public string Prop1 { get; set; }
}
/// <summary>
/// A representation of a dog
/// </summary>
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Dog : Pet
{
/// <summary>
/// The size of the pack the dog is from
/// Minimum: 1
/// </summary>
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="packSize")]
[System.ComponentModel.DataAnnotations.Range(1, System.Int32.MaxValue)]
public int PackSize { get; set; } = 1;
}
/// <summary>
/// A representation of a honey bee
/// </summary>
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class HoneyBee : Pet
{
/// <summary>
/// Average amount of honey produced per day in ounces
/// </summary>
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="honeyPerDay")]
public float HoneyPerDay { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Order
{
/// <summary>
/// Order ID
/// </summary>
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
/// <summary>
/// Pet ID
/// </summary>
[System.Runtime.Serialization.DataMember(Name="petId")]
public System.Nullable<System.Int64> PetId { get; set; }
/// <summary>
/// Minimum: 1
/// </summary>
[System.Runtime.Serialization.DataMember(Name="quantity")]
[System.ComponentModel.DataAnnotations.Range(1, System.Int32.MaxValue)]
public System.Nullable<System.Int32> Quantity { get; set; }
/// <summary>
/// Estimated ship date
/// </summary>
[System.Runtime.Serialization.DataMember(Name="shipDate")]
public System.Nullable<System.DateTimeOffset> ShipDate { get; set; }
/// <summary>
/// Order Status
/// </summary>
[System.Runtime.Serialization.DataMember(Name="status")]
public OrderStatus Status { get; set; }
/// <summary>
/// Indicates whenever order was completed or not
/// </summary>
[System.Runtime.Serialization.DataMember(Name="complete")]
public System.Nullable<System.Boolean> Complete { get; set; }
/// <summary>
/// Unique Request Id
/// </summary>
[System.Runtime.Serialization.DataMember(Name="requestId")]
public string RequestId { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public enum OrderStatus
{
[System.Runtime.Serialization.EnumMemberAttribute()]
placed = 0,
[System.Runtime.Serialization.EnumMemberAttribute()]
approved = 1,
[System.Runtime.Serialization.EnumMemberAttribute()]
delivered = 2,
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Pet
{
/// <summary>
/// Pet ID
/// </summary>
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
/// <summary>
/// Categories this pet belongs to
/// </summary>
[System.Runtime.Serialization.DataMember(Name="category")]
public Category Category { get; set; }
/// <summary>
/// The name given to a pet
/// </summary>
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="name")]
public string Name { get; set; }
/// <summary>
/// The list of URL to a cute photos featuring pet
/// Maximum items: 20
/// </summary>
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="photoUrls")]
[System.ComponentModel.DataAnnotations.MaxLength(20)]
public string[] PhotoUrls { get; set; }
[System.Runtime.Serialization.DataMember(Name="friend")]
public Pet Friend { get; set; }
/// <summary>
/// Tags attached to the pet
/// Minimum items: 1
/// </summary>
[System.Runtime.Serialization.DataMember(Name="tags")]
[System.ComponentModel.DataAnnotations.MinLength(1)]
public Tag[] Tags { get; set; }
/// <summary>
/// Pet status in the store
/// </summary>
[System.Runtime.Serialization.DataMember(Name="status")]
public PetStatus Status { get; set; }
/// <summary>
/// Type of a pet
/// </summary>
[System.Runtime.Serialization.DataMember(Name="petType")]
public string PetType { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Tag
{
/// <summary>
/// Tag ID
/// </summary>
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
/// <summary>
/// Tag name
/// Min length: 1
/// </summary>
[System.Runtime.Serialization.DataMember(Name="name")]
[System.ComponentModel.DataAnnotations.StringLength(int.MaxValue, MinimumLength=1)]
public string Name { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public enum PetStatus
{
[System.Runtime.Serialization.EnumMemberAttribute()]
available = 0,
[System.Runtime.Serialization.EnumMemberAttribute()]
pending = 1,
[System.Runtime.Serialization.EnumMemberAttribute()]
sold = 2,
}
public partial class PetClient
{
private System.Net.Http.HttpClient client;
private JsonSerializerSettings jsonSerializerSettings;
public PetClient(System.Net.Http.HttpClient client,
JsonSerializerSettings jsonSerializerSettings=null)
{
if (client == null)
throw new ArgumentNullException("Null HttpClient.", "client");
if (client.BaseAddress == null)
throw new ArgumentNullException("HttpClient has no BaseAddress", "client");
this.client = client;
this.jsonSerializerSettings = jsonSerializerSettings;
}
/// <summary>
/// Add a new pet to the store
/// Add new pet to the store inventory.
/// AddPet pet
/// </summary>
/// <param name="requestBody">Pet object that needs to be added to the store</param>
public async Task AddPetAsync(Pet requestBody,
Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "pet";
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri))
{
using (var requestWriter = new System.IO.StringWriter())
{
var requestSerializer = JsonSerializer.Create(jsonSerializerSettings);
requestSerializer.Serialize(requestWriter, requestBody);
var content = new StringContent(requestWriter.ToString(),
System.Text.Encoding.UTF8, "application/json");
httpRequestMessage.Content = content;
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await client.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
}
finally
{
responseMessage.Dispose();
}
}
}
}
/// <summary>
/// Update an existing pet
/// UpdatePet pet
/// </summary>
/// <param name="requestBody">Pet object that needs to be added to the store</param>
public async Task UpdatePetAsync(Pet requestBody,
Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "pet";
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, requestUri))
{
using (var requestWriter = new System.IO.StringWriter())
{
var requestSerializer = JsonSerializer.Create(jsonSerializerSettings);
requestSerializer.Serialize(requestWriter, requestBody);
var content = new StringContent
(requestWriter.ToString(), System.Text.Encoding.UTF8, "application/json");
httpRequestMessage.Content = content;
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await client.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
}
finally
{
responseMessage.Dispose();
}
}
}
}
/// <summary>
/// Find pet by ID
/// Returns a single pet
/// GetPetById pet/{petId}
/// </summary>
/// <param name="petId">ID of pet to return</param>
/// <returns>successful operation</returns>
public async Task<Pet> GetPetByIdAsync
(long petId, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "pet/"+petId;
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri))
{
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await client.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
var stream = await responseMessage.Content.ReadAsStreamAsync();
using (JsonReader jsonReader = new JsonTextReader
(new System.IO.StreamReader(stream)))
{
var serializer = new JsonSerializer();
return serializer.Deserialize<Pet>(jsonReader);
}
}
finally
{
responseMessage.Dispose();
}
}
}
/// <summary>
/// Deletes a pet
/// DeletePet pet/{petId}
/// </summary>
/// <param name="petId">Pet id to delete</param>
public async Task DeletePetAsync
(long petId, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "pet/"+petId;
using (var httpRequestMessage =
new HttpRequestMessage(HttpMethod.Delete, requestUri))
{
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await client.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
}
finally
{
responseMessage.Dispose();
}
}
}
/// <summary>
/// Finds Pets by status
/// Multiple status values can be provided with comma separated strings
/// FindPetsByStatus pet/findByStatus
/// </summary>
/// <param name="status">Status values that need to be considered for filter</param>
/// <returns>successful operation</returns>
public async Task<Pet[]> FindPetsByStatusAsync(PetStatus[] status,
Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
var requestUri = "pet/findByStatus?"+String.Join
("&", status.Select(z => $"status={z}"));
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri))
{
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = await client.SendAsync(httpRequestMessage);
try
{
responseMessage.EnsureSuccessStatusCodeEx();
var stream = await responseMessage.Content.ReadAsStreamAsync();
using (JsonReader jsonReader =
new JsonTextReader(new System.IO.StreamReader(stream)))
{
var serializer = new JsonSerializer();
return serializer.Deserialize<Pet[]>(jsonReader);
}
}
finally
{
responseMessage.Dispose();
}
}
}
正如您可能看到的,生成的代码比其他工具生成的代码看起来更简洁,但提供了相似的数据处理和错误处理能力。更重要的是,数据类型的匹配更加全面和精确。
为 Angular 5+ 生成的 TypeScript 代码示例
import { Injectable, Inject } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
export namespace My_Pet_Client {
export interface ApiResponse {
code?: number;
type?: string;
message?: string;
}
/** A representation of a cat */
export interface Cat extends Pet {
/** The measured skill for hunting */
huntingSkill: CatHuntingSkill;
}
export enum CatHuntingSkill { clueless = 0, lazy = 1, adventurous = 2, aggressive = 3 }
export interface Category {
/** Category ID */
id?: number;
/**
* Category name
* Min length: 1
*/
name?: string;
/** Test Sub Category */
sub?: CategorySub;
}
export interface CategorySub {
/** Dumb Property */
prop1?: string;
}
/** A representation of a dog */
export interface Dog extends Pet {
/**
* The size of the pack the dog is from
* Minimum: 1
*/
packSize: number;
}
/** A representation of a honey bee */
export interface HoneyBee extends Pet {
/** Average amount of honey produced per day in ounces */
honeyPerDay: number;
}
export interface Order {
/** Order ID */
id?: number;
/** Pet ID */
petId?: number;
quantity?: number;
/** Estimated ship date */
shipDate?: Date;
/** Order Status */
status?: OrderStatus;
/** Indicates whenever order was completed or not */
complete?: boolean;
/** Unique Request Id */
requestId?: string;
}
export enum OrderStatus { placed = 0, approved = 1, delivered = 2 }
export interface Pet {
/** Pet ID */
id?: number;
/** Categories this pet belongs to */
category?: Category;
/** The name given to a pet */
name: string;
/**
* The list of URL to a cute photos featuring pet
* Maximum items: 20
*/
photoUrls: Array<string>;
friend?: Pet;
/**
* Tags attached to the pet
* Minimum items: 1
*/
tags?: Array<Tag>;
/** Pet status in the store */
status?: PetStatus;
/** Type of a pet */
petType?: string;
}
export interface Tag {
/** Tag ID */
id?: number;
/**
* Tag name
* Min length: 1
*/
name?: string;
}
export enum PetStatus { available = 0, pending = 1, sold = 2 }
@Injectable()
export class PetClient {
constructor(@Inject('baseUri') private baseUri:
string = location.protocol + '//' + location.hostname +
(location.port ? ':' + location.port : '') + '/', private http: HttpClient) {
}
/**
* Add a new pet to the store
* Add new pet to the store inventory.
* Post pet
* @param {Pet} requestBody Pet object that needs to be added to the store
* @return {void}
*/
AddPet(requestBody: Pet, headersHandler?: () => HttpHeaders):
Observable<HttpResponse<string>> {
return this.http.post(this.baseUri + 'pet',
JSON.stringify(requestBody), { headers: headersHandler ?
headersHandler().append('Content-Type', 'application/json;charset=UTF-8') :
new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }),
observe: 'response', responseType: 'text' });
}
/**
* Update an existing pet
* Put pet
* @param {Pet} requestBody Pet object that needs to be added to the store
* @return {void}
*/
UpdatePet(requestBody: Pet, headersHandler?: () => HttpHeaders):
Observable<HttpResponse<string>> {
return this.http.put(this.baseUri + 'pet', JSON.stringify(requestBody),
{ headers: headersHandler ? headersHandler().append
('Content-Type', 'application/json;charset=UTF-8') :
new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }),
observe: 'response', responseType: 'text' });
}
/**
* Find pet by ID
* Returns a single pet
* Get pet/{petId}
* @param {number} petId ID of pet to return
* @return {Pet} successful operation
*/
GetPetById(petId: number, headersHandler?: () => HttpHeaders): Observable<Pet> {
return this.http.get<Pet>(this.baseUri + 'pet/' + petId,
{ headers: headersHandler ? headersHandler() : undefined });
}
/**
* Deletes a pet
* Delete pet/{petId}
* @param {number} petId Pet id to delete
* @return {void}
*/
DeletePet(petId: number, headersHandler?: () => HttpHeaders):
Observable<HttpResponse<string>> {
return this.http.delete(this.baseUri + 'pet/' + petId,
{ headers: headersHandler ? headersHandler() : undefined,
observe: 'response', responseType: 'text' });
}
/**
* Finds Pets by status
* Multiple status values can be provided with comma separated strings
* Get pet/findByStatus
* @param {Array<PetStatus>} status Status values that need to be
* considered for filter
* @return {Array<Pet>} successful operation
*/
FindPetsByStatus(status: Array<PetStatus>, headersHandler?: () =>
HttpHeaders): Observable<Array<Pet>> {
return this.http.get<Array<Pet>>(this.baseUri + 'pet/findByStatus?' +
status.map(z => `status=${z}`).join('&'),
{ headers: headersHandler ? headersHandler() : undefined });
}
优化了什么
强类型的API和客户端API
强类型的Web API为具有复杂语义建模和工作流程的业务应用程序提供了更好的支持。客户端API通过强类型数据更好地反映了这些语义建模。与WebApiClientGen
类似,OpenApiClientGen
可以尽可能精确地翻译Open API定义中的组件和数据类型。更多详情,请参阅
远程过程调用
Swagger/Open API规范假定RESTful设计,而WebApiClientGen
及其衍生产品OpenApiClientGen
则针对RPC进行了优化。
从某种意义上说,REST是一种构建RPC的规范或受限方式。对于构建复杂的业务应用程序,REST可能对整体开发有利,但也可能过于技术化,迫使开发人员将高级业务逻辑翻译成REST,而不是直接进行业务领域建模。
“考虑一下软件项目开始采用最新的架构设计潮流,而直到后来才发现系统需求是否需要这种架构。”
参考文献
不支持的内容
不支持的内容是出于设计考虑。本文对此进行了简要说明。
HTTP请求头作为参数
HTTP请求头通常用于两个目的:
- Authorization
- 与业务模型语义无关的相关ID和其他元数据
像NSwag这样的工具会生成包含请求头作为参数的客户端API代码,如果Open API定义将请求头包含为参数的话。如果您喜欢这种方式,可以忽略OpenApiClientGen
,继续使用NSwag或类似工具。
public System.Threading.Tasks.Task<PatientClaimInteractiveResponseType> McpPatientclaiminteractiveGeneralV1Async(PatientClaimInteractiveRequestType body, string authorization, string dhs_auditId, string dhs_subjectId, string dhs_messageId, string dhs_auditIdType, string dhs_correlationId, string dhs_productId, string dhs_subjectIdType)
{
OpenApiClientGen生成了一个 更简洁的函数原型
public async Task<PatientClaimInteractiveResponseType> McpPatientClaimInteractiveGeneralAsync(PatientClaimInteractiveRequestType requestBody, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
没有将请求头作为函数参数,客户端API函数看起来更干净,客户端应用程序代码结构更清晰,您可以更专注于业务逻辑,而不是请求头的技术细节。
对于授权,通常会使用HTTP拦截。而.NET HttpClient、Angular Http服务和JQuery Ajax等都提供了对HTTP拦截的内置支持。
要处理请求头(如果Open API定义了某些请求头),您可以在生成客户端API代码时,在CodeGen.json
中包含"HandleHttpRequestHeaders": true
,这将在客户端API函数的参数列表末尾添加一个回调函数。
其他
请参阅 wiki。
关注点
如果您正在开发ASP.NET Web API或.NET Core Web API,则不需要OpenApiClientGen
,因为WebApiClientGen
可以在不涉及Swagger/Open API规范的情况下为C#、Angular 2+、Aurelia、jQuery、AXIOS和Fetch API生成多组客户端API。您可能会对以下文章更感兴趣:
- 为ASP.NET Web API生成C# .NET客户端API
- 为 ASP.NET Web API 生成 TypeScript 客户端 API
- ASP.NET Web API、Angular2、TypeScript 和 WebApiClientGen
- 为 ASP.NET Core Web API 生成 C# 客户端 API
- WebApiClientGen vs Swashbuckle 加上 NSwag
备注
OpenApiClientGen
的开发始于 WebApiClientGen vs Swashbuckle plus NSwag 的发布之后,而WebApiClientGen
和OpenApiClientGen
共享一些关键组件和设计理念。
该工具已经 针对1000多个Open API定义进行了测试。
让生成的代码通过编译不足以证明其有效性。您不能假设为相应Web API服务生成的客户端API是正确的,除非您 **构建了与Web API服务交互的集成测试套件**。
为其他JavaScript库和框架扩展OpenApiClientGen
与其他Open API / Swagger客户端代码生成器相比,OpenApiClientGen
更易于扩展。如果您的首选JavaScript库和框架未包含在内,您可以像 ClientApiTsNG2FunctionGen 和 ControllersTsNG2ClientApiGen 一样,通过扩展ClientApiTsFunctionGenBase
和ControllersTsClientApiGenBase
来实现。如您所见,每个插件的代码量仅约200行。
历史
- 2020 年 7 月 3 日:初始版本