Click here to Skip to main content
13,662,003 members
Click here to Skip to main content
Add your own
alternative version

Stats

9.4K views
13 bookmarked
Posted 13 May 2018
Licenced CPOL

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

, 20 May 2018
Rate this:
Please Sign up or sign in to vote.
Code First approach for generating client APIs for ASP.NET Core Web API, in C# and in TypeScript for jQuery and Angular 2+

Introduction

Strongly Typed Client API Generators generate strongly typed client API in C# and in TypeScript for jQuery and Angular 2+. The toolkit is to minimize repetitive tasks, streamline the coordination between the backend development and the frontend development, and improve the productivity and the quality.

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 and Angular 2
  3. TypeScript CodeDOM, a CodeDOM component for TypeScript, derived from CodeDOM of .NET Framework
  4. POCO2TS.exe, a command line program that generates TypeScript interfaces from POCO classes
  5. Fonlow.Poco2Ts, a component that generates TypeScript interfaces from POCO classes

This article is focused on generating C# Client API libraries for ASP.NET Core 2.0, while the full coverage is at "Generate C# Client API for ASP.NET Web API".

Using the Code

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:

#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 IHostingEnvironment hostingEnvironment;

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

        /// <summary>
        /// Trigger the API to generate WebApiClientAuto.cs for an established client API project.
        /// POST to  http://localhost:56321/api/CodeGen with json object CodeGenParameters
        /// </summary>
        /// <param name="settings"></param>
        /// <returns>OK if OK</returns>
        [HttpPost]
        public ActionResult TriggerCodeGen([FromBody] CodeGenSettings settings)
        {
            if (settings == null || settings.ClientApiOutputs == null)
                return new BadRequestResult();

            string webRootPath = hostingEnvironment.WebRootPath;
            Fonlow.Web.Meta.WebApiDescription[] apiDescriptions;
            try
            {
                var descriptions = ApiExplorerHelper.GetApiDescriptions(apiExplorer);
                apiDescriptions = descriptions.Select
                            (d => Fonlow.Web.Meta.MetaTransform.GetWebApiDescription(d)).ToArray();

            }
            catch (System.InvalidOperationException e)
            {
                System.Diagnostics.Trace.TraceWarning(e.Message);
                return StatusCode((int)HttpStatusCode.ServiceUnavailable);
            }

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

			try
			{
				CodeGen.GenerateClientAPIs(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();
        }
    }

}
#endif

Hints

The updated CodeGenController is located in the WebApiClientGen repository.

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:

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(
                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.

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

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

Then there's no need to add ApiExplorerVisibilityEnabledConvention.

Step 1.3 Copy HttpClient.ts for jQuery

If you will be using jQuery, you may be interested in using client API generated  in TypeScript for jQuery. Then you need to copy HttpClient.ts to the Scripts folder.

Step 2: Create .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:

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:

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

		"DataModelAssemblyNames": [
			"DemoWebApi.DemoDataCore",
			"DemoCoreWeb"
		],
		"CherryPickingMethods": 3
	},

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

		"CamelCase": true,
		"ContentType": "application/json;charset=UTF-8",
		"TypeScriptJQFolder": "..\\..\\..\\Scripts\\ClientApi",
		"TypeScriptNG2Folder": "..\\..\\..\\..\\DemoNGCli\\NGSource\\src\\ClientApi",
        "NGVersion" : 5

	}
}

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

Hints

The ExcludedControllerNames property will exclude those controllers that are already visible to ApiExplorer, while 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\netcoreapp2.0, then run

dotnet democoreweb.dll

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:

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\netcoreapp2.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:

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 had explained well at:

.NET SDK Projects - No more Content and Tools

License

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

Share

About the Author

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

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05-2016 | 2.8.180810.1 | Last Updated 20 May 2018
Article Copyright 2018 by Zijian
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid