Coding Stephan

Automatic efficient API Client Generation with Kiota

I don’t like repeating myself. I also don’t like writing boilerplate code. Whenever I have to interact with an API, I hope that there is an openapi specification available, or even better a client library. Microsoft has built an open source project called Kiota that can generate client libraries for APIs based on their OpenAPI specifications.

You have to run a command line tool to generate your client library, but I also like to see that automated as part of my regular development lifecycle.

Prerequisites

This guide assumes you have:

  • An OpenAPI specification for the API you want to interact with, preferably in a file local to your solution.
  • Dotnet SDK installed on your machine.

Step 1: Tool manifest and installing Kiota

First, we need to create a tool manifest for our project if we don’t have one already. This will allow us to manage our dotnet tools locally. Run the following command in the root of you solution:

dotnet new tool-manifest

Next, we can install the Kiota tool as a local dotnet tool:

dotnet tool install Microsoft.OpenApi.Kiota

This will add Kiota to your tool manifest, and install the tool locally to your solution.

Step 2: Create a new class library project

Now we need to create a new class library project where our generated client will live. You can do this with the following command:

dotnet new classlib -n MyApi.Client

This will create a new class library project called MyApi.Client in a folder with the same name, adjust accordingly.

Step 3: Try generating the client

The point of this step is to figure out the exact commands that suit your needs. Check out the documentation for more details.

cd MyApi.Client
dotnet kiota generate -l CSharp -c MyApiClient -n MyApi.Client -d ../openapi.v1.json -o ./Generated --co  --ll error

This command will generate a C# client library for the API defined in ../openapi.v1.json, and output it to the ./Generated folder. Adjust the parameters as needed for your specific use case.

Step 4: Update project file

Open your project file (MyApi.Client.csproj) and add the following:

Dependencies

  <ItemGroup>
    <!-- This attribute is added to exclude the generated code from code coverage analysis -->
    <AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage" />
    <!-- This package reference is required for the generated client to work properly -->
    <PackageReference Include="Microsoft.Kiota.Bundle" Version="1.22.0" />
  </ItemGroup>

Restore tools target

This target will ensure that the Kiota tools are restored before we try to generate the client, by creating a file as a marker, it can keep track of whether the tools are already restored or not, and only restore them if they are not.

  <Target Name="RestoreTools" Outputs="../.vs/tools_restored.txt">
    <Exec Command="dotnet tool restore -v q" Condition="!Exists('../.vs/tools_restored.txt')" />
    <WriteLinesToFile File="../.vs/tools_restored.txt" Lines="Tools were automatically restored, this file is just to keep track of that." Condition="!Exists('../.vs/tools_restored.txt')" />
  </Target>

Auto generate client target

This target is responsible for generating the client. It uses the BeforeTargets attribute to ensure that it runs before the CollectPackageReferences target, which is a target that runs early in the build process. In fact this target is triggered as soon as the project is opened in the IDE, or if you call any cli command, like dotnet restore or dotnet build on it.

  <!-- This is the target responsible for generating the client. BeforeTargets=CollectPackageReferences is the magic sauce here. -->
  <Target Name="AutoGenerateRestClient" BeforeTargets="CollectPackageReferences" Outputs="Generated/MyApiClient.cs" DependsOnTargets="RestoreTools">
    <Message Text="Genering REST Client" Importance="High" Condition="!Exists('./Generated/MyApiClient.cs')" />
    <Exec Command="dotnet kiota generate -l CSharp -c MyApiClient -n MyApi.Client -d ../openapi.v1.json -o ./Generated --co  --ll error" Condition="!Exists('./Generated/MyApiClient.cs') AND Exists('../openapi.v1.json')" />
    <!-- If the client generation fails, we want to fail the build with a clear error message -->
    <OnError ExecuteTargets="ClientGenerationError" />
  </Target>

  <!-- Error target, that will be executed if the client generation fails. -->
  <Target Name="ClientGenerationError">
    <Error Text="MyApiClient could not be generated" />
  </Target>

Support regenerating the code

The target above will only generate the client the first time the project is loaded. We need some extra targets to support regenerating from command line dotnet msbuild -t:GenerateRestClient or by doing a dotnet clean on the project.

    <!-- This target is called when you do a dotnet clean on the project -->
    <Target Name="CleanGenerateRestClient" AfterTargets="CoreClean">
        <RemoveDir Directories="Generated" />
    </Target>
    <!-- This target allows you to regenerate the client by running dotnet msbuild -t:GenerateRestClient -->
    <Target Name="GenerateRestClient" DependsOnTargets="CleanGenerateRestClient;AutoGenerateRestClient" />

Step 5: Helper classes

The changes to the project file should have generated the client in the MyApiClient/Generated folder. To use this client with a preferred authentication method, and to set it up from dependency injection, you can create some helper classes. Be sure to not put them in the Generated folder, as that is meant to be fully managed by Kiota and any changes there will be overwritten when the client is regenerated.

