Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

Generate TypeScript Client API for ASP.NET Web API

4.88/5 (57 votes)
11 Nov 2019CPOL11 min read 124.2K  
Generate strongly typed client API in TypeScript for ASP.NET Web API

Introduction

For developing client programs of ASP.NET Web API or ASP. NET Core Web APIStrongly Typed Client API Generators generate strongly typed client API in C# codes and TypeScript codes for minimizing repetitive tasks and improving the productivity of application developers and the quality of the products. You may then provide or publish either the generated source codes or the compiled client API libraries to yourself and other developers in your team or B2B partners.

This project provides these products:

  1. Code generator for strongly typed client API in C# supporting desktop, Universal Windows, Android and iOS.
  2. Code generator for strongly typed client API in TypeScript for jQuery, Angular 2+ and Aurelia, as well as TypeScript/JavaScript applications that use Axios.
  3. TypeScript CodeDOM, a CodeDOM component for TypeScript, derived from CodeDOM of .NET Framework.
  4. POCO2TS.exe, a command line program that generates TypsScript interfaces from POCO classes.
  5. Fonlow.Poco2Ts, a component that generates TypsScript interfaces from POCO classes

This article is focused on generating TypeScript Client API for jQuery. 

Remarks

The support for Angular2 is available since WebApiClientGen v1.9.0-beta in June 2016 when Angular 2 was still in RC1. And the support for Angular 2 production release is available since WebApiClientGen v2.0. Please refer to article "ASP.NET Web API, Angular2, TypeScript and WebApiClientGen". 

Even if you are doing JavaScript programming, you may still use WebApiClientGen since the generated TypeScript file could be compiled into a JavaScript file. Though you won't get design time type checking and compile time type checking, you may still enjoy design time intellisense with in-source documents provided your JS editors support such feature.

Background

If you have ever developed SOAP base Web services using WCF, you might have enjoyed using the client API codes generated by SvcUtil.exe or Web Service References of Visual Studio IDE. When moving to Web API, I felt that I had got back to the Stone Age, since I had to do a lot of data type checking at design time using my precious brain power while computers should have done the job.

I had developed some RESTful Web services on top of IHttpHandler/IHttpModule in 2010 for some Web services that did not handle strongly typed data but arbitrary data like documents and streams. However, I have been getting more Web projects with complex business logic and data types, and I would utilize highly abstraction and semantic data types throughout SDLC.

I see that ASP.NET Web API does support highly abstraction and strongly typed function prototypes through class ApiController, and ASP.NET MVC framework optionally provides nicely generated Help Page describing the API functions. However, after developing the Web API, I had to hand-craft some very primitive and repetitive client codes to consume the Web services. If the Web API was developed by others, I had to read the online help pages and then craft.

Therefore, I had searched and tried to find some solutions that could release me from crafting primitive and repetitive codes so I could focus on building business logic at the client sides on higher technical abstractions. Here's a list of open source projects assisting the development of client programs:

  1. WADL
  2. RAML with .NET
  3. WebApiProxy
  4. Swashbuckle
  5. AutoRest
  6. OData
  7. TypeLITE
  8. TypeWriter

While these solutions could generated strongly typed client codes and reduce repetitive tasks at some degree, I found none of them could give me all the efficient programming experiences that I would expect:

  1. Strongly typed client data models mapping to the data models of the service.
  2. Strongly typed function prototypes mapping to the functions of derived classes of ApiController.
  3. Code generations in the wholesale style like the way of WCF programming.
  4. Cherry-picking data models through data annotations using popular attributes like DataContractAttribute and JsonObjectAttribute, etc.
  5. Type checking at design time and compile time.
  6. Intellisense for client data models, function prototypes and doc comments.

Here comes WebApiClientGen.

Presumptions

  1. You are developing ASP.NET Web API 2.x applications, and will be developing the JavaScript libraries for the Web front end based on AJAX, with jQuery or SPA with Angular2.
  2. You and fellow developers prefer high abstraction through strongly typed functions in both the server side and the client side, and TypeScript is utilized.
  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 utilizing WebApiClientGen were considering Trunk based development which is more efficient for Continuous Integration than other branching strategies like Feature Branching and Gitflow etc.

For following up this new way of developing client programs, it is better for you to have an ASP.NET Web API project, or a MVC project which contains Web API. You may use an existing project, or create a demo one.

Using the Code

This article is focused on the code example with jQuery. Similar code example for Angular 2+ is available at "ASP.NET Web API, Angular2, TypeScript and WebApiClientGen".

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

The installation will also install dependent NuGet packages Fonlow.TypeScriptCodeDOM and Fonlow.Poco2Ts to the project references.

