Click here to Skip to main content
13,249,813 members (86,637 online)
Click here to Skip to main content
Add your own
alternative version

Stats

27K views
30 bookmarked
Posted 15 Jan 2017

ASP.NET Web API, Angular2, TypeScript and WebApiClientGen

, 19 Nov 2017
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.

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 Angular2.
  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, you should be using WebApiClientGen v2.2.5. If you are using the HttpClient service available in Angular 4.3 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"

    }
}

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 and 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 that launch the Web (API) project on IIS Express then post the JSON config file to trigger the code generation.

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';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
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 jobs 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".

 

 

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

Pro
Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.171114.1 | Last Updated 19 Nov 2017
Article Copyright 2017 by Zijian
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid