Click here to Skip to main content
15,867,568 members
Articles / Web Development / ASP.NET / ASP.NET Core

Generate C# Client API for ASP.NET Web API

Rate me:
Please Sign up or sign in to vote.
4.94/5 (72 votes)
4 May 2020CPOL16 min read 289.4K   244   62
Generate strongly typed client API in C# for ASP.NET Web API and .NET Core Web API supporting desktop, Universal Windows, Android and iOS
This article is focused on generating C# Client API libraries. It looks at: Installing NuGet package WebApiClientGen to the Web MVC/API Project, creating the .NET Client API Project, preparing JSON Config Data, POST JSON Config data to Trigger the Generation of Client API Codes, and publishing Client API Libraries.

Introduction

For developing client programs of ASP.NET Web API or ASP.NET Core Web API, Strongly Typed Client API Generators generate strongly typed client API in C# codes and TypeScript codes. The toolkit is to minimize repetitive tasks, streamline the coordination between the backend development and the frontend development, and improve the productivity of the dev team and the quality of the product through less efforts, works and stress.

This open source project provides these products:

  1. Code generator for strongly typed client API in C# supporting desktop, Universal Windows, Android and iOS
  2. Code generators for strongly typed client API in TypeScript for jQuery, Angular 2+, Axios, Aurelia and Fetch API.
  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 C# Client API libraries. For client API libraries in TypeScript, please check the other article. If you are developing .NET Core applications, you may be interested in checking "Generate C# Client API for ASP.NET Core Web API" after reading this.

Key Features

  1. Client API codes generated are directly mapped from the Web API controller methods, .NET primitive types and POCO classes. This is similar to what svcutil.exe in WCF has offered.
  2. Doc comments of controller methods and POCO classes are copied to the client codes.

Key Benefits

  1. WebApiClientGen is seamlessly integrated with ASP.NET Web API with very little steps/overheads to setup, maintain and synchronize between Web API and client APIs, during RAD or Agile Software Development.
  2. Support all .NET primitive types including decimal.
  3. Support DataTime, DataTimeOffset, Array, Tuple, Dynamic Object, Dictionary and KeyValuePair
  4. Strongly typed generated codes are subject to design time type checking and compile time type checking.
  5. Provide high level of abstraction, shielding application developers from trivial technical details of RESTful practices.
  6. Rich meta information including doc comments makes IDE intellisense more helpful, so application developers have less need for reading separated API documents.

Background

If you have ever developed SOAP based Web services using WCF, you might have enjoyed using the client API codes generated by SvcUtil.exe or Service References of Visual Studio IDE. When moving to Web API, I felt that I had gone back to the stone age, since I had to spend a lot of minutes for checking data types and function prototypes during development, and this consumes too much of my precious brain power while computers should have done such checking.

I had developed some RESTful Web services on top of IHttpHandler/IHttpModule back in 2010, which did not need strongly typed data but arbitrary data like documents and streams. However, I have been developing more Web projects that need high abstraction and semantic data types.

I see that ASP.NET Web API does support high 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 have to hand-craft some very primitive and repetitive client codes to consume the Web services. If the Web API was developed by others, I will have to read the online help pages.

I miss my good old days with WCF. :) The overhead of client programming should be reduced.

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. Here's a list of open source projects assisting the development of client programs:

  1. WADL
  2. RAML with .NET
  3. WebApiProxy
  4. Swashbuckle based on Swagger
  5. AutoRest
  6. OData

While these solutions could generate strongly typed client codes and reduce repetitive tasks at some degree, I found that none of them could give me all the fluent and 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, thus least overhead during SDLC
  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 applications running on Windows desktop, Universal Windows, Android or iOS using C# as the primary programming language.
  2. You and fellow developers prefer high abstraction through strongly typed functions in both the server side and the client side.
  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 class members to client programs.

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

Step 0: Install NuGet package WebApiClientGen to the Web MVC/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 project's Controllers folder.
The CodeGenController should be available only during development in the debug build, since the client API should be generated only 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 System.Linq;
using System.Web.Http;

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.
        /// </summary>
        /// <param name="settings"></param>
        /// <returns>OK if OK</returns>
        [HttpPost]
        public IHttpActionResult TriggerCodeGen(CodeGenSettings settings)
        {
            if (settings == null)
                return BadRequest("No settings");

            if (settings.ClientApiOutputs == null)
                return BadRequest("No settings/ClientApiOutputs");

            string webRootPath = System.Web.Hosting.HostingEnvironment.MapPath("~");
            Fonlow.Web.Meta.WebApiDescription[] apiDescriptions;
            try
            {
                apiDescriptions = 
                  Configuration.Services.GetApiExplorer().ApiDescriptions.Select
                  (d => Fonlow.Web.Meta.MetaTransform.GetWebApiDescription(d)).OrderBy
                  (d => d.ActionDescriptor.ActionName).ToArray();

            }
            catch (System.InvalidOperationException e)
            {
                System.Diagnostics.Trace.TraceWarning(e.Message);
                return InternalServerError(e);
            }

            if (!settings.ClientApiOutputs.CamelCase.HasValue)
            {
                var camelCase = GlobalConfiguration.Configuration.Formatters.
                    JsonFormatter.SerializerSettings.ContractResolver is 
                    Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver;
                settings.ClientApiOutputs.CamelCase = camelCase;
            }

            try
            {
                CodeGen.GenerateClientAPIs(webRootPath, settings, apiDescriptions);
            }
            catch (Fonlow.Web.Meta.CodeGenException e)
            {
                var s = e.Message + " : " + e.Description;
                System.Diagnostics.Trace.TraceError(s);
                return BadRequest(s);
            }

            return Ok("Done");
        }
    }

}
#endif

Remarks

  1. CodeGenController is installed in YourMvcOrWebApiProject/Controllers, even though the scaffolding of a MVC project has folder API for derived classes of ApiController.
  2. WebApiClientGenCore for .NET Core however does not install CodeGenController, and you ought to copy the file over.

Step 1: Create the .NET Client API Project

Ensure the following packages are referenced:

  1. Microsoft ASP.NET Web API 2.2 Client Libraries
  2. Newtonsoft Json.NET.
  3. System.Runtime.Serialization
  4. System.ServiceModel
  5. System.ComponentModel.DataAnnotations

As illustrated in this screenshot:

Image 1

Step 2: 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 payload is like this:

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

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

        "CherryPickingMethods": 3
    },

    "ClientApiOutputs": {
        "ClientLibraryProjectFolderName": "..\\DemoWebApi.ClientApi",
        "UseEnsureSuccessStatusCodeEx": true,
        "DataAnnotationsEnabled": true,
        "DataAnnotationsToComments": true,

        "Plugins": []
    }
}

It is recommended to save the JSON payload into a file like this one.

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, or C# client API codes, or both.

The CodeGen generates C# client proxy classes 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. For example, 3 means both DataContract and NewtonsoftJson. And you may even define different cherry picking methods for different data model assemblies. For example:

JavaScript
"DataModels": [
    {
        "AssemblyName": "DemoWebApi.DemoData",
        "CherryPickingMethods": 3
    },

    {
        "AssemblyName": "DemoWebApi",
        "CherryPickingMethods": 1
    }
]

 

Step 3: Run the DEBUG Build of the Web API Project

Step 4: 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 one step to generate the client API, since you don't need to install the NuGet package 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 reference: a CreateClientApi.ps1 that launch the Web (API) project on IIS Express then POST the JSON config file. So basically, most of the time, for continuously updating / synchronizing Web API and client API, you just need step 3 through running a Powershell script. This reduces a lot of overheads for Continuous Integration.

This sequence diagram illustrates the development cycle:

Image 2

Publish Client API Libraries

After these steps, now you have the client API in C# generated, similar to this example:

C#
namespace DemoWebApi.DemoData.Client
{
    public class Address : object
    {
        
        public string City { get; set; }
        
