Tutorial: Generate a REST API client
An application which consumes a REST API is a very common scenario. Usually, you need to generate client code that your application can use to call the REST API. In this tutorial, you'll learn how to generate the REST API client automatically during build process using MSBuild. You'll use NSwag, a tool that generates client code for a REST API.
The complete sample code is available at REST API client generation in the .NET samples repo on GitHub.
The example shows a console app that consumes the public Pet Store API, which publishes an OpenAPI spec.
The tutorial assumes basic knowledge of MSBuild terms such as tasks, targets, properties, or runtimes; for the necessary background, see MSBuild Concepts article.
When you want to run a command-line tool as part of a build, there are two approaches to consider. One is to use the MSBuild Exec task, which lets you run a command-line tool and specify its parameters. The other method is to create a custom task derived from ToolTask, which gives you greater control.
Prerequisites
You should have an understanding of MSBuild concepts such as tasks, targets, and properties. See MSBuild concepts.
The examples require MSBuild, which is installed with Visual Studio, but can also be installed separately. See Download MSBuild without Visual Studio.
Option 1: Exec task
The Exec task simply invokes the specified process with the specified arguments, waits for it to complete, and then returns true
if the process completes successfully, and false
if an error occurs.
NSwag code generation can be used from MSBuild; see NSwag.MSBuild.
The complete code is in the PetReaderExecTaskExample folder; you can download and take a look. In this tutorial, you'll go through step by step and learn the concepts on the way.
Create a new console application named
PetReaderExecTaskExample
. Use .NET 6.0 or later.Create another project in the same solution:
PetShopRestClient
(This solution is going to contain the generated client as a library). For this project, use .NET Standard 2.1. The generated client doesn't compile on .NET Standard 2.0.In the
PetReaderExecTaskExample
project, and add a project dependency toPetShopRestClient
project.In the
PetShopRestClient
project, include the following NuGet packages:- Nswag.MSBuild, which allows access to the code generator from MSBuild
- Newtonsoft.Json, needed to compile the generated client
- System.ComponentModel.Annotations, needed to compile the generated client
In the
PetShopRestClient
project, add a folder (namedPetShopRestClient
) for the code generation and delete the Class1.cs that was automatically generated.Create a text file named petshop-openapi-spec.json at the root of the project. Copy the OpenApi spec from here and save it in the file. It's best to copy a snapshot of the spec instead of reading it online during the build. You always want a consistently reproducible build that depends only on the input. Consuming the API directly could transform a build which works today to a build which fails tomorrow from the same source. The snapshot saved on petshop-openapi-spec.json will allow us to still have a version that builds even if the spec changes.
Next, modify PetShopRestClient.csproj and add a MSBuild targets to generate the client during build process.
First, add some properties useful for client generation:
<PropertyGroup> <PetOpenApiSpecLocation>petshop-openapi-spec.json</PetOpenApiSpecLocation> <PetClientClassName>PetShopRestClient</PetClientClassName> <PetClientNamespace>PetShopRestClient</PetClientNamespace> <PetClientOutputDirectory>PetShopRestClient</PetClientOutputDirectory> </PropertyGroup>
Add the following targets:
<Target Name="generatePetClient" BeforeTargets="CoreCompile" Inputs="$(PetOpenApiSpecLocation)" Outputs="$(PetClientOutputDirectory)\$(PetClientClassName).cs"> <Exec Command="$(NSwagExe) openapi2csclient /input:$(PetOpenApiSpecLocation) /classname:$(PetClientClassName) /namespace:$(PetClientNamespace) /output:$(PetClientOutputDirectory)\$(PetClientClassName).cs" ConsoleToMSBuild="true"> <Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" /> </Exec> </Target> <Target Name="forceReGenerationOnRebuild" AfterTargets="CoreClean"> <Delete Files="$(PetClientOutputDirectory)\$(PetClientClassName).cs"></Delete> </Target>
Notice that this target uses the attributes BeforeTarget and AfterTarget as way to define build order. The first target called
generatePetClient
will be executed before the core compilation target, so the source will be created before the compiler executes. The input and output parameter are related to Incremental Build. MSBuild can compare the timestamps of the input files with the timestamps of the output files and determine whether to skip, build, or partially rebuild a target.After installing the
NSwag.MSBuild
NuGet package in your project, you can use the variable$(NSwagExe)
in your.csproj
file to run the NSwag command-line tool in an MSBuild target. This way, the tools can easily be updated via NuGet. Here, you're using theExec
MSBuild task to execute the NSwag program with the required parameters to generate the client Rest Api. See NSwag command and parameters.You can capture output from
<Exec>
addingConsoleToMsBuild="true"
to your<Exec>
tag and then capturing the output using theConsoleOutput
parameter in an<Output>
tag.ConsoleOutput
returns the output as anItem
. Whitespace is trimmed.ConsoleOutput
is enabled whenConsoleToMSBuild
is true.The second target called
forceReGenerationOnRebuild
deletes the generated class during cleanup to force the regeneration of the generated code during rebuild target execution. This target runs after theCoreClean
MSBuild predefined target.Execute a Visual Studio Solution rebuild and see the client generated on the
PetShopRestClient
folder.Now, use the generated client. Go to the client Program.cs, and copy the following code:
using System; using System.Net.Http; namespace PetReaderExecTaskExample { internal class Program { private const string baseUrl = "https://petstore.swagger.io/v2"; static void Main(string[] args) { HttpClient httpClient = new HttpClient(); httpClient.BaseAddress = new Uri(baseUrl); var petClient = new PetShopRestClient.PetShopRestClient(httpClient); var pet = petClient.GetPetByIdAsync(1).Result; Console.WriteLine($"Id: {pet.Id} Name: {pet.Name} Status: {pet.Status} CategoryName: {pet.Category.Name}"); } } }
Note
This code uses
new HttpClient()
because it's simple to demonstrate, but it is not the best practice for real-world code. The best practice is to useHttpClientFactory
to create anHttpClient
object which addresses the known issues ofHttpClient
request like Resource Exhaustion or Stale DNS problems. See Use IHttpClientFactory to implement resilient HTTP requests.
Congratulations! Now, you can execute the program to see how it works.
Option 2: Custom task derived from ToolTask
In many cases, using the Exec
task is good enough to execute an external tool to do something like REST API client code generation, but what if you want to allow REST API client code generation if and only if you don't use an absolute Windows path as input? Or what if you need to calculate in some way where the executable is? When there's any situation where you need to execute some code to do extra work, the MSBuild Tool Task is the best solution. The ToolTask
class is an abstract class derived from MSBuild Task
. You can define a concrete subclass, which creates a custom MSBuild task. This approach lets you run any code that is needed to prepare for command execution. You should read the tutorial Create a custom task for code generation first.
You'll create a custom task derived from MSBuild ToolTask which will generate a REST API client, but it will be designed to emit an error if you try to reference the OpenApi spec using an http address. NSwag supports an http address as OpenApi spec input, but for the purposes of this example, let's suppose there's a design requirement to disallow that.
The complete code is in this PetReaderToolTaskExample
folder; you can download and take a look. In this tutorial, you'll go through step by step and learn some concepts that you can apply to your own scenarios.
Create a new Visual Studio Project for the custom task. Call it
RestApiClientGenerator
and use the Class Library (C#) template with .NET Standard 2.0. Name the solutionPetReaderToolTaskExample
.Delete Class1.cs, which was automatically generated.
Add the
Microsoft.Build.Utilities.Core
NuGet packages:Create a class called
RestApiClientGenerator
Inherit from MSBuild
ToolTask
and implement the abstract method as shown in the following code:using Microsoft.Build.Utilities; namespace RestApiClientGenerator { public class RestApiClientGenerator : ToolTask { protected override string ToolName => throw new System.NotImplementedException(); protected override string GenerateFullPathToTool() { throw new System.NotImplementedException(); } } }
Add the following parameters:
- InputOpenApiSpec, where the spec is
- ClientClassName, name of the generated class
- ClientNamespaceName, namespace where the class is generated
- FolderClientClass, path to the folder where the class will be located
- NSwagCommandFullPath, full path to the directory where NSwag.exe is located
[Required] public string InputOpenApiSpec { get; set; } [Required] public string ClientClassName { get; set; } [Required] public string ClientNamespaceName { get; set; } [Required] public string FolderClientClass { get; set; } [Required] public string NSwagCommandFullPath { get; set; }
Install NSwag command line tool. You'll need the full path to the directory where NSwag.exe is located.
Implement the abstract methods:
protected override string ToolName => "RestApiClientGenerator"; protected override string GenerateFullPathToTool() { return $"{NSwagCommandFullPath}\\NSwag.exe"; }
There are many methods that you can override. For the current implementation, define these two:
- Define the command parameter:
protected override string GenerateCommandLineCommands() { return $"openapi2csclient /input:{InputOpenApiSpec} /classname:{ClientClassName} /namespace:{ClientNamespaceName} /output:{FolderClientClass}\\{ClientClassName}.cs"; }
- Parameter validation:
protected override bool ValidateParameters() { //http address is not allowed var valid = true; if (InputOpenApiSpec.StartsWith("http:") || InputOpenApiSpec.StartsWith("https:")) { valid = false; Log.LogError("URL is not allowed"); } return valid; }
Note
This simple validation could be done in other way on the MSBuild file, but it is recommended do it in C# code and encapsulate the command and the logic.
Build the project.
Create a console app to use the new MSBuild task
The next step is to create an app that uses the task.
Create a Console App project, and call it
PetReaderToolTaskConsoleApp
. Choose .NET 6.0. Mark it as startup project.Create a Class Library project to generate the code, called
PetRestApiClient
. Use .NET Standard 2.1.In the
PetReaderToolTaskConsoleApp
project, create a project dependency toPetRestApiClient
.In the
PetRestApiClient
project, create a folderPetRestApiClient
. This folder will contain the generated code.Delete Class1.cs, which was automatically generated.
On
PetRestApiClient
, add the following NuGet packages:- Newtonsoft.Json, needed to compile the generated client
- System.ComponentModel.Annotations, needed to compile the generated client
In the
PetRestApiClient
project, create a text file named petshop-openapi-spec.json (in the project folder). To add the OpenApi spec, copy the content from here into the file. We like a reproducible build that depends only on the input, as discussed previously. In this example, you'll raise a build error if a user chooses a URL as the OpenApi spec input.Important
A general rebuild won't work. You'll see errors that indicate it's unable to copy or delete
RestApiClientGenerator
.dll'. This is because it's trying to build the MBuild custom task in the same build process which uses it. SelectPetReaderToolTaskConsoleApp
and rebuild only that project. The another solution is put the custom task in a completely independent Visual Studio solution as you did in Tutorial: Create a custom task example.Copy the following code into Program.cs:
using System; using System.Net.Http; namespace PetReaderToolTaskConsoleApp { internal class Program { private const string baseUrl = "https://petstore.swagger.io/v2"; static void Main(string[] args) { HttpClient httpClient = new HttpClient(); httpClient.BaseAddress = new Uri(baseUrl); var petClient = new PetRestApiClient.PetRestApiClient(httpClient); var pet = petClient.GetPetByIdAsync(1).Result; Console.WriteLine($"Id: {pet.Id} Name: {pet.Name} Status: {pet.Status} CategoryName: {pet.Category.Name}"); } } }
Change the MSBuild instructions to call the task and generate the code. Edit PetRestApiClient.csproj by following these steps:
Register the use of the MSBuild custom task:
<UsingTask TaskName="RestApiClientGenerator.RestApiClientGenerator" AssemblyFile="..\RestApiClientGenerator\bin\Debug\netstandard2.0\RestApiClientGenerator.dll" />
Add some properties needed to execute the task:
<PropertyGroup> <!--The place where the OpenApi spec is in--> <PetClientInputOpenApiSpec>petshop-openapi-spec.json</PetClientInputOpenApiSpec> <PetClientClientClassName>PetRestApiClient</PetClientClientClassName> <PetClientClientNamespaceName>PetRestApiClient</PetClientClientNamespaceName> <PetClientFolderClientClass>PetRestApiClient</PetClientFolderClientClass> <!--The directory where NSawg.exe is in--> <NSwagCommandFullPath>C:\Nsawg\Win</NSwagCommandFullPath> </PropertyGroup>
Important
Select the proper
NSwagCommandFullPath
value based on the installation location on your system.Add an MSBuild target to generate the client during the build process. This target should execute before
CoreCompile
executes to generate the code used in the compilation.<Target Name="generatePetClient" BeforeTargets="CoreCompile" Inputs="$(PetClientInputOpenApiSpec)" Outputs="$(PetClientFolderClientClass)\$(PetClientClientClassName).cs"> <!--Calling our custom task derivated from MSBuild Tool Task--> <RestApiClientGenerator InputOpenApiSpec="$(PetClientInputOpenApiSpec)" ClientClassName="$(PetClientClientClassName)" ClientNamespaceName="$(PetClientClientNamespaceName)" FolderClientClass="$(PetClientFolderClientClass)" NSwagCommandFullPath="$(NSwagCommandFullPath)"></RestApiClientGenerator> </Target> <Target Name="forceReGenerationOnRebuild" AfterTargets="CoreClean"> <Delete Files="$(PetClientFolderClientClass)\$(PetClientClientClassName).cs"></Delete> </Target>
Input
andOutput
are related to Incremental Build, and theforceReGenerationOnRebuild
target deletes the generated file afterCoreClean
, which forces the client to be regenerated during the rebuild operation.Select
PetReaderToolTaskConsoleApp
and rebuild only that project. Now, the client code is generated and the code compiles. You can execute it and see how it works. This code generates the code from a file, and that is allowed.In this step, you'll demonstrate the parameter validation. In PetRestApiClient.csproj, change the property
$(PetClientInputOpenApiSpec)
to use the URL:<PetClientInputOpenApiSpec>https://petstore.swagger.io/v2/swagger.json</PetClientInputOpenApiSpec>
Select
PetReaderToolTaskConsoleApp
and rebuild only that project. You'll get the error, "URL is not allowed" in accordance with the design requirement.
Download the code
Install the NSwag command line tool. Then, you'll need the full path to the directory where NSwag.exe is located. After that, edit PetRestApiClient.csproj and select the proper $(NSwagCommandFullPath)
value based on the installation path on your computer. Now, select RestApiClientGenerator
and build only that project, and finally select and rebuild PetReaderToolTaskConsoleApp
. You can execute PetReaderToolTaskConsoleApp
. to verify that everything works as expected.
Next steps
You might want to publish your custom task as a NuGet package.
Or, learn how to test a custom task.