Generate Strongly Typed Client API Codes Based on Swagger / Open API Definitions in C# on .NET Frameworks and .NET Core, and in TypeScript for Angular 5+, Aurelia, jQuery, AXIOS and Fetch API
Introduction
OpenAPI Client Generators is a .NET Core command line program to generate strongly typed client API codes in C# on .NET Frameworks and .NET Core, and in TypeScript for Angular 5+, Aurelia, jQuery, AXIOS and Fetch API.
It is assumed that you already have decent knowledge and experience in Swagger/Open API specifications and respective client side application development.
OpenApiClientGen is an alternative solution besides the following tools typically used by .NET application developers:
They are decent tools, covering wider spectrums than OpenApiClientGen
. And this article is intent to outline some differences for you to choose the right tool for the right contexts.
Background
The development of OpenApiClientGen
is based on some core components of WebApiClientGen, and sharing the same principle design. Thus, the generated client codes share the same characteristics.
Using the code
How to Generate
When running Fonlow.OpenApiClientGen.exe without parameter, you will see the following hints:
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>
A typical CodeGen JSON file is like this "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"
}
]
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Cat : Pet
{
[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
{
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
[System.Runtime.Serialization.DataMember(Name="name")]
[System.ComponentModel.DataAnnotations.StringLength(int.MaxValue, MinimumLength=1)]
public string Name { get; set; }
[System.Runtime.Serialization.DataMember(Name="sub")]
public CategorySub Sub { get; set; }
}
public class CategorySub
{
[System.Runtime.Serialization.DataMember(Name="prop1")]
public string Prop1 { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Dog : Pet
{
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="packSize")]
[System.ComponentModel.DataAnnotations.Range(1, System.Int32.MaxValue)]
public int PackSize { get; set; } = 1;
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class HoneyBee : Pet
{
[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
{
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
[System.Runtime.Serialization.DataMember(Name="petId")]
public System.Nullable<System.Int64> PetId { get; set; }
[System.Runtime.Serialization.DataMember(Name="quantity")]
[System.ComponentModel.DataAnnotations.Range(1, System.Int32.MaxValue)]
public System.Nullable<System.Int32> Quantity { get; set; }
[System.Runtime.Serialization.DataMember(Name="shipDate")]
public System.Nullable<System.DateTimeOffset> ShipDate { get; set; }
[System.Runtime.Serialization.DataMember(Name="status")]
public OrderStatus Status { get; set; }
[System.Runtime.Serialization.DataMember(Name="complete")]
public System.Nullable<System.Boolean> Complete { get; set; }
[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
{
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
[System.Runtime.Serialization.DataMember(Name="category")]
public Category Category { get; set; }
[System.ComponentModel.DataAnnotations.Required()]
[System.Runtime.Serialization.DataMember(Name="name")]
public string Name { get; set; }
[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; }
[System.Runtime.Serialization.DataMember(Name="tags")]
[System.ComponentModel.DataAnnotations.MinLength(1)]
public Tag[] Tags { get; set; }
[System.Runtime.Serialization.DataMember(Name="status")]
public PetStatus Status { get; set; }
[System.Runtime.Serialization.DataMember(Name="petType")]
public string PetType { get; set; }
}
[System.Runtime.Serialization.DataContract(Name="http://pet.domain/2020/03")]
public class Tag
{
[System.Runtime.Serialization.DataMember(Name="id")]
public System.Nullable<System.Int64> Id { get; set; }
[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;
}
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();
}
}
}
}
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();
}
}
}
}
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();
}
}
}
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();
}
}
}
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();
}
}
}
As you may see, the generated codes are looking simpler than what was generated by other tools while providing similar capacity of data handling and error handling. More importantly, the data types matching is more comprehensive and precise.
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;
}
export interface Cat extends Pet {
huntingSkill: CatHuntingSkill;
}
export enum CatHuntingSkill { clueless = 0, lazy = 1, adventurous = 2, aggressive = 3 }
export interface Category {
id?: number;
name?: string;
sub?: CategorySub;
}
export interface CategorySub {
prop1?: string;
}
export interface Dog extends Pet {
packSize: number;
}
export interface HoneyBee extends Pet {
honeyPerDay: number;
}
export interface Order {
id?: number;
petId?: number;
quantity?: number;
shipDate?: Date;
status?: OrderStatus;
complete?: boolean;
requestId?: string;
}
export enum OrderStatus { placed = 0, approved = 1, delivered = 2 }
export interface Pet {
id?: number;
category?: Category;
name: string;
photoUrls: Array<string>;
friend?: Pet;
tags?: Array<Tag>;
status?: PetStatus;
petType?: string;
}
export interface Tag {
id?: number;
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) {
}
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' });
}
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' });
}
GetPetById(petId: number, headersHandler?: () => HttpHeaders): Observable<Pet> {
return this.http.get<Pet>(this.baseUri + 'pet/' + petId,
{ headers: headersHandler ? headersHandler() : undefined });
}
DeletePet(petId: number, headersHandler?: () => HttpHeaders):
Observable<HttpResponse<string>> {
return this.http.delete(this.baseUri + 'pet/' + petId,
{ headers: headersHandler ? headersHandler() : undefined,
observe: 'response', responseType: 'text' });
}
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 });
}
Optimized for What
Strongly Typed API and Client API
Strongly typed Web API provides better supports for business applications with complex semantic modelings and workflows. And client APIs have better to reflect such semantic modelings through strongly typed data. Similar to what WebApiClientGen
can do, OpenApiClientGen
can translate components and data types in Open API definitions as precise as possible. For more details, please check:
Remote Procedure Call
Swagger/Open API Specifications assumes RESTful designs, while WebApiClientGen
and its spin-off OpenApiClientGen
is optimized for RPC.
From a certain point of view, REST is a disciplined or constrained way of building RPC. For building complex business applications, REST may be beneficial to overall development, or may be too technical and forcing developers to translate high level business logic into REST, rather than to work on business domain modeling more directly.
“Consider how often we see software projects begin with adoption of the latest fad in architectural design, and only later discover whether or not the system requirements call for such an architecture.”
References
What Not Supported
What not supported is by design. This article explains this briefly.
HTTP Request Headers as Parameters
HTTP request headers are often for 2 purposes:
- Authorization
- Correlation ID and other meta data not semantically related to business models
Tools like NSwag will generated client API codes with function/operation parameters including headers if the Open API definition includes headers as parameters. If you like this, you may ignore OpenApiClientGen
and continue to use NSwag or alike.
Upon mcp.yaml, NSwag generates such function prototype:
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 generates a simpler function prototype:
public async Task<PatientClaimInteractiveResponseType> McpPatientClaimInteractiveGeneralAsync(PatientClaimInteractiveRequestType requestBody, Action<System.Net.Http.Headers.HttpRequestHeaders> handleHeaders = null)
{
Without headers as function parameters, the client API functions look cleaner, the client application codes look more clearly structural, and you may be focused more on business logic rather than technical details of headers.
For authorization, typically you would use HTTP interception. And .NET HttpClient, Angular Http service and JQuery Ajax, etc. provide built-in support for HTTP interception.
For handling request headers if the Open API definition defines some, you may include "HandleHttpRequestHeaders": true
in CodeGen.json
when generating client API codes, and this will put a callback function at the end of the parameter list of the client API functions.
Others
Please check wiki.
Points of Interest
If you are developing ASP.NET Web API or .NET Core Web API, you don't need OpenApiClientGen
because WebApiClientGen
can generate a few sets of client APIs for C#, Angular 2+, Aurelia, jQuery, AXIOS and Fetch API without involving Swagger/Open API Specification. And you may be more interested in the following articles:
- Generate C# .NET Client API for ASP.NET Web API
- Generate TypeScript Client API for ASP.NET Web API
- ASP.NET Web API, Angular2, TypeScript and WebApiClientGen
- Generate C# Client API for ASP.NET Core Web API
- WebApiClientGen vs Swashbuckle plus NSwag
Remarks
The development of OpenApiClientGen
had been started after the publishing of WebApiClientGen vs Swashbuckle plus NSwag while WebApiClientGen
and OpenApiClientGen
share some key components and design concepts.
This tool has been tested with over 1000 Open API definitions.
Having the generated codes to pass compilation proves the generated codes is not enough. And you cannot assume the generated client API for a respective Web API service is correct without building integration test suites which talk to the Web API service.
Extending OpenApiClientGen for Other JavaScript Libraries and Frameworks
Comparing with other client codes generators for Open API / Swagger, OpenApiClientGen
is much easier to extend. If your favorite JavaScript libraries and frameworks are not included, you may extend ClientApiTsFunctionGenBase
like ClientApiTsNG2FunctionGen and ControllersTsClientApiGenBase
like ControllersTsNG2ClientApiGen. As you can see, each of the plugins involves around 200 lines of codes specifically.
History
- 3rd July, 2020: Initial version