        public string Country { get; set; } = "Australia";
               
        public System.Guid Id { get; set; }
        
        public string PostalCode { get; set; }
        
        public string State { get; set; }
        
        public string Street1 { get; set; }
        
        public string Street2 { get; set; }
        
        public DemoWebApi.DemoData.Client.AddressType Type { get; set; }
        
        /// <summary>
        /// It is a field
        /// </summary>
        public DemoWebApi.DemoData.Another.Client.MyPoint Location { get; set; }
    }
    
    public enum AddressType
    {
        
        Postal,
        
        Residential,
    }
    
    public class Company : DemoWebApi.DemoData.Client.Entity
    {
        
        /// <summary>
        /// BusinessNumber to be serialized as BusinessNum
        /// </summary>
        public string BusinessNumber { get; set; }
        
        public string BusinessNumberType { get; set; }
        
        public string[][] TextMatrix { get; set; }
        
        public int[,] Int2D { get; set; }
        
        public int[][] Int2DJagged { get; set; }
        
        public string[] Lines { get; set; }
    }
    
    public enum Days
    {
        
        Sat = 1,
        
        Sun = 2,
        
        Mon = 3,
        
        Tue = 4,
        
        Wed = 5,
        
        /// <summary>
        /// Thursday
        /// </summary>
        Thu = 6,
        
        Fri = 7,
    }
    
    /// <summary>
    /// Base class of company and person
    /// </summary>
    public class Entity : object
    {
        
        /// <summary>
        /// Multiple addresses
        /// </summary>
        public DemoWebApi.DemoData.Client.Address[] Addresses { get; set; }
        
        public System.Guid Id { get; set; }
        
        /// <summary>
        /// Name of the entity.
        /// Required
        /// </summary>
        [System.ComponentModel.DataAnnotations.RequiredAttribute()]
        public string Name { get; set; }
        
        public DemoWebApi.DemoData.Client.PhoneNumber[] PhoneNumbers { get; set; }
        
        public System.Uri Web { get; set; }
    }
    
    public class Person : DemoWebApi.DemoData.Client.Entity
    {
        
        /// <summary>
        /// Date of Birth.
        /// This is optional.
        /// </summary>
        public System.Nullable<System.DateTime> DOB { get; set; }
        
        public string GivenName { get; set; }
        
        public string Surname { get; set; }
    }
    
    public class PhoneNumber : object
    {
        
        public string FullNumber { get; set; }
        
        public System.Guid Id { get; set; }
        
        public DemoWebApi.DemoData.Client.PhoneType PhoneType { get; set; }
    }
    
    /// <summary>
    /// Phone type
    /// Tel, Mobile, Skyp and Fax
    /// 
    /// </summary>
    public enum PhoneType
    {
        
        /// <summary>
        /// Land line
        /// </summary>
        Tel,
        
        /// <summary>
        /// Mobile phone
        /// </summary>
        Mobile,
        
        Skype,
        
        Fax,
    }
}

namespace DemoWebApi.Controllers.Client
{
    using System;
    using System.Linq;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using System.Net.Http;
    using Newtonsoft.Json;
    using Fonlow.Net.Http;
    
    
    public partial class Entities
    {
        
        private System.Net.Http.HttpClient client;
        
        public Entities(System.Net.Http.HttpClient client)
        {
            if (client == null)
                throw new ArgumentNullException("Null HttpClient.", "client");

            if (client.BaseAddress == null)
                throw new ArgumentNullException("HttpClient has no BaseAddress", "client");

            this.client = client;
        }
        
        /// <summary>
        /// POST api/Entities/createPerson
        /// </summary>
        public async Task<long> CreatePersonAsync(DemoWebApi.DemoData.Client.Person p)
        {
            var requestUri = "api/Entities/createPerson";
            using (var requestWriter = new System.IO.StringWriter())
            {
            var requestSerializer = JsonSerializer.Create();
            requestSerializer.Serialize(requestWriter, p);
            var content = new StringContent(requestWriter.ToString(), System.Text.Encoding.UTF8, "application/json");
            var responseMessage = await client.PostAsync(requestUri, content);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
                var stream = await responseMessage.Content.ReadAsStreamAsync();
                using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream)))
                {
                return System.Int64.Parse(jsonReader.ReadAsString());
                }
            }
            finally
            {
                responseMessage.Dispose();
            }
            }
        }
                    
        /// <summary>
        /// DELETE api/Entities/{id}
        /// </summary>
        public async Task DeleteAsync(long id)
        {
            var requestUri = "api/Entities/"+id;
            var responseMessage = await client.DeleteAsync(requestUri);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
            }
            finally
            {
                responseMessage.Dispose();
            }
        }
        
        /// <summary>
        /// GET api/Entities/Company/{id}
        /// </summary>
        public async Task<DemoWebApi.DemoData.Client.Company> GetCompanyAsync(long id)
        {
            var requestUri = "api/Entities/Company/"+id;
            var responseMessage = await client.GetAsync(requestUri);
            try
            {
                responseMessage.EnsureSuccessStatusCodeEx();
                var stream = await responseMessage.Content.ReadAsStreamAsync();
                using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream)))
                {
                var serializer = new JsonSerializer();
                return serializer.Deserialize<DemoWebApi.DemoData.Client.Company>(jsonReader);
                }
            }
            finally
            {
                responseMessage.Dispose();
            }
        }
        

Noticeably the EntityId property in service class Address does not appear in the client data model, since obviously you don't want to publish such service side technical details to the client side, nor the client application developers should care about this property. 

If you would expect some external developers to use your Web API, you may publish the C# client API codes or compiled libraries targeting various platforms.

Consume the API Codes Generated

When writing client codes in some decent text editors like Visual Studio, you may get nice intellisense, so you rarely need to read Web API Help Page.

Image 3

Image 4

Image 5

Support Universal Windows Apps, Android Apps, and iOS Apps

For Universal Windows apps, you may create a client API library like this:

Image 6

For Android apps, you may have a client API project like this with Mono.Android:

Image 7

For iOS apps, you may create a client API project like this with Xamarin.iOS:

Image 8

Hints

If you would provide compiled libraries for various platforms with the same codebase, you may create a symbolic link to file WebApiClientAuto.cs generated in folder DemoWebApi.ClientApi.

Image 9

As illustrated in the screenshot, click on "Add As Link", you will create a symbolic link to the cs file in project DemoWebApi.iOSClientApi. Or, you may used Shared Project, or more preferably .NET Standard project.

Summary of Benefits

  1. Seamlessly integrated with ASP.NET Web API with very little steps/overheads to setup, maintain and synchronize between Web API and client APIs
  2. Support all built-in types including decimal
  3. Support DataTime, DataTimeOffset, Array, Tuple, Dynamic Object, Dictionary and KeyValuePair
  4. Strongly typed generated codes to be subject to design time type checking and compile time type checking
  5. High abstraction
  6. Intellisense

SDLC

Image 10

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.

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 are translated to client namespaces through 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 namespaces/function names and the client namespaces/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, one way or the other.

To client application 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 application 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 of handling errors. 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 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 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 development, 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 in the generated C# codes, thus you have little need for reading the Help Pages generated by MVC, and your client programming with the service will become more seamless.

Hints

And it shouldn't be hard to write scripts to automate some steps altogether for Continuous Integration.

 

WebApiClientGen vs Swagger

While Swagger toolchains are mostly and primarily for meta first approach, there are tools supporting code first approaches, that is, the server side tools generate Swagger definition files and the client tools generate codes based on the definitions. If you had used Swashbuckle plus NSwag, this section gives you a brief comparison. Swagger here means Swashbuckle.AspNetCore on ASP.NET Core Web API plus NSwagStudio for generating client codes.

C# Clients

As the name had suggested,  Strongly Typed Client API Generators provides exact data type mappings between server and C# clients, as precise as possible.

Swagger does not support:

  1. User defined struct.
  2. Object
  3. dynamic