HttpClient helper library should be copied to the Scripts folder along side with the generated codes which will be updated every time the CodeGen is executed.

Additionally, CodeGenController.cs for triggering the CodeGen is added to the 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.

C#
#if DEBUG  //This controller is not needed in production release, 
	// since the client API should be generated during development of the Web Api.
...

namespace Fonlow.WebApiClientGen
{
    [System.Web.Http.Description.ApiExplorerSettings(IgnoreApi = true)]//this controller is a 
			//dev backdoor during development, no need to be visible in ApiExplorer.
    public class CodeGenController : ApiController
    {
        /// <summary>
        /// Trigger the API to generate WebApiClientAuto.cs for an established client API project.
        /// POST to  http://localhost:10965/api/CodeGen with json object CodeGenParameters
        /// </summary>
        /// <param name="parameters"></param>
        /// <returns>OK if OK</returns>
        [HttpPost]
        public string TriggerCodeGen(CodeGenParameters parameters)
        {
...
        }
    }

Remarks

  1. CodeGenController is installed in YourMvcOrWebApiProject/Controllers, even though the scaffolding of a MVC project might has folder API for derived classes of ApiController. However, generally it is good to have the Web API implemented in a standalone Web API project. And if you want the MVC project and the Web API project run in the same Website, you may just installed the Web API as an application of the MVC Website.
  2. WebApiClientGenCore does not install CodeGenController, and you ought to copy the file over.

Enable Doc Comments of Web API

In C:\YourWebSlnPath\Your.WebApi\Areas\HelpPage\App_Start\HelpPageConfig.cs, there is such line:

C#
//config.SetDocumentationProvider(new XmlDocumentationProvider
(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

Uncomment it and make it be like this:

C#
config.SetDocumentationProvider(new XmlDocumentationProvider
(HttpContext.Current.Server.MapPath("~/bin/Your.WebApi.xml")));

In the Build tab of the project Properties page, check Output/XML Document File and set "bin\Your.WebApi.xml", while the output path is "bin" by default.

If you have other assemblies for data models, you may do the same to ensure doc comments to be generated and copied over to the client API.

Step 1: Prepare JSON Config Data

Your Web API project may have POCO classes and API functions like the ones blow. [Full code examples for data models and ApiController]

C#
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 Address
    {
        [DataMember]
        public Guid Id { get; set; }

        public Entity Entity { get; set; }

        /// <summary>
        /// Foreign key to Entity
        /// </summary>
        public Guid EntityId { get; set; }

        [DataMember]
        public string Street1 { get; set; }

        [DataMember]
        public string Street2 { get; set; }

        [DataMember]
        public string City { get; set; }

        [DataMember]
        public string State { get; set; }

        [DataMember]
        public string PostalCode { get; set; }

        [DataMember]
        public string Country { get; set; }

        [DataMember]
        public AddressType Type { get; set; }

        [DataMember]
        public DemoWebApi.DemoData.Another.MyPoint Location;
    }

    [DataContract(Namespace = Constants.DataNamespace)]
    public class Entity
    {
        public Entity()
        {
            Addresses = new List<Address>();
        }

        [DataMember]
        public Guid Id { get; set; }

        
        [DataMember(IsRequired =true)]//MVC and Web API does not care
        [System.ComponentModel.DataAnnotations.Required]//MVC and Web API care about only this
        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;
        }

    }

    [DataContract(Namespace = Constants.DataNamespace)]
    public class Company : Entity
    {
        [DataMember]
        public string BusinessNumber { get; set; }

        [DataMember]
        public string BusinessNumberType { get; set; }

        [DataMember]
        public string[][] TextMatrix
        { get; set; }

        [DataMember]
        public int[][] Int2DJagged;

        [DataMember]
        public int[,] Int2D;

        [DataMember]
        public IEnumerable<string> Lines;
    }

...
...

namespace DemoWebApi.Controllers
{
    [RoutePrefix("api/SuperDemo")]
    public class EntitiesController : ApiController
    {
        /// <summary>
        /// Get a person
        /// </summary>
        /// <param name="id">unique id of that guy</param>
        /// <returns>person in db</returns>
        [HttpGet]
        public Person GetPerson(long id)
        {
            return new Person()
            {
                Surname = "Huang",
                GivenName = "Z",
                Name = "Z Huang",
                BirthDate = DateTime.Now.AddYears(-20),
            };
        }

        [HttpPost]
        public long CreatePerson(Person p)
        {
            Debug.WriteLine("CreatePerson: " + p.Name);

            if (p.Name == "Exception")
                throw new InvalidOperationException("It is exception");

            Debug.WriteLine("Create " + p);
            return 1000;
        }

        [HttpPut]
        public void UpdatePerson(Person person)
        {
            Debug.WriteLine("Update " + person);
        }

        [HttpPut]
        [Route("link")]
        public bool LinkPerson(long id, string relationship, [FromBody] Person person)
        {
            return person != null && !String.IsNullOrEmpty(relationship);
        }

        [HttpDelete]
        public void Delete(long id)
        {
            Debug.WriteLine("Delete " + id);
        }

        [Route("Company")]
        [HttpGet]
        public Company GetCompany(long id)
        {

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

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

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoData",
            "DemoWebApi"
        ],

        "CherryPickingMethods": 3
    },

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

        "Plugins": [
            {
                "AssemblyName": "Fonlow.WebApiClientGen.jQuery",
                "TargetDir": "Scripts\\ClientApi",
                "TSFile": "WebApiJQClientAuto.ts",
                "AsModule": false,
                "ContentType": "application/json;charset=UTF-8"
            },

            {
                "AssemblyName": "Fonlow.WebApiClientGen.NG2",
                "TargetDir": "..\\DemoNGCli\\NGSource\\src\\ClientApi",
                "TSFile": "WebApiNG2ClientAuto.ts",
                "AsModule": true,
                "ContentType": "application/json;charset=UTF-8"
            }
        ]


    }
}

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.

"TypeScriptNG2Folder" is an absolute path or relative path to the Angular2 project. For example, "..\\DemoAngular2\\ClientApi" indicates an Angular 2 project 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:

C#
/// <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:

JavaScript
/// <reference path="../typings/jquery/jquery.d.ts" />
/// <reference path="HttpClient.ts" />
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 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>;
    }

    export interface Person extends DemoWebApi_DemoData_Client.Entity {
        Surname?: string;
        GivenName?: string;
        BirthDate?: Date;
    }

    export interface Company extends DemoWebApi_DemoData_Client.Entity {
        BusinessNumber?: string;
        BusinessNumberType?: string;
        TextMatrix?: Array<Array<string>>;
        Int3D?: Array<Array<Array<number>>>;
        Lines?: Array<string>;
    }
}

namespace DemoWebApi_DemoData_Another_Client {
    export interface MyPoint {
        X?: number;
        Y?: number;
    }
}

namespace DemoWebApi_Controllers_Client {

    export class Entities {
        httpClient: HttpClient;
        constructor(public baseUri?: string, public error?: 
        (xhr: JQueryXHR, ajaxOptions: string, thrown: string) => 
        any, public statusCode?: { [key: string]: any; }){
            this.httpClient = new HttpClient();
        }

        /**
         * Get a person
         * GET api/Entities/{id}
         * @param {number} id unique id of that guy
         * @return {DemoWebApi_DemoData_Client.Person} person in db
         */
        GetPerson(id: number, callback: (data : DemoWebApi_DemoData_Client.Person) => any){
            this.httpClient.get(encodeURI(this.baseUri + 
            'api/Entities/'+id), callback, this.error, this.statusCode);
        }

        /**
         * POST api/Entities
         * @param {DemoWebApi_DemoData_Client.Person} person
         * @return {number}
         */
        CreatePerson(person: DemoWebApi_DemoData_Client.Person, 
        	callback: (data : number) => any){
            this.httpClient.post(encodeURI(this.baseUri + 
            'api/Entities'), person, callback, this.error, this.statusCode);
        }

        /**
         * PUT api/Entities
         * @param {DemoWebApi_DemoData_Client.Person} person
         * @return {void}
         */
        UpdatePerson(person: DemoWebApi_DemoData_Client.Person, callback: (data : void) => any){
            this.httpClient.put(encodeURI(this.baseUri + 
            'api/Entities'), person, callback, this.error, this.statusCode);
        }

        /**
         * DELETE api/Entities/{id}
         * @param {number} id
         * @return {void}
         */
        Delete(id: number, callback: (data : void) => any){
            this.httpClient.delete(encodeURI(this.baseUri + 
            'api/Entities/'+id), callback, this.error, this.statusCode);
        }
    }

    export class Values {
        httpClient: HttpClient;
        constructor(public baseUri?: string, public error?: 
        (xhr: JQueryXHR, ajaxOptions: string, thrown: string) => any, 
        public statusCode?: { [key: string]: any; }){
            this.httpClient = new HttpClient();
        }

        /**
         * GET api/Values
         * @return {Array<string>}
         */
        Get(callback: (data : Array<string>) => any){
            this.httpClient.get(encodeURI(this.baseUri + 
            'api/Values'), callback, this.error, this.statusCode);
        }

