This article introduces how to use WebApiClientGen and Code First approach for generating client APIs for ASP.NET Core Web API, in C# and in TypeScript for jQuery, Aurelia, Axios and Angular 2+, without Swagger/OpenApi definitions.
Introduction
For developing client programs of ASP.NET Web API or ASP. NET Core Web API, Strongly Typed Client API Generators generate strongly typed client API in C# codes and TypeScript codes. The toolkit is to minimize repetitive tasks, streamline the coordination between the backend development and the frontend development, and improve the productivity of the dev team and the quality of the product through less efforts, works and stress.
This open source project provides these products:
- Code generator for strongly typed client API in C# supporting desktop, Universal Windows, Android and iOS.
- Code generator for strongly typed client API in TypeScript for jQuery, Angular 2+ and Aurelia, as well as TypeScript/JavaScript applications that use Axios.
- TypeScript CodeDOM, a CodeDOM component for TypeScript, derived from CodeDOM of .NET.
- POCO2TS.exe, a command line program that generates TypsScript interfaces from POCO classes.
- Fonlow.Poco2Ts, a component that generates TypsScript interfaces from POCO classes
This article is focused on generating C# Client API libraries for ASP.NET Core 2.0+. If you are still working on .NET Framework, please check "Generate C# Client API for ASP.NET Web API". For client API libraries in TypeScript, please check the"ASP.NET Web API, Angular 2, TypeScript and WebApiClientGen".
Background
When developing Web apps that need high abstraction and semantic data types, you want that both the APIs and the client APIs could utilize strong data types for improving development productivity and data constraints.
Before developing WebApiClientGen
in 2015, I had searched and tried to find some existing solutions that could release me from crafting repetitive codes so I could focus on building business logic at the client sides. Here's a list of open source projects assisting the development of client programs:
- WADL
- RAML with .NET
- WebApiProxy
- Swashbuckle
- AutoRest
- OData
NSwag had come to the surface in 2016 after the initial release of WebApiClientGen
in November 2015, and had been endorsed by Microsoft. NSwag "combines the functionality of Swashbuckle and AutoRest in one toolchain", however, it is architecturally coupled with Swagger/OpenApi which has some inherent and architectural limitations.
While these solutions could generate strongly typed client codes and reduce repetitive tasks at some degree, I found that none of them could give me all the fluent and efficient programming experiences that I would expect:
- Strongly typed client data models mapping to the data models of the ASP.NET Web service
- Strongly typed function prototypes mapping to the functions of derived classes of
ApiController
- Code generations in the wholesale style like the way of WCF SvcUtils, thus least overhead during SDLC
- Cherry-picking data models through data annotations using popular .NET attributes like
DataContractAttribute
and JsonObjectAttribute
, etc. - Type checking at design time and compile time
- Intellisense for client data models, function prototypes and doc comments
Here comes WebApiClientGen.
Using the Code
Presumptions
- You have been developing ASP.NET Web API applications, and will be developing client applications running on Windows desktop, Universal Windows, Android or iOS using C# as the primary programming language.
- You and fellow developers prefer high abstraction through strongly typed functions in both the server side and the client side.
- The POCO classes are used by both Web API and Entity Framework Code First, and you may not want to publish all data classes and class members to client programs.
The installation will also install dependent NuGet packages Fonlow.TypeScriptCodeDOMCore
and Fonlow.Poco2TsCore
to the project references.
Step 1: Post NuGet Installation
Step 1.1 Create CodeGenController
In your Web API project, add the following controller (Copy the latest in 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;
public CodeGenController(IApiDescriptionGroupCollectionProvider apiExplorer,
IWebHostEnvironment hostingEnvironment)
{
this.apiExplorer = apiExplorer;
this.webRootPath = hostingEnvironment.WebRootPath;
}
[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
Remarks
The CodeGenController
should be available only during development in the debug build, since the client API should be generated only once for each version of the Web API.
Step 1.2 Make ApiExplorer Become Visible
This is to tell WebApiClientGen
which controllers will be subject to client codes generation.
Opt-out Approach
In Startup.cs, add the highlighted line below:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(
options =>
{
#if DEBUG
options.Conventions.Add
(new Fonlow.CodeDom.Web.
ApiExplorerVisibilityEnabledConvention());
#endif
}
);
Using ApiExplorerVisibilityEnabledConvention
is an opt-out approach to include all controllers except those decorated by ApiExplorerSettingsAttribute
or ApiControllerAttribute
. This is more appropriate if most of your controllers will be subject to generating strongly typed client APIs.
Opt-in Approach
Alternatively, if you prefer opt-in approach, you may use ApiExplorerSettingsAttribute
to decorate a Web API controller, like this one:
[ApiExplorerSettings(IgnoreApi = false)]
[Route("api/[controller]")]
public class HeroesController : ControllerBase
{
Then there's no need to add ApiExplorerVisibilityEnabledConvention
. This is more appropriate if most of your controllers are not subject to generating strongly typed client APIs.
Step 2: Create .NET Core Client API Project
Hints
If you are sure that System.Text.Json can handle all your scenarios of strongly data types on both the server and the .NET clients, you may set UseSystemTextJson to true
in codegen.json, so you don't need Newtonsoft.Json.
Step 3: Prepare JSON Config Data
Your Web API project may have POCO classes and API functions like the ones below:
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)]
[System.ComponentModel.DataAnnotations.Required]
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
{
[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);
}
The JSON config data is like this:
{
"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"
}
]
}
}
It is recommended to save the JSON payload into a file as illustrated in this screenshot:
Hints
The ExcludedControllerNames
property will exclude those controllers that are already visible to ApiExplorer, alternatively controllers decorated by [ApiExplorerSettings(IgnoreApi = true)]
won't be visible to ApiExplorer
.
StringAsString
is an option for .NET Core Web API which will return text/plain string
by default, rather than application/json JSON object, so the client codes generated won't deserialize the response body of respective Web API function.
Step 4: Run the DEBUG Build of the Web API Project and POST JSON Config Data to Trigger the Generation of Client API Codes
During development, you have two ways of launching the Web API within the VS solution folder.
DotNet
In command prompt, CD to a folder like C:\VSProjects\MySln\DemoCoreWeb\bin\Debug\netcoreapp3.0, then run:
dotnet democoreweb.dll
or just run democoreweb.exe.
IIS Express
Run the Web project in the VS IDE, IIS Express will be launched to host the Web app.
Remarks
Different hostings of the Web app may result in different Web root path, so you may need to adjust the JSON config data accordingly for the folders.
You may create and run a PowerShell file to launch the Web service and POST:
cd $PSScriptRoot
$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 = 'http://localhost:5000/api/codegen'
Method = 'Post'
InFile = "$PSScriptRoot\DemoCoreWeb\CodeGen.json"
ContentType = 'application/json'
}
Invoke-RestMethod @restArgs
Stop-Process $process
Publish Client API Libraries
After these steps, now you have the client API in C# generated to a file named as WebApiClientAuto.cs, similar to this example:
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;
}
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);
}
}
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);
}
}
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
After the initial setup, every time you have made some changes to the interfaces of the Web APIs, you just need to:
- Build the DEBUG build of the Web API.
- Run CreateClientApi.ps1 which launches dotnet Kestrel Web server or IIS Express.
- Build and run your client integration tests.
The following sequence diagram illustrates the interactions of programmers and the automatic steps.
Teamwork
This section describes some basic scenarios of teamwork. Situations and contexts may vary in different companies and teams, thus you shall tune your team practices accordingly.
Your team has a backend developer Brenda working on the Web API, and a frontend developer Frank working on the frontend. Each development machine has the integration testing environment properly setup, so most CI works could be done on each development machine without the team CI server. Trunk base development is the default branching practice. If you are not using TBD but Git Flow or other branching strategy, it shouldn't be hard for you to adjust.
1 Repository Including Backend Codes and Frontend Codes
- Brenda wrote some new Web API codes, and build.
- Brenda executes CreateClientApi.ps1 to generate client codes.
- Brenda writes and runs some basic integration test cases against the Web API.
- Brenda commits/pushes the changes to the main development branch or the trunk.
- Frank updates/pulls the changes, builds, and runs the test cases.
- Frank develops new frontend features based on the new Web APIs and client APIs.
1 Backend Repository and 1 Frontend Repository
Brenda adjusted CodeGen.json that will direct the generated codes to the client API folders in the working folder of the frontend repository.
- Brenda wrote some new Web API codes, and build.
- Brenda executes CreateClientApi.ps1 to generate client codes.
- Brenda writes and runs some basic integration test cases against the Web API.
- Brenda commits/pushes the changes to the main development branch or the trunk of both repositories.
- Frank updates/pulls the changes with both repositories, builds, and runs the test cases.
- Frank develops new frontend features based on the new Web APIs and client APIs.
Points of Interests
Controller and ApiController of ASP.NET, and Controller and ControllerBase of ASP.NET Core
In the old days before ASP.NET Web API, programmers had to use a MVC controller to create JSON-based Web API. Then Microsoft had created ASP.NET Web API, so programmers have been using System.Web.Http.ApiController
ever since. Now with ASP.NET Core, programmers use Microsoft.AspNetCore.Mvc.ControllerBase
or Microsoft.AspNetCore.Mvc.Controller
for creating Web APIs, while ControllerBase
supports Web API only and Controller
supports both Web API and MVC view.
Nevertheless, it may be wise not to mix API functions and View
functions in the same Controller
derived class.
Handling String in the HTTP Response
In ASP.NET Web API, if a Web API function returns a string
, the response body is always a JSON object, unless you provide a custom made formatter that returns string
as string
. In .NET Core Web API, such API function will by default return a string as a string in the response body, unless the client HTTP request provides an accept header "application/json
". When providing "StringAsString" : true
in the CodeGen JSON config, the client codes generated won't deserialize the response body of respective Web API function, and obviously this is more efficient if the Web API function will return a large string
.
About NuGet for .NET Core
Presumably, you have read "Generate C# Client API for ASP.NET Web API". When importing NuGet package Fonlow.WebApiClientGen, installing the NuGet package could copy CodeGenController
and other files to the Web project. However, for .NET Core Web project, Fonlow.WebApiClientGenCore could copy only the assemblies. Rick Strahl has explained well at:
.NET SDK Projects - No more Content and Tools
WebApiClientGen vs Swagger
OpenApiClientGen
OpenApiClientGen
is based on Fonlow.TypeScriptCodeDomCore
and Fonlow.Poco2TsCore
which are core components of WebApiClientGen, thus the codes generated share similar characteristics.
Comparison with NSwag
When using Web services provided by the other vendors with Swagger/OpenAPI definitions, you may try OpenApiClientGen
.
If you are doing full stack development of both Web service and client programs, you don't need Swagger/OpenAPI unless you want to provide client APIs to other companies some of which are using technical stacks that WebApiClientGen
does not support.
References