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

Generate TypeScript Client API for ASP.NET Core Web API

5.00/5 (3 votes)
17 Oct 2023CPOL11 min read 8.9K  
How to generate strongly typed client API in TypeScript for ASP.NET Core
Generate strongly typed client API in TypeScript for ASP.NET Core through Code First approach without using Swagger/OpenAPI definitions.

Introduction

For developing client programs of ASP. NET Core Web API, Strongly 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 article is focused on generating TypeScript Client API for various JavaScript libraries and TypeScript frameworks.

Remarks

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

The development of WebApiClientGen started in 2015 for C# .NET client API used in a WPF application. Swagger (renamed to OpenApi) has some inherent shortfalls:

  1. Limited number of data types
  2. Not supporting Generic
  3. ...

WebApiClientGen was primarily designed for .NET Framework and .NET (Core), fully (95%) utilizing rich data types of .NET. Later on, I had developed the support for jQuery, initially for fun and later for production, after developing TypeScript CodeDOM.

As an application developer engaged in mostly complex business applications, I expect such efficient programming experiences:

  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.
  7. Consistent API prototypes across programming platforms: C#, TypeScript for jQuery, Angular, Aurelia, Fetch API and AXIOS

Here comes WebApiClientGen.

Presumptions

  1. You are developing ASP.NET Core applications, and will be developing the JavaScript libraries for the Web front end based on AJAX, with jQuery or SPA with Angular2, Vue or React.
  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 Core 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 WebApiClientGenCore and WebApiClientGenCore.jQuery to the Web API Project

The installation will also install dependent NuGet packages Fonlow.TypeScriptCodeDomCore and Fonlow.Poco2TsCore to the project references.

A HttpClient helper library should be copied to the Scripts folder alongside 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.
using Fonlow.CodeDom.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using System.Linq;
using System.Net;

namespace Fonlow.WebApiClientGen
{
    [ApiExplorerSettings(IgnoreApi = true)]
    [Route("api/[controller]")]
    public class CodeGenController : ControllerBase
    {
        private readonly IApiDescriptionGroupCollectionProvider apiExplorer;
        private readonly string webRootPath;

        /// <summary>
        /// For injecting some environment config by the run time.
        /// </summary>
        /// <param name="apiExplorer"></param>
        /// <param name="hostingEnvironment"></param>
        public CodeGenController(IApiDescriptionGroupCollectionProvider apiExplorer, 
                                 IWebHostEnvironment hostingEnvironment)
        {
            this.apiExplorer = apiExplorer;
            this.webRootPath = hostingEnvironment.WebRootPath;
        }

Remarks

  1. Nuget package WebApiClientGenCore does not install CodeGenController, and you ought to copy the file over.

Enable Doc Comments of Web API

In the Properties of the ASP.NET Core project, check Build/Output/Documentation file/Generate a file containing API documentation. For a complex/enterprise application, you may likely establish data models in other assemblies. Please check the same setting of these assemblies, and WebApiClientGen will read the XML documentation files and copy the contents to the generated codes.

Step 1: Prepare JSON Config Data

Your Web API project may have POCO classes and API functions like the ones below:

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"
            }
        ]
    }
}

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 DotNet Kestrel then post the JSON config data 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 interface Address {
        city?: string;
        country?: string;
        id?: string;
        postalCode?: string;
        state?: string;
        street1?: string;
        street2?: string;
        type?: DemoWebApi_DemoData_Client.AddressType;

        /**
         * It is a field
         */
        location?: DemoWebApi_DemoData_Another_Client.MyPoint;
    }

    export enum AddressType { Postal, Residential }

    export interface Company extends DemoWebApi_DemoData_Client.Entity {

        /**
         * BusinessNumber to be serialized as BusinessNum
         */
        BusinessNum?: string;
        businessNumberType?: string;
        foundDate?: Date;
        registerDate?: Date;
        textMatrix?: Array<Array<string>>;
        int2D?: number[][];
        int2DJagged?: Array<Array<number>>;
        lines?: Array<string>;
    }

    export enum Days {
        Sat = 1,
        Sun = 2,
        Mon = 3,
        Tue = 4,
        Wed = 5,

        /**
         * Thursday
         */
        Thu = 6,
        Fri = 7
    }

    /**
     * Base class of company and person
     */
    export interface Entity {

        /**
         * Multiple addresses
         */
        addresses?: Array<DemoWebApi_DemoData_Client.Address>;
        id?: string;

        /**
         * Name of the entity.
         */
        name: string;
        phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber>;
        web?: string;
    }

