Generate Angular 2+ friendly TypeScript client codes for developing Angular 2+ applications communicating with ASP.NET (Core) Web API. This article also briefly compares WebApiClientGen with Swagger plug NSwag.
Introduction
This article is extending “Generate TypeScript Client API for ASP.NET Web API “ and is focused on Angular 2+ code examples and respective SDLC. If you are developing a .NET Core Web API backend, you may need to read Generate C# Client API for ASP.NET Core Web API.
Background
The support for Angular2 has been available since WebApiClientGen
v1.9.0-beta in June 2016 when Angular 2 was still in RC2. And the support for Angular 2 production release has been available since WebApiClientGen
v2.0.
A few weeks after the first production release of Angular 2 being released at the end of September 2016, I happened to start a major Web application project utilizing Angular2, so I have been using pretty much the same WebApiClientGen
for NG2 application development.
Presumptions
- You are developing ASP.NET Web API 2.x applications or ASP.NET Core applications, and will be developing the TypeScript libraries for a SPA based on Angular 2+.
- You and fellow developers highly prefer abstraction through strongly typed data and functions in both the server side and the client sides.
- The POCO classes are used by both Web API data serialization and Entity Framework Code First, and you may not want to publish all data classes and members to client programs.
And optionally, it is better that you or your team is endorsing Trunk based development, since the design of WebApiClientGen
and the workflow of using WebApiClientGen
were assuming Trunk based development which is more efficient for Continuous Integration than other branching strategies like Feature Branching and GitFlow, etc. for teams skillful at TDD.
For following up this new way of developing client programs, it is better to have an ASP.NET Web API project. You may use an existing project, or create a demo one.
Using the Code
This article is focused on the code examples with Angular 2+. It is presumed you have an ASP.NET Web API project and an Angular2 project sitting as sibling projects in a VS solution. If you have them very separated, it shouldn't be hard for you to write scripts in order to make the steps of development seamless.
I would presume that you have read "Generate TypeScript Client API for ASP.NET Web API". The steps of generating client API for jQuery are almost identical to the ones of generating for Angular 2. And the demo TypeScript codes is based on TUTORIAL: TOUR OF HEROES, from which many people had learned Angular2. So you would be able to see how WebApiClientGen
could fit in and improve typical development cycles of Angular2 apps.
Here are the Web API codes:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Runtime.Serialization;
using System.Collections.Concurrent;
namespace DemoWebApi.Controllers
{
[RoutePrefix("api/Heroes")]
public class HeroesController : ApiController
{
public Hero[] Get()
{
return HeroesData.Instance.Dic.Values.ToArray();
}
public Hero Get(long id)
{
Hero r;
HeroesData.Instance.Dic.TryGetValue(id, out r);
return r;
}
public void Delete(long id)
{
Hero r;
HeroesData.Instance.Dic.TryRemove(id, out r);
}
public Hero Post(string name)
{
var max = HeroesData.Instance.Dic.Keys.Max();
var hero = new Hero { Id = max + 1, Name = name };
HeroesData.Instance.Dic.TryAdd(max + 1, hero);
return hero;
}
public Hero Put(Hero hero)
{
HeroesData.Instance.Dic[hero.Id] = hero;
return hero;
}
[HttpGet]
public Hero[] Search(string name)
{
return HeroesData.Instance.Dic.Values.Where(d => d.Name.Contains(name)).ToArray();
}
}
[DataContract(Namespace = DemoWebApi.DemoData.Constants.DataNamespace)]
public class Hero
{
[DataMember]
public long Id { get; set; }
[DataMember]
public string Name { get; set; }
}
public sealed class HeroesData
{
private static readonly Lazy<HeroesData> lazy =
new Lazy<HeroesData>(() => new HeroesData());
public static HeroesData Instance { get { return lazy.Value; } }
private HeroesData()
{
Dic = new ConcurrentDictionary<long, Hero>(new KeyValuePair<long, Hero>[] {
new KeyValuePair<long, Hero>(11, new Hero {Id=11, Name="Mr. Nice" }),
new KeyValuePair<long, Hero>(12, new Hero {Id=12, Name="Narco" }),
new KeyValuePair<long, Hero>(13, new Hero {Id=13, Name="Bombasto" }),
new KeyValuePair<long, Hero>(14, new Hero {Id=14, Name="Celeritas" }),
new KeyValuePair<long, Hero>(15, new Hero {Id=15, Name="Magneta" }),
new KeyValuePair<long, Hero>(16, new Hero {Id=16, Name="RubberMan" }),
new KeyValuePair<long, Hero>(17, new Hero {Id=17, Name="Dynama" }),
new KeyValuePair<long, Hero>(18, new Hero {Id=18, Name="Dr IQ" }),
new KeyValuePair<long, Hero>(19, new Hero {Id=19, Name="Magma" }),
new KeyValuePair<long, Hero>(20, new Hero {Id=29, Name="Tornado" }),
});
}
public ConcurrentDictionary<long, Hero> Dic { get; private set; }
}
}
The installation will also install dependent NuGet packages Fonlow.TypeScriptCodeDOM
and Fonlow.Poco2Ts
to the project references.
Additionally, CodeGenController.cs for triggering the CodeGen is added to the Web API project's Controllers folder.
The CodeGenController
should be available only during development in the debug build, since the client API should be generated once for each version of the Web API.
Hints
- If you are using the Http service of Angular2 defined in @angular/http, you should be using
WebApiClientGen
v2.2.5. If you are using the HttpClient
service available in Angular 4.3 defined in @angular/common/http and deprecated in Angular 5, you should be using WebApiClientGen
v2.3.0. - WebApiClientGen 3+ along with WebApiClientGen.NG2 supports Angular 6+ only.
Step 1: Prepare JSON Config Data
The JSON config data below is to POST
to the CodeGen Web API:
{
"ApiSelections": {
"ExcludedControllerNames": [
"DemoWebApi.Controllers.Account",
"DemoWebApi.Controllers.FileUpload"
],
"DataModelAssemblyNames": [
"DemoWebApi.DemoData",
"DemoWebApi"
],
"CherryPickingMethods": 3
},
"ClientApiOutputs": {
"CamelCase": true,
"Plugins": [
{
"AssemblyName": "Fonlow.WebApiClientGen.NG2",
"TargetDir": "..\\DemoNGCli\\NGSource\\src\\ClientApi",
"TSFile": "WebApiNG2ClientAuto.ts",
"AsModule": true,
"ContentType": "application/json;charset=UTF-8"
}
]
}
}
Remarks
You should make sure the folder defined by "TypeScriptNG2Folder
" exists, since WebApiClientGen
won't create this folder for you, and this is by design.
It is recommended to save the JSON payload data into a file like this one located in the Web API project folder.
If you have all POCO classes defined in the Web API project, you should put the assembly name of the Web API project to the array of "DataModelAssemblyNames
". If you have some dedicated data model assemblies for good separation of concerns, you should put respective assembly names to the array.
"TypeScriptNG2Folder
" is an absolute path or relative path to the Angular2 project. For example, "..\\DemoAngular2\\ClientApi" indicates an Angular 2 project "DemoAngular2
" created as a sibling project of the Web API project.
The CodeGen
generates strongly typed TypeScript interfaces from POCO classes according to "CherryPickingMethods
" which is described in the doc comment below:
[Flags]
public enum CherryPickingMethods
{
All = 0,
DataContract =1,
NewtonsoftJson = 2,
Serializable = 4,
AspNet = 8,
}
The default one is DataContract
for opt-in. And you may use any or combinations of methods.
Step 2: Run the DEBUG Build of the Web API Project
Step 3: POST JSON Config data to Trigger the Generation of Client API Codes
Run the Web project in IDE on IIS Express.
You then use Curl or Poster or any of your favorite client tools to POST to http://localhost:10965/api/CodeGen, with content-type=application/json
.
It shouldn't be hard for you to write some batch scripts to launch the Web API and POST the JSON config data. And I have actually drafted one for your convenience: a Powershell script file CreateClientApi.ps1 that launches the Web (API) project on IIS Express, then post the JSON config file to trigger the code generation.
So basically, you craft Web API codes including API controllers and data models, and then execute CreateClientApi.ps1. That's it! WebApiClientGen
and CreateClientApi.ps1 will do the rest for you.
Publish Client API Libraries
Now you have the client API in TypeScript generated, similar to this example:
import { Injectable, Inject } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
export namespace DemoWebApi_DemoData_Client {
export enum AddressType {Postal, Residential}
export enum Days {Sat=1, Sun=2, Mon=3, Tue=4, Wed=5, Thu=6, Fri=7}
export interface PhoneNumber {
fullNumber?: string;
phoneType?: DemoWebApi_DemoData_Client.PhoneType;
}
export enum PhoneType {Tel, Mobile, Skype, Fax}
export interface Address {
id?: string;
street1?: string;
street2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
type?: DemoWebApi_DemoData_Client.AddressType;
location?: DemoWebApi_DemoData_Another_Client.MyPoint;
}
export interface Entity {
id?: string;
name: string;
addresses?: Array<DemoWebApi_DemoData_Client.Address>;
phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
}
export interface Person extends DemoWebApi_DemoData_Client.Entity {
surname?: string;
givenName?: string;
dob?: Date;
}
export interface Company extends DemoWebApi_DemoData_Client.Entity {
businessNumber?: string;
businessNumberType?: string;
textMatrix?: Array<Array<string>>;
int2DJagged?: Array<Array<number>>;
int2D?: number[][];
lines?: Array<string>;
}
export interface MyPeopleDic {
dic?: {[id: string]: DemoWebApi_DemoData_Client.Person };
anotherDic?: {[id: string]: string };
intDic?: {[id: number]: string };
}
}
export namespace DemoWebApi_DemoData_Another_Client {
export interface MyPoint {
x: number;
y: number;
}
}
export namespace DemoWebApi_Controllers_Client {
export interface FileResult {
fileNames?: Array<string>;
submitter?: string;
}
export interface Hero {
id?: number;
name?: string;
}
}
@Injectable()
export class Heroes {
constructor(@Inject('baseUri') private baseUri: string = location.protocol + '//' +
location.hostname + (location.port ? ':' + location.port : '') +
'/', private http: Http){
}
get(): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
return this.http.get(this.baseUri + 'api/Heroes').map(response=> response.json());
}
getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero>{
return this.http.get(this.baseUri + 'api/Heroes/'+id).map
(response=> response.json());
}
delete(id: number): Observable<Response>{
return this.http.delete(this.baseUri + 'api/Heroes/'+id);
}
post(name: string): Observable<DemoWebApi_Controllers_Client.Hero>{
return this.http.post(this.baseUri + 'api/Heroes?name='+encodeURIComponent(name),
JSON.stringify(null), { headers: new Headers({ 'Content-Type':
'text/plain;charset=UTF-8' }) }).map(response=> response.json());
}
put(hero: DemoWebApi_Controllers_Client.Hero):
Observable<DemoWebApi_Controllers_Client.Hero>{
return this.http.put(this.baseUri + 'api/Heroes', JSON.stringify(hero),
{ headers: new Headers({ 'Content-Type': 'text/plain;charset=UTF-8'
}) }).map(response=> response.json());
}
search(name: string): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
return this.http.get(this.baseUri + 'api/Heroes?name='+
encodeURIComponent(name)).map(response=> response.json());
}
}
Hints
If you want the TypeScript codes generated to conform to the camel casing of JavaScript and JSON, you may add the following line in class WebApiConfig
of scaffolding codes of Web API:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
then the property names and function names will be in camel casing, provided the respective names in C# are in Pascal casing. For more details, please check camelCasing or PascalCasing.
Client Application Programming
When writing client codes in some decent text editors like Visual Studio, you may get nice intellisense.
import { Component, Inject, OnInit } from '@angular/core';
import * as namespaces from '../clientapi/WebApiNG2ClientAuto';
@Component({
selector: 'my-dashboard',
templateUrl: 'dashboard.component.html',
styleUrls: ['dashboard.component.css']
})
export class DashboardComponent implements OnInit {
heroes: namespaces.DemoWebApi_Controllers_Client.Hero[] = [];
constructor(private heroService: namespaces.DemoWebApi_Controllers_Client.Heroes) { }
ngOnInit(): void {
this.heroService.getHeros().subscribe(
heroes => this.heroes = heroes.slice(1, 5),
error => console.error(error)
);
}
}
With the design time type checking by the IDE and the compile time type checking on top of the generated codes, the productivity of client programming and the quality of the product will be improved with less efforts.
Don't do what computers can do, and let computers work hard for us. It is our job to provide automation solutions to our customers, so it is better to automate our own jobs first.
Points of Interest
In typical Angular 2 tutorials, including the official one which had been archived, the authors often urge application developers to craft a service class such as "HeroService
", and the golden rule is: always delegate data access to a supporting service class.
WebApiClientGen
generates this service class for you, and it is DemoWebApi_Controllers_Client.Heroes
that will consume the real Web API rather than the in-memory Web API. During the development of WebApiClientGen
, I had created a demo project DemoAngular2 and respective Web API controller for testing.
And typical tutorials also recommended using a mock service for the sake of unit testing. WebApiClientGen
had made using a real Web API service become much cheaper, so you may not need to create a mock service. You should balance the cost/benefit of using a mock or a real service during development, depending on your contexts. Generally, if your team has been able to utilize a Continuous Integration environment in each development machine, it could be quite seamless and fast to run tests with a real service.
In typical SDLC, after the initial setup, here are the typical steps of developing Web API and NG2 apps:
- Upgrade the Web API.
- Run CreateClientApi.ps1 to update the client API in TypeScript for NG2.
- Craft new integration test cases upon the updates of the Web API using the generated TypeScript client API codes or C# client API codes.
- Modify the NG2 apps accordingly.
- For testing, run StartWebApi.ps1 to launch the Web API, and run the NG2 app.
Hints
For step 5, there are alternatives. For example, you may use VS IDE to launch both the Web API and the NG2 app in debug mode at the same time. And some developers may prefer using "npm start
".
This article had been initially written for Angular 2, with the Http service. WebApiClientGen
2.3.0 supports HttpClient
introduced in Angular 4.3. And the generated APIs remain unchanged at the interface level. This makes the migration from the obsolete Http service to the HttpClient service fairly effortless or seamless, comparing with Angular application programming without using the generated APIs but using the Http service directly.
BTW, if you hadn't done migration to Angular 5, then this article may help: Upgrade to Angular 5 and HttpClient. If you are using Angular 6, you should be using WebApiClientGen
2.4.0+.
How About Swagger?
If you had walked through Swagger, particularly with Swashbuckle.AspNetCore
plus NSwag, you may be wondering which to use for generating client API 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
- Fetch API
- Aurelia
Axios
(preview)
Example: Controller Operation HeroesController.Get(id)
[HttpGet("{id}")]
public Hero Get(long id)
{...
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);
}
Hints
More details at Angular.ts.
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);
}
Hints
More details at WebApiCoreNG2ClientAuto.ts.
Hints
For more details, please check WebApiClientGen vs Swashbuckle.AspNetCore plus NSwagStudio.
References
History
- 24th February, 2020: Initial version