KiotaServiceCollectionExtensions

Create some IServiceCollection extensions to register the Kiota handlers, and to attach them to the http client builder. Create a new file KiotaServiceCollectionExtensions.cs with the following content:

using Microsoft.Kiota.Http.HttpClientLibrary;
using Microsoft.Extensions.DependencyInjection;

namespace MyApi.Client;
/// <summary>
/// Service collection extensions for Kiota handlers.
/// </summary>
public static class KiotaServiceCollectionExtensions
{
    /// <summary>
    /// Adds the Kiota handlers to the service collection.
    /// </summary>
    /// <param name="services"><see cref="IServiceCollection"/> to add the services to</param>
    /// <returns><see cref="IServiceCollection"/> as per convention</returns>
    /// <remarks>The handlers are added to the http client by the <see cref="AttachKiotaHandlers(IHttpClientBuilder)"/> call, which requires them to be pre-registered in DI</remarks>
    public static IServiceCollection AddKiotaHandlers(this IServiceCollection services)
    {
        // Dynamically load the Kiota handlers from the Client Factory
        var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
        // And register them in the DI container
        foreach (var handler in kiotaHandlers)
        {
            services.AddTransient(handler);
        }

        return services;
    }

    /// <summary>
    /// Adds the Kiota handlers to the http client builder.
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    /// <remarks>
    /// Requires the handlers to be registered in DI by <see cref="AddKiotaHandlers(IServiceCollection)"/>.
    /// The order in which the handlers are added is important, as it defines the order in which they will be executed.
    /// </remarks>
    public static IHttpClientBuilder AttachKiotaHandlers(this IHttpClientBuilder builder)
    {
        // Dynamically load the Kiota handlers from the Client Factory
        var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
        // And attach them to the http client builder
        foreach (var handler in kiotaHandlers)
        {
            builder.AddHttpMessageHandler((sp) => (DelegatingHandler)sp.GetRequiredService(handler));
        }

        return builder;
    }
}

MyApiClientFactory

We will be injecting the generated client in our services, and I like to use the factory pattern for that. Create a new file MyApiClientFactory.cs with the following content:

using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;

namespace MyApi.Client;

internal class MyApiClientFactory
{
    private readonly IAuthenticationProvider _authenticationProvider;
    private readonly HttpClient _httpClient;

    public MyApiClientFactory(HttpClient httpClient, IAuthenticationProvider? authenticationProvider = null)
    {
        _authenticationProvider = authenticationProvider ?? new AnonymousAuthenticationProvider();
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }

    public MyApiClient CreateClient()
    {
        return new MyApiClient(new HttpClientRequestAdapter(_authenticationProvider, httpClient: _httpClient));
    }
}

Step 6: Registering the client in DI

Now that we have a fully working client, we can register it in our DI container. In your Program.cs or wherever you configure your services, add the following:

using MyApi.Client;
// Register the Kiota handlers
builder.Services.AddKiotaHandlers();

// Register whatever authentication provider you need, this is optional and depends on your specific use case, if you don't register one, the client will use the AnonymousAuthenticationProvider, which will work for APIs that don't require authentication.
//builder.Services.AddTransient<IAuthenticationProvider, MyCustomAuthenticationProvider>();

// Register the MyApiClientFactory using the AddHttpClient<..> method, which will allow us to configure the http client used by this factory.
// Set whatever defaults you want on the http client, like the base address or default headers.
builder.Services.AddHttpClient<MyApiClientFactory>((sp, client) =>
{
   client.BaseAddress = new Uri(...);
}).AttachKiotaHandlers();// Attach the Kiota handlers to the http client, which will ensure that the Kiota middleware is properly executed when the client is used.

// Register the MyApiClient itself, by resolving the factory from DI and calling the CreateClient method.
builder.Services.AddTransient(sp => sp.GetRequiredService<MyApiClientFactory>().CreateClient());

Step 7: Use the client

In any of your services or controllers, you can now inject the MyApiClient and use it to interact with your API:

using MyApi.Client;

public class MyService
{
    private readonly MyApiClient _client;

    public MyService(MyApiClient client)
    {
        _client = client;
    }
    public async Task DoSomethingWithApi()
    {
        var result = await _client.SomeEndpoint.GetAsync();
        // Do something with the result
    }
}

Conclusion

With this setup, you have an automatically generated, always up to date client for your API, that can be registered in your DI container and used throughout your application. Whenever you update the OpenAPI specification, just do a dotnet clean on the MyApiClient project, or run dotnet msbuild -t:GenerateRestClient, and the client will be regenerated with the latest changes. You are now ready to publish your client as a (private) NuGet package, or use it as a project reference in your solution and/or test suits.

I combine this with Identity Proxy to write strongly typed integration tests against the actual endpoints.