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

Generate C# Client API for ASP.NET Core Web API

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
12 Sep 2023CPOL9 min read 45.9K   53   2
Code First approach for generating client APIs for ASP.NET Core Web API, in C# and in TypeScript for jQuery, Aurelia, Axios and Angular 2+.
This article introduces how to use WebApiClientGen and Code First approach for generating client APIs for ASP.NET Core Web API, in C# and in TypeScript for jQuery, Aurelia, Axios and Angular 2+, without Swagger/OpenApi definitions.

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 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.
  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 ASP.NET Core 2.0+. If you are still working on .NET Framework, please check "Generate C# Client API for ASP.NET Web API". For client API libraries in TypeScript, please check the"ASP.NET Web API, Angular 2, TypeScript and WebApiClientGen".

Background

When developing Web apps that need high abstraction and semantic data types, you want that both the APIs and the client APIs could utilize strong data types for improving development productivity and data constraints.

Before developing WebApiClientGen in 2015, I had searched and tried to find some existing solutions that could release me from crafting 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
  5. AutoRest
  6. OData

NSwag had come to the surface in 2016 after the initial release of WebApiClientGen in November 2015, and had been endorsed by Microsoft. NSwag "combines the functionality of Swashbuckle and AutoRest in one toolchain", however, it is architecturally coupled with Swagger/OpenApi which has some inherent and architectural limitations.

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 ASP.NET Web 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 SvcUtils, thus least overhead during SDLC
  4. Cherry-picking data models through data annotations using popular .NET 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.

Using the Code

Presumptions

  1. You have been developing ASP.NET Web API applications, and will be developing client 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.

Step 0: Install NuGet package WebApiClientGenCore to the ASP.NET Core 2.0+ Web MVC/API Project

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

Step 1: Post NuGet Installation

Step 1.1 Create CodeGenController

In your Web API project, add the following controller (Copy the latest in Github):

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;
        }

        /// <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 ActionResult TriggerCodeGen([FromBody] CodeGenSettings settings)
        {
            if (settings == null)
                return BadRequest("No settings");

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

            Fonlow.Web.Meta.WebApiDescription[] apiDescriptions;
            try
            {
                var descriptions = ApiExplorerHelper.GetApiDescriptions(apiExplorer);
                apiDescriptions = descriptions.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 StatusCode((int)HttpStatusCode.InternalServerError, e.Message);
            }

            if (!settings.ClientApiOutputs.CamelCase.HasValue)
            {
                settings.ClientApiOutputs.CamelCase = true;
            }

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

            return Ok("Done");
        }
    }

}
#endif 

Image 1

Remarks

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.

Step 1.2 Make ApiExplorer Become Visible

This is to tell WebApiClientGen which controllers will be subject to client codes generation.

Opt-out Approach

In Startup.cs, add the highlighted line below:

C#
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc( // or AddControllers, or AddControllersWithViews, 
                options =>
                {
#if DEBUG
                    options.Conventions.Add
                     (new Fonlow.CodeDom.Web.
                      ApiExplorerVisibilityEnabledConvention());//To make ApiExplorer 
                                                  //be visible to WebApiClientGen
#endif
                }
                );

Using ApiExplorerVisibilityEnabledConvention is an opt-out approach to include all controllers except those decorated by ApiExplorerSettingsAttribute or ApiControllerAttribute. This is more appropriate if most of your controllers will be subject to generating strongly typed client APIs.

Opt-in Approach

Alternatively, if you prefer opt-in approach, you may use ApiExplorerSettingsAttribute to decorate a Web API controller, like this one:

C#
[ApiExplorerSettings(IgnoreApi = false)] // or [ApiController]
[Route("api/[controller]")]
public class HeroesController : ControllerBase
{

Then there's no need to add ApiExplorerVisibilityEnabledConvention. This is more appropriate if most of your controllers are not subject to generating strongly typed client APIs.

Step 2: Create .NET Core Client API Project

Image 2

Hints

If you are sure that System.Text.Json can handle all your scenarios of strongly data types on both the server and the .NET clients, you may set UseSystemTextJson to true in codegen.json, so you don't need Newtonsoft.Json.

Step 3: 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 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;
        }
    }

...

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

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

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

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

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

The JSON config data is like this:

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

        "DataModelAssemblyNames": [
            "DemoWebApi.DemoDataCore",
            "DemoCoreWeb"
        ],

        "CherryPickingMethods": 3
    },

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

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

        ]
    }
}

It is recommended to save the JSON payload into a file as illustrated in this screenshot:

Image 3

Hints

The ExcludedControllerNames property will exclude those controllers that are already visible to ApiExplorer, alternatively controllers decorated by [ApiExplorerSettings(IgnoreApi = true)] won't be visible to ApiExplorer.

StringAsString is an option for .NET Core Web API which will return text/plain string by default, rather than application/json JSON object, so the client codes generated won't deserialize the response body of respective Web API function.

Step 4: Run the DEBUG Build of the Web API Project and POST JSON Config Data to Trigger the Generation of Client API Codes

During development, you have two ways of launching the Web API within the VS solution folder.

DotNet

In command prompt, CD to a folder like C:\VSProjects\MySln\DemoCoreWeb\bin\Debug\netcoreapp3.0, then run:

BAT
dotnet democoreweb.dll

or just run democoreweb.exe.

IIS Express

Run the Web project in the VS IDE, IIS Express will be launched to host the Web app.

Remarks

Different hostings of the Web app may result in different Web root path, so you may need to adjust the JSON config data accordingly for the folders.

You may create and run a PowerShell file to launch the Web service and POST:

PowerShell
cd $PSScriptRoot
<#
Make sure CodeGen.json is saved in format ANSI or UTF-8 without BOM, 
 since ASP.NET Core 2.0 Web API will fail to deserialize POST Body that contains BOM.
#>
$path = "$PSScriptRoot\DemoCoreWeb\bin\Debug\netcoreapp3.0"
$procArgs = @{
    FilePath         = "dotnet.exe"
    ArgumentList     = "$path\DemoCoreWeb.dll"
    WorkingDirectory = $path
    PassThru         = $true
}
$process = Start-Process @procArgs

$restArgs = @{
    Uri         = 'http://localhost:5000/api/codegen'
    Method      = 'Post'
    InFile      = "$PSScriptRoot\DemoCoreWeb\CodeGen.json"
    ContentType = 'application/json'
}
Invoke-RestMethod @restArgs

Stop-Process $process

Publish Client API Libraries

After these steps, now you have the client API in C# generated to a file named as WebApiClientAuto.cs, similar to this example:

C#
public partial class Entities
{
    private System.Net.Http.HttpClient client;

    private System.Uri baseUri;

    public Entities(System.Net.Http.HttpClient client, System.Uri baseUri)
    {
        if (client == null)
            throw new ArgumentNullException("client", "Null HttpClient.");

        if (baseUri == null)
            throw new ArgumentNullException("baseUri", "Null baseUri");

        this.client = client;
        this.baseUri = baseUri;
    }