Swagger gives data type mappings for the following types:

  1. Decimal ==> double
  2. Nullable<T> ==> T
  3. float ==>double
  4. uint, short, byte ==> int
  5. ulong ==> long
  6. char==> string
  7. Tuple ==> Generated user defined type with similar structure to Tuple
  8. int[,] ==> ICollection<object>
  9. int[][] ==> ICollection<int>
  10. KeyValuePair ==> Generated user defined type with similar structure to KeyValuePair

Swagger generates verbose, larger and complex codes:

In the sln, Core3WebApi is with WebApiClientGen, and SwaggerDemo is with Swashbuckle.AspNetCore for creating an Open API definition. When generating async functions only, codes generated by WebApiClientGen is 97KB, along with debug build 166KB and release build 117KB, while Swagger's NSwagStudio gives 489KB-495KB, along with debug build 340KB-343KB and release build 263KB-283KB.

Swagger yield verbose GeneratedCodeAttribute

According to https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute?view=netcore-3.1 , the GeneratedCodeAttribute class can be used by code analysis tools to identify computer-generated code, and to provide an analysis based on the tool and the version of the tool that generated the code.

It is a good practice to put generated codes into a dedicated assembly with generated codes only. Thus an application programmer may simply exclude the assembly from code analysis tools. Therefore GeneratedCodeAttribute is not necessary in the generated codes.

TypeScript Clients

WebApiClientGen:

  • jQuery with callbacks
  • Angular 2+
  • Axios
  • Aurelia
  • Fetch API

 

NSwag:

  • JQuery with Callbacks, JQueryCallbacks
  • JQuery with promises JQueryPromises
  • AngularJS using $http, AngularJS
  • Angular (v2+) using the http service, Angular
  • window.fetch API and ES6 promises, Fetch (use this template in your React/Redux app)
  • Aurelia using the HttpClient from aurelia-fetch-client, Aurelia (based on the Fetch template)
  • Axios (preview)

 

How WebApiClientGen is superior to Swagger?

For generating C# clients, WebApiClientGen supports more .NET built-in data types and give more exact data type mappings. Exact type mappings make client programming much easier for high quality since the integration tests should pick up data out of range easily because of proper type constraints.

Smaller codes and smaller compiled images are always welcome.

The manual steps of generating client codes is less and faster.

How Swagger is superior to WebApiClientGen?

Swagger here means the Open API standard and respective toolchains.

Swagger is an open standard and platform neutral, being supported by major software vendors and developed by hundreds of developers around the world. Microsoft Docs has a dedicated section for Swagger at https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-3.1, and Microsoft has been using Swagger for her own Web API products.

Swagger supports fine grained control over HTTP headers, while WebApiClientGen ignores this area, by design.

How about online help?

Swashbuckle.AspNetCore provides "a rich, customizable experience for describing the web API functionality".

WebApiClientGen copies in-source documents of published data types and controllers to client codes, and decent IDE like Visual Studio could display intellisense along with the in-source documents in the client codes. This minimizes the need for online help.

If you really like online help, you may use Sandcastle for C# client codes, use Compodoc for Angular 2+ client codes, and use TypeDoc for TypeScript client codes based on other Javascript frameworks.

Can WebApiClientGen and Swagger coexist?

Swagger here means Swashbuckle.AspNetCore on ASP.NET Core Web API plus NSwagStudio for generating client codes.

The answer is yes.

These two products are greatly overlapping in the .NET landscapes, while Swagger covers wider and deeper spectrum.

If you are developing ASP.NET (Core) Web API and expect all clients are coded in C# and TypeScript only, WebApiClientGen gives you more advantages.

When you need to support clients coded in languages other than C# and TypeScript, you may introduce Swagger into your Web API and generate the Open API definition files either in JSON or YAML.

Perfect SDLC with WebApiClientGen and Swagger