...

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

        /**
         * POST api/Entities/createCompany
         */
        createCompany(p: DemoWebApi_DemoData_Client.Company, 
              callback: (data : DemoWebApi_DemoData_Client.Company) => any, 
              headersHandler?: () => {[header: string]: string}) {
            this.httpClient.post(this.baseUri + 'api/Entities/createCompany', 
              p, callback, this.error, this.statusCode, 
              'application/json;charset=UTF-8', headersHandler);
        }

        /**
         * POST api/Entities/createPerson
         */
        createPerson(p: DemoWebApi_DemoData_Client.Person, callback: (data : number) 
                     => any, headersHandler?: () => {[header: string]: string}) {
            this.httpClient.post(this.baseUri + 'api/Entities/createPerson', 
              p, callback, this.error, this.statusCode, 
              'application/json;charset=UTF-8', headersHandler);
        }

        /**
         * DELETE api/Entities/{id}
         */
        delete(id: number, callback: (data : void) => any, 
                           headersHandler?: () => {[header: string]: string}) {
            this.httpClient.delete(this.baseUri + 'api/Entities/' + id, 
                            callback, this.error, this.statusCode, headersHandler);
        }

        /**
         * GET api/Entities/Company/{id}
         */
        getCompany(id: number, callback: 
                  (data : DemoWebApi_DemoData_Client.Company) => any, 
                   headersHandler?: () => {[header: string]: string}) {
            this.httpClient.get(this.baseUri + 'api/Entities/Company/' + id, 
                   callback, this.error, this.statusCode, headersHandler);
        }

Hints

  1. The PowerShell script also compiles the TS file for jQuery 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.

For Angular

Prerequisites

Step 1: In the Web API project, import NuGet package: Fonlow.WebApiClientGenCore.NG2

Step 2: In CodeGen.json which will be posted to the Web service, add the following:

JavaScript
"Plugins": [
    {
        "AssemblyName": "Fonlow.WebApiClientGenCore.NG2",
        "TargetDir": "..\\..\\..\\..\\..\\HeroesDemo\\src\\ClientApi",
        "TSFile": "WebApiCoreNG2ClientAuto.ts",
        "AsModule": true,
        "ContentType": "application/json;charset=UTF-8",
        "ClientNamespaceSuffix": ".Client",
        "ContainerNameSuffix": "",
        "DataAnnotationsToComments": true,
        "HelpStrictMode": true
    },

Generated Codes

JavaScript
...
export namespace DemoWebApi_Controllers_Client {
    @Injectable()
    export class Entities {
        constructor(@Inject('baseUri') private baseUri: string = location.protocol + 
        '//' + location.hostname + (location.port ? ':' + location.port : '') + 
        '/', private http: HttpClient) {
        }

        /**
         * POST api/Entities/createCompany
         */
        createCompany(p?: DemoWebApi_DemoData_Client.Company, headersHandler?: 
              () => HttpHeaders): Observable<DemoWebApi_DemoData_Client.Company> {
            return this.http.post<DemoWebApi_DemoData_Client.Company>
              (this.baseUri + 'api/Entities/createCompany', JSON.stringify(p), 
              { headers: headersHandler ? headersHandler().append('Content-Type', 
              'application/json;charset=UTF-8') : new HttpHeaders
              ({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

        /**
         * POST api/Entities/createPerson
         */
        createPerson(p?: DemoWebApi_DemoData_Client.Person, headersHandler?: 
                    () => HttpHeaders): Observable<number> {
            return this.http.post<number>(this.baseUri + 'api/Entities/createPerson', 
              JSON.stringify(p), { headers: headersHandler ? headersHandler().append
              ('Content-Type', 'application/json;charset=UTF-8') : 
              new HttpHeaders({ 'Content-Type': 'application/json;charset=UTF-8' }) });
        }

        /**
         * DELETE api/Entities/{id}
         */
        delete(id?: number, headersHandler?: () => HttpHeaders): 
                                             Observable<HttpResponse<string>> {
            return this.http.delete(this.baseUri + 'api/Entities/' + id, 
                   { headers: headersHandler ? headersHandler() : undefined, 
                     observe: 'response', responseType: 'text' });
        }

        /**
         * GET api/Entities/Company/{id}
         */
        getCompany(id?: number, headersHandler?: () => HttpHeaders): 
                                Observable<DemoWebApi_DemoData_Client.Company> {
            return this.http.get<DemoWebApi_DemoData_Client.Company>
                   (this.baseUri + 'api/Entities/Company/' + id, 
                   { headers: headersHandler ? headersHandler() : undefined });
        }

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

For Fetch API

Prerequisites

Step 1: Add NuGet package: Fonlow.WebApiClientGenCore.Fetch

Step 2: Post this payload:

JavaScript
{
    "AssemblyName": "Fonlow.WebApiClientGenCore.Fetch",
    "TargetDir": "..\\..\\..\\..\\..\\fetchapi\\src\\clientapi",
    "TSFile": "WebApiCoreFetchClientAuto.ts",
    "AsModule": true,
    "ContentType": "application/json;charset=UTF-8",
    "HelpStrictMode": true
}

Generated Codes

JavaScript
export namespace DemoWebApi_Controllers_Client {
    export class Entities {
        constructor(private baseUri: string = location.protocol + '//' + 
        location.hostname + (location.port ? ':' + location.port : '') + '/') {
        }

        /**
         * POST api/Entities/createCompany
         */
        createCompany(p?: DemoWebApi_DemoData_Client.Company, 
                      headersHandler?: () => {[header: string]: string}): 
                      Promise<DemoWebApi_DemoData_Client.Company> {
            return fetch(this.baseUri + 'api/Entities/createCompany', 
                      { method: 'post', headers: headersHandler ? 
                      Object.assign(headersHandler(), { 'Content-Type': 
                      'application/json;charset=UTF-8' }): { 'Content-Type': 
                      'application/json;charset=UTF-8' }, 
                      body: JSON.stringify(p) }).then(d => d.json());
        }

        /**
         * POST api/Entities/createPerson
         */
        createPerson(p?: DemoWebApi_DemoData_Client.Person, 
          headersHandler?: () => {[header: string]: string}): Promise<number> {
            return fetch(this.baseUri + 'api/Entities/createPerson', 
              { method: 'post', headers: headersHandler ? Object.assign(headersHandler(), 
              { 'Content-Type': 'application/json;charset=UTF-8' }): 
              { 'Content-Type': 'application/json;charset=UTF-8' }, 
                 body: JSON.stringify(p) }).then(d => d.json());
        }

        /**
         * DELETE api/Entities/{id}
         */
        delete(id?: number, headersHandler?: () => {[header: string]: string}): 
                                                     Promise<Response> {
            return fetch(this.baseUri + 'api/Entities/' + id, 
            { method: 'delete', headers: headersHandler ? headersHandler() : undefined });
        }

        /**
         * GET api/Entities/Company/{id}
         */
        getCompany(id?: number, headersHandler?: () => 
           {[header: string]: string}): Promise<DemoWebApi_DemoData_Client.Company> {
            return fetch(this.baseUri + 'api/Entities/Company/' + id, 
            { method: 'get', headers: headersHandler ? headersHandler() : 
                             undefined }).then(d => d.json());
        }

Points of Interests

Simplicity and Consistency

As you have seen, the APIs of the generated codes for jQuery, Angular and Aurelia are looking almost identical. Unless you are using the callback, the application codes look identical.

Angular and Aurelia are TypeScript Frameworks coming with their own HttpClient component. jQuery as a library comes with jQuery.ajax, while helper library HttpClient.ts makes the API and the implementation of the generated codes simple and consistent.

Many JavaScript libraries like React and Vue.js do not come with a built-in HTTP request library or component, and JS programmers have been typically using AXIOS or Fetch API. You may find some examples about how React and Vue.js utilize the generated codes for AXIOS:

Remarks

The Babel team behind React had changed their mind and then supported namespace in March 2019, as documented in issue 60.

Implementation Details of the Service

While ASP.NET Core MVC and Web API may use either System.Text.Json or 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.

Continuous Integration

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 for .NET Framework, .NET Standard, Xamarin, and vue TS.
  3. .NET Core Demo for ASP.NET Core MVC, Web API, ASP.NET Core + Angular, MAUI, fetchAPI, vue TS and React TS.

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.

SDLC

Image 5

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.

Teamwork

This section describes some basic scenarios of teamwork. Situations and contexts may vary in different companies and teams, thus you shall tune your team practices accordingly.

Your team has a backend developer Brenda working on the Web API, and a frontend developer Frank working on the frontend. Each development machine has the integration testing environment properly setup, so most CI works could be done on each development machine without the team CI server. Trunk base development is the default branching practice.

1 Repository Including Backend Codes and Frontend Codes

  1. Brenda wrote some new Web API codes, and build.
  2. Brenda executes CreateClientApi.ps1 to generate client codes.
  3. Brenda writes and runs some basic integration test cases against the Web API.
  4. Brenda commits/pushes the changes to the main development branch or the trunk.
  5. Frank updates/pulls the changes, builds, and runs the test cases.
  6. Frank develops new frontend features based on the new Web APIs and client APIs.

1 Backend Repository and 1 Frontend Repository

Brenda adjusted CodeGen.json that will direct the generated codes to the client API folders in the working folder of the frontend repository.

  1. Brenda wrote some new Web API codes, and build.
  2. Brenda executes CreateClientApi.ps1 to generate client codes.
  3. Brenda writes and runs some basic integration test cases against the Web API.
  4. Brenda commits/pushes the changes to the main development branch or the trunk of both repositories.
  5. Frank updates/pulls the changes with both repositories, builds, and runs the test cases.
  6. Frank develops new frontend features based on the new Web APIs and client APIs.

References

History

  • 17th October, 2023: Initial version

License

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