    /// <summary>
    /// Get a person
    /// so to know the person
    /// GET api/Entities/getPerson/{id}
    /// </summary>
    /// <param name="id">unique id of that guy</param>
    /// <returns>person in db</returns>
    public async Task<DemoWebApi.DemoData.Client.Person> GetPersonAsync(long id)
    {
        var requestUri = new Uri(this.baseUri, "api/Entities/getPerson/"+id);
        var responseMessage = await client.GetAsync(requestUri);
        responseMessage.EnsureSuccessStatusCode();
        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.Person>
                                                                 (jsonReader);
        }
    }

    /// <summary>
    /// Get a person
    /// so to know the person
    /// GET api/Entities/getPerson/{id}
    /// </summary>
    /// <param name="id">unique id of that guy</param>
    /// <returns>person in db</returns>
    public DemoWebApi.DemoData.Client.Person GetPerson(long id)
    {
        var requestUri = new Uri(this.baseUri, "api/Entities/getPerson/"+id);
        var responseMessage = this.client.GetAsync(requestUri).Result;
        responseMessage.EnsureSuccessStatusCode();
        var stream = responseMessage.Content.ReadAsStreamAsync().Result;
        using (JsonReader jsonReader =
               new JsonTextReader(new System.IO.StreamReader(stream)))
        {
        var serializer = new JsonSerializer();
        return serializer.Deserialize<DemoWebApi.DemoData.Client.Person>
                                                                 (jsonReader);
        }
    }

    /// <summary>
    /// POST api/Entities/createPerson
    /// </summary>
    public async Task<long> CreatePersonAsync(DemoWebApi.DemoData.Client.Person p)
    {
        var requestUri = new Uri(this.baseUri, "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);
        responseMessage.EnsureSuccessStatusCode();
        var stream = await responseMessage.Content.ReadAsStreamAsync();
        using (JsonReader jsonReader =
               new JsonTextReader(new System.IO.StreamReader(stream)))
        {
        var serializer = new JsonSerializer();
        return System.Int64.Parse(jsonReader.ReadAsString());
        }
        }
    }

SDLC

After the initial setup, every time you have made some changes to the interfaces of the Web APIs, you just need to:

  1. Build the DEBUG build of the Web API.
  2. Run CreateClientApi.ps1 which launches dotnet Kestrel Web server or IIS Express.
  3. Build and run your client integration tests.

The following sequence diagram illustrates the interactions of programmers and the automatic steps.

Image 4

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. If you are not using TBD but Git Flow or other branching strategy, it shouldn't be hard for you to adjust.

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

Controller and ApiController of ASP.NET, and Controller and ControllerBase of ASP.NET Core

In the old days before ASP.NET Web API, programmers had to use a MVC controller to create JSON-based Web API. Then Microsoft had created ASP.NET Web API, so programmers have been using System.Web.Http.ApiController ever since. Now with ASP.NET Core, programmers use Microsoft.AspNetCore.Mvc.ControllerBase or Microsoft.AspNetCore.Mvc.Controller for creating Web APIs, while ControllerBase supports Web API only and Controller supports both Web API and MVC view.

Nevertheless, it may be wise not to mix API functions and View functions in the same Controller derived class.

Handling String in the HTTP Response

In ASP.NET Web API, if a Web API function returns a string, the response body is always a JSON object, unless you provide a custom made formatter that returns string as string. In .NET Core Web API, such API function will by default return a string as a string in the response body, unless the client HTTP request provides an accept header "application/json". When providing "StringAsString" : true in the CodeGen JSON config, the client codes generated won't deserialize the response body of respective Web API function, and obviously this is more efficient if the Web API function will return a large string.

About NuGet for .NET Core

Presumably, you have read "Generate C# Client API for ASP.NET Web API". When importing NuGet package Fonlow.WebApiClientGen, installing the NuGet package could copy CodeGenController and other files to the Web project. However, for .NET Core Web project, Fonlow.WebApiClientGenCore could copy only the assemblies. Rick Strahl has explained well at:

.NET SDK Projects - No more Content and Tools

WebApiClientGen vs Swagger

OpenApiClientGen

OpenApiClientGen is based on Fonlow.TypeScriptCodeDomCore and Fonlow.Poco2TsCore which are core components of WebApiClientGen, thus the codes generated share similar characteristics.

Comparison with NSwag

When using Web services provided by the other vendors with Swagger/OpenAPI definitions, you may try OpenApiClientGen.

If you are doing full stack development of both Web service and client programs, you don't need Swagger/OpenAPI unless you want to provide client APIs to other companies some of which are using technical stacks that WebApiClientGen does not support.

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

 
QuestionAutorest Pin
Tomislav Markovski19-Feb-20 3:28
Tomislav Markovski19-Feb-20 3:28 
AnswerRe: Autorest Pin
Zijian26-Feb-20 12:18
Zijian26-Feb-20 12:18 

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.