Introduction
This article compares Strongly Typed Client API Generators with Swagger toolchains in the .NET landscapes, so you could choose the right tools for the right contexts. It is presumed that you have experience in Swagger toolchains and you have read at least one of the following articles:
While Swagger toolchains are mostly and primarily for meta first approach, there are tools supporting code first approaches, that is, the server side tools generate Swagger definition files and the client tools generate codes based on the definitions, while WebApiClientGen
generates client codes directly on the server side during the service development. And this article is focused on the code first approach, specifically with Swashbuckle.AspNetCore
plus NSwagStudio
, since these two are promoted in Microsoft Docs.
C# Clients
As its name had suggested, Strongly Typed Client API Generators provide exact data type mappings between server and C# clients, as precise as possible.
Swashbuckle+NSwag Does Not Support
- User defined struct
- Object
- dynamic
- Generic
- Namespace
- Enum
Remarks
- Swashbuckle translates server side
struct System.Drawing.Point
to client side class Point
. - Open API and NSwag supports inheritance, however Swashbuckle's support for inheritance is poor, as of
Swashbuckle.AspNetCore
5.0. - Open API and NSwag provide limited supports for
enum
, however, Swashbuckle supports even less. - NSwag does support namespace and enum, however, not worrking well with the Swagger definition file generated by Swashbuckle.AspNet Core 5.0.
Swashbuckle+NSwag Provides Imprecise Data Type Mappings for the Following Types
Decimal
==> double
Nullable<T>
==> T
float
==>double
uint
, short
, byte
==> int
ulong
==> long
char
==> string
Tuple
==> Generated user defined type with similar structure to Tuple
int[,]
==> ICollection<object>
int[][]
==> ICollection<int>
KeyValuePair
==> Generated user defined type with similar structure to KeyValuePair
NSwag Generates Verbose, Larger and Complex Codes
In the sln of SwaggerDemo
, Core3WebApi
is with WebApiClientGen
, and SwaggerDemo
is with Swashbuckle.AspNetCore
for creating an Open API definition. When generating async functions only, codes generated by WebApiClientGen
is 97KB, along with debug build 166KB and release build 117KB, while Swagger's NSwagStudio
gives 489KB-495KB, along with debug build 340KB-343KB and release build 263KB-283KB. There might be good reasons why NSwag generates complex codes, and you may inspect and compare to see whether such complexity is needed in your project content and contexts.
NSwag Yields Verbose GeneratedCodeAttribute
According to this, the GeneratedCodeAttribute
class can be used by code analysis tools to identify computer-generated code, and to provide an analysis based on the tool and the version of the tool that generated the code.
It is a good practice to put generated codes into a dedicated assembly with generated codes only. Thus an application programmer may simply exclude the assembly from code analysis tools. Therefore, GeneratedCodeAttribute
is not necessary in the generated codes.
TypeScript Clients for JavaScript Libraries or Frameworks
WebApiClientGen
- jQuery with callbacks
- Angular 2+
- Axios
- Aurelia
- Fetch API
NSwag
- JQuery with Callbacks
- JQuery with promises
- Angular (v2+) using the http service
window.fetch
API and ES6 promises - Aurelia using the
HttpClient
from aurelia-fetch-client
Axios
(preview)
How is WebApiClientGen Superior to Swagger?
For generating C# clients, WebApiClientGen
supports more .NET built-in data types and gives more exact data type mappings. Exact type mappings make client programming much easier for high quality since the integration tests should pick up data out of range easily because of proper type constraints.
Smaller codes and smaller compiled images are always welcome.
The manual steps of generating client codes is less and faster.
Remarks
Swashbuckle.AspNetCore
does not support types with the same name but in different namespaces. In complex business applications, there may be custom data types with the same names in different namespaces.
How is Swagger Superior to WebApiClientGen?
Swagger here means the Open API standard and respective toolchains.
Swagger is an open standard and platform neutral, being supported by major software vendors and developed by hundreds of developers around the world. Microsoft Docs has a dedicated section for Swagger here, and Microsoft has been using Swagger for her own Web API products.
Swagger supports fine grained control over HTTP headers, while WebApiClientGen
ignores this area.
How About Online Help?
Swashbuckle.AspNetCore
provides "a rich, customizable experience for describing the web API functionality".
WebApiClientGen
copies in-source documents of published data types and controller operations to client codes, and decent IDE like Visual Studio could display intellisense along with the in-source documents in the client codes. This minimizes the need for online help.
If you really want online help, you may use Sandcastle for C# client codes, use Compodoc for Angular 2+ client codes, and use TypeDoc for other JavaScript frameworks.
Differences on Preferences
Swagger/Open API is designed for RESTful service, while ASP.NET Web API is designed for RPC which covers RESTful service. And the design preferences of WebApiClientGen
is based on RPC, not REST. 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.
“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
Can WebApiClientGen and Swagger Coexist?
The answer is yes.
The Swagger toolchains and WebApiClientGen
are greatly overlapping in the .NET landscapes, while Swagger covers wider and deeper spectrum, and WebApiClientGen is optimized for SDLC with .NET Framework and .NET Core, as well as strongly typing.
If you are developing ASP.NET (Core) Web API and expect all clients are coded in C# and TypeScript only, WebApiClientGen
gives you more advantages.
When you need to support clients coded in languages other than C# and TypeScript, you may introduce Swashbuckle into your Web API and generate the Open API definition files either in JSON or YAML, then use NSwag or other Swagger/Open API tools for clients.
Perfect SDLC with WebApiClientGen and Swagger
Whenever you as a backend developer have just updated the Web API, you run WebApiClientGen
with a batch file to generate C# client codes and TypeScript client codes for client application developers. And the Swagger endpoint of the Web API gives the Open API definition files, so client application developers working on other languages may generate client API codes in other languages.
So you get the best of WebApiClientGen
and Swagger/Open API
.
Points of Interest
When writing this article, I had done a detailed study on Swagger/Open API Specification since I had done a similar study in 2015 when the WebApiClientGen
project was started. The landscape of generating codes from Swagger had been changed a lot with comprehensive and matured toolchains for a wide variety of server platforms and client platforms. However, existing client codegen tools for C# and TypeScript could not satisfy me, if I have a 3rd party service to consume, which does not provide client libraries but some definition files of Swagger/Open API Specification. It shouldn't be hard to write an alternative to NSwag or Autorest, based on core components of WebApiClientGen
. Here you are: OpenApiClientGen. And the Wiki of this project has pages to compare what generated by NSwag and OpenApiClientGen based on the same set of Swagger/Open API definitions.
Appendixes
The appendixes give you some basic comparisons of codes generated by Swagger and WebApiClientGen
, when you are considering your SDLC and the contexts of your SDLC.
Type Person in Services
[DataContract(Namespace = Constants.DataNamespace)]
public class Entity
{
public Entity()
{
Addresses = new List<Address>();
}
[DataMember]
public Guid Id { get; set; }
[DataMember(IsRequired =true)]
[System.ComponentModel.DataAnnotations.Required]
public string Name { get; set; }
[DataMember]
public IList<Address> Addresses { get; set; }
[DataMember]
public virtual ObservableCollection<PhoneNumber> PhoneNumbers { get; set; }
public override string ToString()
{
return Name;
}
[DataMember]
public Uri Web { get; set; }
}
[DataContract(Namespace = Constants.DataNamespace)]
public class Person : Entity
{
[DataMember]
public string Surname { get; set; }
[DataMember]
public string GivenName { get; set; }
[DataMember]
public DateTime? DOB { get; set; }
public override string ToString()
{
return Surname + ", " + GivenName;
}
}
C# Client Codes Generated by NSwagStudio
[System.CodeDom.Compiler.GeneratedCode
("NJsonSchema", "10.1.4.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Entity
{
[Newtonsoft.Json.JsonProperty("id",
Required = Newtonsoft.Json.Required.DisallowNull,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Guid Id { get; set; }
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("addresses",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Collections.Generic.ICollection<Address> Addresses { get; set; }
[Newtonsoft.Json.JsonProperty("phoneNumbers",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Collections.Generic.ICollection<PhoneNumber> PhoneNumbers { get; set; }
[Newtonsoft.Json.JsonProperty("web",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Uri Web { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode
("NJsonSchema", "10.1.4.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Person
{
[Newtonsoft.Json.JsonProperty("surname",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Surname { get; set; }
[Newtonsoft.Json.JsonProperty("givenName",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string GivenName { get; set; }
[Newtonsoft.Json.JsonProperty("dob",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.DateTimeOffset? Dob { get; set; }
[Newtonsoft.Json.JsonProperty("id",
Required = Newtonsoft.Json.Required.DisallowNull,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Guid Id { get; set; }
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
[System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
public string Name { get; set; }
[Newtonsoft.Json.JsonProperty("addresses",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Collections.Generic.ICollection<Address> Addresses { get; set; }
[Newtonsoft.Json.JsonProperty("phoneNumbers",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Collections.Generic.ICollection<PhoneNumber> PhoneNumbers { get; set; }
[Newtonsoft.Json.JsonProperty("web",
Required = Newtonsoft.Json.Required.Default,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.Uri Web { get; set; }
}
C# Client Codes Generated by WebApiClientGen
public class Entity : object
{
public DemoWebApi.DemoData.Client.Address[] Addresses { get; set; }
public System.Guid Id { get; set; }
[System.ComponentModel.DataAnnotations.RequiredAttribute()]
public string Name { get; set; }
public DemoWebApi.DemoData.Client.PhoneNumber[] PhoneNumbers { get; set; }
}
public class Person : DemoWebApi.DemoData.Client.Entity
{
public System.Nullable<System.DateTime> DOB { get; set; }
public string GivenName { get; set; }
public string Surname { get; set; }
}
TypeScript Client Codes Generated by NSwagStudio
export interface IEntity {
id?: string;
name: string;
addresses?: Address[] | undefined;
phoneNumbers?: PhoneNumber[] | undefined;
web?: string | undefined;
}
interface IPerson {
surname?: string | undefined;
givenName?: string | undefined;
dob?: Date | undefined;
id?: string;
name: string;
addresses?: Address[] | undefined;
phoneNumbers?: PhoneNumber[] | undefined;
web?: string | undefined;
}
TypeScript Client Codes Generated by WebApiClientGen
export interface Entity {
addresses?: Array<DemoWebApi_DemoData_Client.Address>;
id?: string;
name: string;
phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
web?: string;
}
export interface Person extends DemoWebApi_DemoData_Client.Entity {
dob?: Date;
givenName?: string;
surname?: string;
}
Type enum PhoneType and Days
[DataContract(Namespace = Constants.DataNamespace)]
public enum PhoneType
{
[EnumMember]
Tel = 0,
[EnumMember]
Mobile = 1,
[EnumMember]
Skype = 2,
[EnumMember]
Fax = 3,
}
[DataContract(Namespace = Constants.DataNamespace)]
public enum Days
{
[EnumMember]
Sat = 1,
[EnumMember]
Sun,
[EnumMember]
Mon,
[EnumMember]
Tue,
[EnumMember]
Wed,
[EnumMember]
Thu,
[EnumMember]
Fri
};
C# Client Codes Generated by NSwagStudio
[System.CodeDom.Compiler.GeneratedCode
("NJsonSchema", "10.1.4.0 (Newtonsoft.Json v12.0.0.0)")]
public enum PhoneType
{
_0 = 0,
_1 = 1,
_2 = 2,
_3 = 3,
}
[System.CodeDom.Compiler.GeneratedCode
("NJsonSchema", "10.1.4.0 (Newtonsoft.Json v12.0.0.0)")]
public enum Days
{
_1 = 1,
_2 = 2,
_3 = 3,
_4 = 4,
_5 = 5,
_6 = 6,
_7 = 7,
}
C# Client Code Generated by WebApiClientGen
public enum PhoneType
{
Tel,
Mobile,
Skype,
Fax,
}
public enum Days
{
Sat = 1,
Sun = 2,
Mon = 3,
Tue = 4,
Wed = 5,
Thu = 6,
Fri = 7,
}
TypeScript Client Codes Generated by NSwagStudio
enum PhoneType {
_0 = 0,
_1 = 1,
_2 = 2,
_3 = 3,
}
enum Days {
_1 = 1,
_2 = 2,
_3 = 3,
_4 = 4,
_5 = 5,
_6 = 6,
_7 = 7,
}
TypeScript Client Generated by WebApiClientGen
export enum PhoneType {
Tel,
Mobile,
Skype,
Fax
}
export enum Days {
Sat = 1,
Sun = 2,
Mon = 3,
Tue = 4,
Wed = 5,
Thu = 6,
Fri = 7
}
Controller Operation HeroesController.Get(id)
[HttpGet("{id}")]
public Hero Get(long id)
{...
C# Client Codes Generated by NSwagStudio
public System.Threading.Tasks.Task<Hero> HeroesGetAsync(long id)
{
return HeroesGetAsync(id, System.Threading.CancellationToken.None);
}
public async System.Threading.Tasks.Task<Hero>
HeroesGetAsync(long id, System.Threading.CancellationToken cancellationToken)
{
if (id == null)
throw new System.ArgumentNullException("id");
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') :
"").Append("/api/Heroes/{id}");
urlBuilder_.Replace("{id}", System.Uri.EscapeDataString
(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture)));
var client_ = _httpClient;
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("GET");
request_.Headers.Accept.Add
(System.Net.Http.Headers.
MediaTypeWithQualityHeaderValue.Parse("text/plain"));
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri =
new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_,
System.Net.Http.HttpCompletionOption.ResponseHeadersRead,
cancellationToken).ConfigureAwait(false);
try
{
var headers_ = System.Linq.Enumerable.ToDictionary
(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = ((int)response_.StatusCode).ToString();
if (status_ == "200")
{
var objectResponse_ =
await ReadObjectResponseAsync<Hero>(response_, headers_).
ConfigureAwait(false);
return objectResponse_.Object;
}
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ?
null : await response_.Content.ReadAsStringAsync().
ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response
was not expected (" + (int)response_.StatusCode + ").",
(int)response_.StatusCode, responseData_, headers_, null);
}
return default(Hero);
}
finally
{
if (response_ != null)
response_.Dispose();
}
}
}
finally
{
}
}
C# Client Codes Generated by WebApiClientGen
public async Task<DemoWebApi.Controllers.Client.Hero> GetHeroAsync(long id)
{
var requestUri = new Uri(this.baseUri, "api/Heroes/"+id);
var responseMessage = await client.GetAsync(requestUri);
try
{
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.Controllers.Client.Hero>(jsonReader);
}
}
finally
{
responseMessage.Dispose();
}
}
TypeScript Fetch API Codes Generated by NSwagStudio
heroesGet(id: number): Promise<Hero> {
let url_ = this.baseUrl + "/api/Heroes/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_ = <RequestInit>{
method: "GET",
headers: {
"Accept": "text/plain"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processHeroesGet(_response);
});
}
protected processHeroesGet(response: Response): Promise<Hero> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach)
{ response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
let resultData200 = _responseText === "" ?
null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = Hero.fromJS(resultData200);
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException
("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Hero>(<any>null);
}
TypeScript Fetch API Codes Generated by WebApiClientGen
getHero(id: number): Promise<DemoWebApi_Controllers_Client.Hero> {
return fetch(this.baseUri + 'api/Heroes/' + id,
{method: 'get'}).then(d => d.json());
}
TypeScript Angular API Codes Generated by NSwagStudio
heroesGet(id: number): Observable<Hero> {
let url_ = this.baseUrl + "/api/Heroes/{id}";
if (id === undefined || id === null)
throw new Error("The parameter 'id' must be defined.");
url_ = url_.replace("{id}", encodeURIComponent("" + id));
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Accept": "text/plain"
})
};
return this.http.request("get", url_, options_).pipe
(_observableMergeMap((response_ : any) => {
return this.processHeroesGet(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processHeroesGet(<any>response_);
} catch (e) {
return <Observable<Hero>><any>_observableThrow(e);
}
} else
return <Observable<Hero>><any>_observableThrow(response_);
}));
}
protected processHeroesGet(response: HttpResponseBase): Observable<Hero> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {};
if (response.headers) { for (let key of response.headers.keys())
{ _headers[key] = response.headers.get(key); }};
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ?
null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = Hero.fromJS(resultData200);
return _observableOf(result200);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.",
status, _responseText, _headers);
}));
}
return _observableOf<Hero>(<any>null);
}
TypeScript Angular API Codes Generated by WebApiClientGen
getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero> {
return this.http.get<DemoWebApi_Controllers_Client.Hero>
(this.baseUri + 'api/Heroes/' + id);
}
History
- 24th February, 2020: Initial version