Whenever you as a backend developer has just updated the Web API, you run WebApiClientGen with a batch file and generate C# client codes and TypeScript client codes for other client developers. And the Swagger endpoint of the Web API gives the Open API definition files, so client developers working on other languages may generate client API codes in other languages.

So you get the best of WebApiClientGen and Swagger.

Updates: To make things better, I had made Strong Typed Open API Client Generators, which generate C# and TypeScript client codes from Open API / Swagger definitions. The codes generated are simpler and covering more functional data types. In some contents and contexts, it may be a good alternative to NSwag.

References

License

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


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

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

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

Comments and Discussions

 
QuestionApiExplorerHelper is missing Pin
frazGJF17-Aug-22 17:24
frazGJF17-Aug-22 17:24 
QuestionProblem with deserializing the returned data Pin
Keith Vinson6-Jun-18 13:27
Keith Vinson6-Jun-18 13:27 
AnswerRe: Problem with deserializing the returned data Pin
Keith Vinson6-Jun-18 14:24
Keith Vinson6-Jun-18 14:24 
GeneralRe: Problem with deserializing the returned data Pin
Zijian26-Aug-18 14:18
Zijian26-Aug-18 14:18 
GeneralRe: Problem with deserializing the returned data Pin
Keith Vinson27-Aug-18 11:06
Keith Vinson27-Aug-18 11:06 
SuggestionAlternative approach Pin
Maurice de Laat31-May-18 22:27
Maurice de Laat31-May-18 22:27 
GeneralRe: Alternative approach Pin
jerommeke28-Aug-18 4:35
jerommeke28-Aug-18 4:35 
QuestionCurrious question: Why trigger the code generation as you do? Pin
Keith Vinson23-May-18 4:29
Keith Vinson23-May-18 4:29 
AnswerRe: Currious question: Why trigger the code generation as you do? Pin
Zijian26-May-18 20:08
Zijian26-May-18 20:08 
GeneralMy vote of 5 Pin
CQüeb17-May-18 13:39
CQüeb17-May-18 13:39 
Question JSON config is auto generated ? Pin
maxoptimus16-May-18 23:41
maxoptimus16-May-18 23:41 
QuestionBut we need to move away from codegenerators Pin
Member 1000333016-May-18 20:53
Member 1000333016-May-18 20:53 
AnswerRe: But we need to move away from codegenerators Pin
Steve Naidamast17-May-18 6:24
professionalSteve Naidamast17-May-18 6:24 
GeneralRe: But we need to move away from codegenerators Pin
Member 1000333021-May-18 3:56
Member 1000333021-May-18 3:56 
GeneralRe: But we need to move away from codegenerators Pin
Steve Naidamast21-May-18 6:08
professionalSteve Naidamast21-May-18 6:08 
AnswerRe: But we need to move away from codegenerators Pin
Zijian21-May-18 2:23
Zijian21-May-18 2:23 
AnswerRe: But we need to move away from codegenerators Pin
Zijian26-May-18 20:23
Zijian26-May-18 20:23 
PraiseThanks Pin
Ashok Kumar RV1-Mar-18 0:25
Ashok Kumar RV1-Mar-18 0:25 
QuestionBusiness classes generation Pin
tatran.eu@gmail.com10-Nov-17 5:35
tatran.eu@gmail.com10-Nov-17 5:35 
AnswerRe: Business classes generation Pin
Zijian11-Nov-17 14:10
Zijian11-Nov-17 14:10 
PraiseThanks Pin
Member 1067628121-Apr-17 17:39
Member 1067628121-Apr-17 17:39 
QuestionRelationship to Swagger Pin
Member 1087658421-Apr-17 9:21
Member 1087658421-Apr-17 9:21 
AnswerRe: Relationship to Swagger Pin
Zijian25-Apr-17 1:51
Zijian25-Apr-17 1:51 
PraiseLooks promising Pin
mldisibio21-Apr-17 8:59
mldisibio21-Apr-17 8:59 
QuestionWCF is alive... Pin
Johnny YYZ21-Apr-17 1:47
professionalJohnny YYZ21-Apr-17 1:47 

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.