Click here to Skip to main content
15,610,563 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article
Posted 13 May 2018

Stats

38K views
42 bookmarked

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

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
11 Feb 2020CPOL7 min read
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 introduce 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+; and also compare WebApiClientGen with Swagger/OpenAPI toolchain for .NET and ASP.NET Core.

Introduction

 

For developing client programs of ASP.NET Web API or ASP. NET Core Web APIStrongly Typed Client API Generators generate strongly typed client API in C# codes and TypeScript codes. 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 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 ASP.NET Core 2.0/3.0, while the full coverage is at "Generate C# Client API for ASP.NET Web API". For client API libraries in TypeScript, please check the other article.

Using the Code

Step 0: Install NuGet package WebApiClientGenCore to the ASP.NET Core 3.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:

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

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

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.

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.

Step 2: Create .NET Framework or .NET Core Client API Project

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:

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 2 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());
        }
        }
    }

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

 

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

C# Clients

Swagger does not support:

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

Swagger does not give exact 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.

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 gives fine grained control over HTTP headers, while WebApiClientGen ignores this area.

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.

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.