Click here to Skip to main content
14,540,600 members

ASP.NET Web API, Angular2, TypeScript and WebApiClientGen

Rate this:
4.98 (19 votes)
Please Sign up or sign in to vote.
4.98 (19 votes)
24 Feb 2020CPOL
Make the development of Angular 2+ application efficient with ASP.NET Web API and Web API Client Generators
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

  1. 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+.
  2. You and fellow developers highly prefer abstraction through strongly typed data and functions in both the server side and the client sides.
  3. 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; }
    }
}

Step 0: Install NuGet package WebApiClientGen and plugin WebApiClientGen.NG2 to the Web API Project

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

  1. 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.
  2. 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:

/// <summary>
/// Flagged options for cherry picking in various development processes.
/// </summary>
[Flags]
public enum CherryPickingMethods
{
    /// <summary>
    /// Include all public classes, properties and properties.
    /// </summary>
    All = 0,

    /// <summary>
    /// Include all public classes decorated by DataContractAttribute,
    /// and public properties or fields decorated by DataMemberAttribute.
    /// And use DataMemberAttribute.IsRequired
    /// </summary>
    DataContract =1,

    /// <summary>
    /// Include all public classes decorated by JsonObjectAttribute,
    /// and public properties or fields decorated by JsonPropertyAttribute.
    /// And use JsonPropertyAttribute.Required
    /// </summary>
    NewtonsoftJson = 2,

    /// <summary>
    /// Include all public classes decorated by SerializableAttribute,
    /// and all public properties or fields
    /// but excluding those decorated by NonSerializedAttribute.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    Serializable = 4,

    /// <summary>
    /// Include all public classes, properties and properties.
    /// And use System.ComponentModel.DataAnnotations.RequiredAttribute.
    /// </summary>
    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.

sequence diagram

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 all heroes.
         * GET api/Heroes
         * @return {Array<DemoWebApi_Controllers_Client.Hero>}
         */
        get(): Observable<Array<DemoWebApi_Controllers_Client.Hero>>{
            return this.http.get(this.baseUri + 'api/Heroes').map(response=> response.json());
        }

        /**
         * Get a hero.
         * GET api/Heroes/{id}
         * @param {number} id
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        getById(id: number): Observable<DemoWebApi_Controllers_Client.Hero>{
            return this.http.get(this.baseUri + 'api/Heroes/'+id).map
                                (response=> response.json());
        }

        /**
         * DELETE api/Heroes/{id}
         * @param {number} id
         * @return {void}
         */
        delete(id: number): Observable<Response>{
            return this.http.delete(this.baseUri + 'api/Heroes/'+id);
        }

        /**
         * Add a hero
         * POST api/Heroes?name={name}
         * @param {string} name
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        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());
        }

        /**
         * Update hero.
         * PUT api/Heroes
         * @param {DemoWebApi_Controllers_Client.Hero} hero
         * @return {DemoWebApi_Controllers_Client.Hero}
         */
        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 heroes
         * GET api/Heroes?name={name}
         * @param {string} name keyword contained in hero name.
         * @return {Array<DemoWebApi_Controllers_Client.Hero>} Hero array matching the keyword.
         */
        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( @Inject(namespaces.DemoWebApi_Controllers_Client.Heroes) 
                 private heroService: namespaces.DemoWebApi_Controllers_Client.Heroes) { }

    ngOnInit(): void {
        this.heroService.getHeros().subscribe(
            heroes => this.heroes = heroes.slice(1, 5),
            error => console.error(error)
        );
    }
}

Image 2

Image 3

Image 4

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:

  1. Upgrade the Web API.
  2. Run CreateClientApi.ps1 to update the client API in TypeScript for NG2.
  3. Craft new integration test cases upon the updates of the Web API using the generated TypeScript client API codes or C# client API codes.
  4. Modify the NG2 apps accordingly.
  5. 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)

/// <summary>
/// Get a hero.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id}")]
public Hero Get(long id)
{...

TypeScript Angular API Codes Generated by NSwagStudio

/**
 * @return Success
 */
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

/**
 * Get a hero.
 * GET api/Heroes/{id}
 */
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Zijian
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Sean Zhang Meng27-Dec-17 16:33
MemberSean Zhang Meng27-Dec-17 16:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 15 Jan 2017

Stats

63.6K views
75 bookmarked