        /**
         * GET api/Values/{id}?name={name}
         * @param {number} id
         * @param {string} name
         * @return {string}
         */
        GetByIdAndName(id: number, name: string, callback: (data : string) => any){
            this.httpClient.get(encodeURI(this.baseUri + 
            'api/Values/'+id+'?name='+name), 
            callback, this.error, this.statusCode);
        }

        /**
         * POST api/Values
         * @param {string} value
         * @return {string}
         */
        Post(value: {'':string}, callback: (data : string) => any){
            this.httpClient.post(encodeURI(this.baseUri + 
            'api/Values'), value, callback, this.error, this.statusCode);
        }

        /**
         * PUT api/Values/{id}
         * @param {number} id
         * @param {string} value
         * @return {void}
         */
        Put(id: number, value: {'':string}, callback: (data : void) => any){
            this.httpClient.put(encodeURI(this.baseUri + 
            'api/Values/'+id), value, callback, this.error, this.statusCode);
        }

        /**
         * DELETE api/Values/{id}
         * @param {number} id
         * @return {void}
         */
        Delete(id: number, callback: (data : void) => any){
            this.httpClient.delete(encodeURI(this.baseUri + 
            'api/Values/'+id), callback, this.error, this.statusCode);
        }
    }
}

Hints

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

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

2. The PowerShell script also compiles the TS file to a JS file.

Internal Usages

When writing client codes in some decent text editors like Visual Studio, you may get nice intellisense.

Image 1

Image 2

Image 3

Image 4

External Usages

If you would expect some external developers to use your Web API through JavaScript, you may publish the generated TypeScript client API file or the compiled JavaScript files, along with the help pages generated by ASP.NET MVC framework.

Points of Interests

While ASP.NET MVC and Web API use NewtonSoft.Json for JSON applications, NewtonSoft.Json can handle well POCO classes decorated by DataContractAttribute.

The CLR namespaces will be translated to TypeScript namespaces through replacing dot with underscore and adding "Client" as suffix. For example, namespace My.Name.space will be translated to My_Name_space_Client.

From a certain point of view, the one to one mapping between the service namespace/function names and the client namespace/function names is exposing the implementation details of the service, which generally is not recommended. However, traditional RESTful client programming requires programmers to be aware of the URL query templates of service functions, and the query templates are of implementation details of the service. So both approaches expose the implementation details of the service at some degree, but with different consequences.

To client developers, classic function prototype like:

C#
ReturnType DoSomething(Type1 t1, Type2 t2 ...)

is the API function, and the rest is the technical implementation details of transportation: TCP/IP, HTTP, SOAP, resource-oriented, CRUD-based URIs, RESTful, XML and JSON, etc. The function prototype and a piece of API document should be good enough for calling the API functions. Client developers should not have to care about those implementation details of transportation, at least when the operation is successful. Only when errors kick in, developers will have to care about the technical details. For example, in SOAP based web services, you have to know about SOAP faults; and in RESTful Web services, you may have to deal with HTTP status codes and Response.

And the query templates give little sense of semantic meaning of the API functions. In contrast, WebApiClientGen names the client functions after the service functions, just as SvcUtil.exe in WCF will do by default, so the client functions generated have good semantic meaning as long as you as the service developers had named the service functions after good semantic names.

In the big picture of SDLC covering both the service development and the client developments, the service developers have the knowledge of semantic meaning of service functions, and it is generally a good programming practice to name functions after functional descriptions. Resource-oriented CRUD may have semantic meaning or just become a technical translation from functional descriptions.

WebApiClientGen copies the doc comments of your Web API to JsDoc3 comments in the generated TypeScript codes, thus you have little need of reading the Help Pages generated by MVC, and your client programming with the service will become more seamless.

Many JavaScript frameworks like React and Vue.js do not come with a built-in HTTP request library, but depend on a 3rd library like Axios. Since Axios is apparently the most popular among JavaScript programmers in recent years, recommended by both React and Vue.js, it may be more viable to support Axios.

Hints

And it shouldn't be hard to write scripts to automate some steps altogether for Continuous Integration. And you can find examples at

  1. WebApiClientGen
  2. WebApiClientGen Examples
  3. .NET Core Demo

And locate those "Create*ClientApi.ps1" file in the root folder.

Remarks

The landscapes of developing Web services and clients have been rapidly changing. Since the first release of WebApiClientGen in September 2015, there came Open API Definition Format run by Open API Initiative, found in November 2015. Hopefully, the initiative will address some shortfalls of Swagger specification 2.0, particularly for handling the decimal / monetary type. Nevertheless, the SDLC utilizing WebApiClientGen is optimized for developing client programs upon ASP.NET Web API and ASP.NET Core Web API.

References

 

License

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