Click here to Skip to main content
13,766,364 members
Click here to Skip to main content
Add your own
alternative version

Stats

48.5K views
59 bookmarked
Posted 15 Jan 2017
Licenced CPOL

ASP.NET Web API, Angular2, TypeScript and WebApiClientGen

, 15 Jul 2018
Rate this:
Please Sign up or sign in to vote.
Make the development of Angular 2 application efficient with ASP.NET Web API and Web API Client Gen

Introduction

This article is on top of “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 in WebApiClientGen v2.0. Hopefully, the evolution of NG2 won't be breaking my CodeGen and my Web frontend applications so frequently. :)

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 am using pretty much the same WebApiClientGen for NG2 application development.

Presumptions

  1. You are developing ASP.NET Web API 2.x applications, and will be developing the TypeScript libraries for the SPA based on Angular 2+.
  2. You and fellow developers prefer highly 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 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 if 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's 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 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

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.

Step 1: Prepare JSON Config Data

The JSON config data below is to POST to the CodeGen Web API:

{
    "ApiSelections": {
        "ExcludedControllerNames": [
            "DemoWebApi.Controllers.Account"
        ],

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],
        "CherryPickingMethods": 1
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "DemoWebApi.ClientApi",
        "GenerateBothAsyncAndSync": true,

        "CamelCase": true,
        "TypeScriptNG2Folder": "..\\DemoAngular2\\clientapi",
        "NGVersion" : 5

    }
}

Hints

Angular 6 is using RxJS v6 which had introduced some breaking changes, particularly for importing Observable. By default, WebApiClientGen 2.4 and greater will by default declare the import as import { Observable } from 'rxjs';  . If you are still using Angular 5.x, you need to declare "NGVersion" : 5 in the JSON config, so the import in the generated codes will be import { Observable } from 'rxjs/Observable'; . Please refer to RxJS v5.x to v6 Update Guide and RxJS: TSLint Rules for Version 6 for more details.

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 config 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. You have options of generating TypeScript client API codes for jQuery or NG2, or C# client API codes, or all three.

"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.

Hints

So basically, you just need step 2 to generate the client API whenever the Web API is updated, since you don't need to install the NuGet package or craft new JSON config data every time.

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 { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
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, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import * as namespaces from '../clientapi/WebApiNG2ClientAuto';
import DemoWebApi_Controllers_Client = namespaces.DemoWebApi_Controllers_Client;

@Component({
    moduleId: module.id,
    selector: 'my-heroes',
    templateUrl: 'heroes.component.html',
    styleUrls: ['heroes.component.css']
})

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, 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 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 you team has been able to utilizing 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 in VS IDE.

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. After reading this article, you may have more momentum to migrate to HttpClient if you hadn't done so, 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+.

 

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.

You may also be interested in...

Comments and Discussions

 
QuestionMessage Closed Pin
3-Jun-18 23:22
memberMember 138371233-Jun-18 23:22 
GeneralMy vote of 5 Pin
Sean Zhang Meng27-Dec-17 17:33
memberSean Zhang Meng27-Dec-17 17: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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.181114.1 | Last Updated 16 Jul 2018
Article Copyright 2017 by Zijian
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid