Generate Refit interfaces from OpenAPI specifications using Refitter

Refit is an awesome tool that I only just recently discovered and started using in systems that involve a lot of HTTP API integration points. With Refit, I can keep my API client code as simple as an interface with some magic sauce behind the scenes.

For those aren’t familiar with Refit, it is an automatic type-safe REST library for .NET. Refit is heavily inspired by Square’s Retrofit library for Android and Java. Refit turns your REST API into a live interface.

Refit currently supports the following platforms and any .NET Standard 2.0 target:

  • .NET Framework 4.6.1 and above
  • .NET 5.0 and above
  • Xamarin.Android
  • Xamarin.Mac
  • Xamarin.iOS
  • Blazor
  • Uno Platform
  • UWP

Given that Refit will actually do most of the work, you still need to create the Refit interface and the contracts that are used by the HTTP API your system is calling. A Refit interface could be something as simple as:

public interface IFooApi
{
    [Get("/foo/{id}")]
    Task<Foo> GetFoo(string id);
}

Using the interface above would be the equivalent of performing GET https://foo.api.somesystem.com/foo/12345 which returns a deserialized Foo resource

Introducing Refitter

I’m generally a lazy person who hates repeating the same tasks more than once and recently I thought that working with Refit could be done easier, so I created Refitter.

Refitter is an open source CLI tool for generating a C# REST API Client using the Refit library. Refitter can generate the Refit interface (and contracts using NSwag) from OpenAPI specifications. Refitter is also available as a library for other IDE extension and develop tool authors to integrate with

Refitter is available in different forms as a CLI Tool and from the REST API Client Code Generator extension that supports the following IDE:

CLI Tool

The CLI tool is packaged as a .NET Tool and is published to nuget.org. You can install the latest version of this tool like this:

dotnet tool install --global Refitter

Refitter provides some --help information for getting started

$ refitter --help
USAGE:
    refitter [input file] [OPTIONS]

EXAMPLES:
    refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --output ./Output.cs

ARGUMENTS:
    [input file]    Path to OpenAPI Specification file

OPTIONS:
                                      DEFAULT                                                          
    -h, --help                                         Prints help information                         
    -n, --namespace                   GeneratedCode    Default namespace to use for generated types    
    -o, --output                      Output.cs        Path to Output file                             
        --no-auto-generated-header                     Don't add <auto-generated> header to output file
        --interface-only                               Don't generate contract types                   
        --use-api-response                             Return Task<IApiResponse<T>> instead of Task<T>             

To generate code from an OpenAPI specifications file, run the following:

$ refitter [path to OpenAPI spec file] --namespace "[Your.Namespace.Of.Choice.GeneratedCode]"

This will generate a file called Output.cs which contains the Refit interface and contract classes generated.

REST API Client Code Generator

Since most of us, including me, spend most of our time in IDE´s, we have a lot of tools in our toolbox. I love Swagger and OpenAPI, which was the reason as to why I built the REST API Client Code Generator. This tool allows me to right click on a solution and select Add New REST API Client and prompts me to enter the URI to where the OpenAPI specifications can be downloaded from

In Visual Studio 2019 and 2022 that looks like this:

and in Visual Studio for Mac it looks like this:

From the context menu, select Generate with Refitter and get this a prompt that looks like this:

This will result in the file being OpenAPI (Swagger) specifications file to be downloaded, included in your project, and configured to use a custom tool that generates a code behind file upon any changes on the OpenAPI specifications file

Example generated code

Here’s an example generated output from the Swagger Petstore example

using Refit;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Your.Namespace.Of.Choice.GeneratedCode
{
    public interface ISwaggerPetstore
    {
        /// <summary>
        /// Update an existing pet by Id
        /// </summary>
        [Put("/pet")]
        Task<Pet> UpdatePet([Body]Pet body);

        /// <summary>
        /// Add a new pet to the store
        /// </summary>
        [Post("/pet")]
        Task<Pet> AddPet([Body]Pet body);

        /// <summary>
        /// Multiple status values can be provided with comma separated strings
        /// </summary>
        [Get("/pet/findByStatus")]
        Task<ICollection<Pet>> FindPetsByStatus([Query]Status? status);

        /// <summary>
        /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
        /// </summary>
        [Get("/pet/findByTags")]
        Task<ICollection<Pet>> FindPetsByTags([Query(CollectionFormat.Multi)]ICollection<string> tags);

        /// <summary>
        /// Returns a single pet
        /// </summary>
        [Get("/pet/{petId}")]
        Task<Pet> GetPetById(long? petId);

        [Post("/pet/{petId}")]
        Task UpdatePetWithForm(long? petId, [Query]string name, [Query]string status);

        [Delete("/pet/{petId}")]
        Task DeletePet(long? petId);

        [Post("/pet/{petId}/uploadImage")]
        Task<ApiResponse> UploadFile(long? petId, [Query]string additionalMetadata, [Body]StreamPart body);

        /// <summary>
        /// Returns a map of status codes to quantities
        /// </summary>
        [Get("/store/inventory")]
        Task<IDictionary<string, int>> GetInventory();

        /// <summary>
        /// Place a new order in the store
        /// </summary>
        [Post("/store/order")]
        Task<Order> PlaceOrder([Body]Order body);

        /// <summary>
        /// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
        /// </summary>
        [Get("/store/order/{orderId}")]
        Task<Order> GetOrderById(long? orderId);

        /// <summary>
        /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
        /// </summary>
        [Delete("/store/order/{orderId}")]
        Task DeleteOrder(long? orderId);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Post("/user")]
        Task CreateUser([Body]User body);

        /// <summary>
        /// Creates list of users with given input array
        /// </summary>
        [Post("/user/createWithList")]
        Task<User> CreateUsersWithListInput([Body]ICollection<User> body);

        [Get("/user/login")]
        Task<string> LoginUser([Query]string username, [Query]string password);

        [Get("/user/logout")]
        Task LogoutUser();

        [Get("/user/{username}")]
        Task<User> GetUserByName(string username);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Put("/user/{username}")]
        Task UpdateUser(string username, [Body]User body);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Delete("/user/{username}")]
        Task DeleteUser(string username);
    }
}

The CLI tool can also be used to generate a Refit interface that is configured to wrap the return type in IApiResponse<T>

$ refitter ./openapi.json --namespace "Your.Namespace.Of.Choice.GeneratedCode" --use-api-response

The command above produces an interface that might look like this:

using Refit;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Your.Namespace.Of.Choice.GeneratedCode.WithApiResponse
{
    public interface ISwaggerPetstore
    {
        /// <summary>
        /// Update an existing pet by Id
        /// </summary>
        [Put("/pet")]
        Task<IApiResponse<Pet>> UpdatePet([Body] Pet body);

        /// <summary>
        /// Add a new pet to the store
        /// </summary>
        [Post("/pet")]
        Task<IApiResponse<Pet>> AddPet([Body] Pet body);

        /// <summary>
        /// Multiple status values can be provided with comma separated strings
        /// </summary>
        [Get("/pet/findByStatus")]
        Task<IApiResponse<ICollection<Pet>>> FindPetsByStatus([Query(CollectionFormat.Multi)] Status? status);

        /// <summary>
        /// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
        /// </summary>
        [Get("/pet/findByTags")]
        Task<IApiResponse<ICollection<Pet>>> FindPetsByTags([Query(CollectionFormat.Multi)] IEnumerable<string> tags);

        /// <summary>
        /// Returns a single pet
        /// </summary>
        [Get("/pet/{petId}")]
        Task<IApiResponse<Pet>> GetPetById(long petId);

        [Post("/pet/{petId}")]
        Task UpdatePetWithForm(long petId, [Query(CollectionFormat.Multi)] string name, [Query(CollectionFormat.Multi)] string status);

        [Delete("/pet/{petId}")]
        Task DeletePet(long petId);

        [Post("/pet/{petId}/uploadImage")]
        Task<IApiResponse<ApiResponse>> UploadFile(long petId, [Query(CollectionFormat.Multi)] string additionalMetadata, [Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> body);

        /// <summary>
        /// Returns a map of status codes to quantities
        /// </summary>
        [Get("/store/inventory")]
        Task<IApiResponse<IDictionary<string, int>>> GetInventory();

        /// <summary>
        /// Place a new order in the store
        /// </summary>
        [Post("/store/order")]
        Task<IApiResponse<Order>> PlaceOrder([Body] Order body);

        /// <summary>
        /// For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
        /// </summary>
        [Get("/store/order/{orderId}")]
        Task<IApiResponse<Order>> GetOrderById(long orderId);

        /// <summary>
        /// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
        /// </summary>
        [Delete("/store/order/{orderId}")]
        Task DeleteOrder(long orderId);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Post("/user")]
        Task CreateUser([Body] User body);

        /// <summary>
        /// Creates list of users with given input array
        /// </summary>
        [Post("/user/createWithList")]
        Task<IApiResponse<User>> CreateUsersWithListInput([Body] IEnumerable<User> body);

        [Get("/user/login")]
        Task<IApiResponse<string>> LoginUser([Query(CollectionFormat.Multi)] string username, [Query(CollectionFormat.Multi)] string password);

        [Get("/user/logout")]
        Task LogoutUser();

        [Get("/user/{username}")]
        Task<IApiResponse<User>> GetUserByName(string username);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Put("/user/{username}")]
        Task UpdateUser(string username, [Body] User body);

        /// <summary>
        /// This can only be done by the logged in user.
        /// </summary>
        [Delete("/user/{username}")]
        Task DeleteUser(string username);
    }
}

Using the Refit interface

Refit provides two ways to use the interface:

  • Resolve the interface via the RestService class
  • Register the Refit interface with HttpClientFactory and use it through dependency injection

RestService

Here’s an example usage of the generated code above

using Refit;
using System;
using System.Threading.Tasks;

namespace Your.Namespace.Of.Choice.GeneratedCode;

internal class Program
{
    private static async Task Main(string[] args)
    {
        var client = RestService.For<ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var pet = await client.GetPetById(1);

        Console.WriteLine("## Using Task<T> as return type ##");
        Console.WriteLine($"Name: {pet.Name}");
        Console.WriteLine($"Category: {pet.Category.Name}");
        Console.WriteLine($"Status: {pet.Status}");
        Console.WriteLine();

        var client2 = RestService.For<WithApiResponse.ISwaggerPetstore>("https://petstore3.swagger.io/api/v3");
        var response = await client2.GetPetById(2);

        Console.WriteLine("## Using Task<IApiResponse<T>> as return type ##");
        Console.WriteLine($"HTTP Status Code: {response.StatusCode}");
        Console.WriteLine($"Name: {response.Content.Name}");
        Console.WriteLine($"Category: {response.Content.Category.Name}");
        Console.WriteLine($"Status: {response.Content.Status}");
    }
}

The RestService class generates an implementation of ISwaggerPetstore that uses HttpClient to make its calls.

The code above when run will output something like this:

## Using Task<T> as return type ##
Name: Gatitotototo
Category: Chaucito
Status: Sold

## Using Task<IApiResponse<T>> as return type ##
HTTP Status Code: OK
Name: Gatitotototo
Category: Chaucito
Status: Sold

ASP.NET Core and HttpClientFactory

Here’s an example Minimal API with the Refit.HttpClientFactory library:

using Refit;
using Your.Namespace.Of.Choice.GeneratedCode;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services
    .AddRefitClient<ISwaggerPetstore>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"));

var app = builder.Build();
app.MapGet(
        "/pet/{id:long}",
        async (ISwaggerPetstore petstore, long id) =>
        {
            try
            {
                return Results.Ok(await petstore.GetPetById(id));
            }
            catch (Refit.ApiException e)
            {
                return Results.StatusCode((int)e.StatusCode);
            }
        })
    .WithName("GetPetById")
    .WithOpenApi();

app.UseHttpsRedirection();
app.UseSwaggerUI();
app.UseSwagger();
app.Run();

.NET Core supports registering the generated ISwaggerPetstore interface via HttpClientFactory

The following request to the API above

$ curl -X 'GET' 'https://localhost:5001/pet/1' -H 'accept: application/json'

Returns a response that looks something like this:

{
  "id": 1,
  "name": "Special_char_owner_!@#$^&()`.testing",
  "photoUrls": [
    "https://petstore3.swagger.io/resources/photos/623389095.jpg"
  ],
  "tags": [],
  "status": "Sold"
}

For those of you who never tried Refit, I think that you should definitely check it out. It’s very easy to use



Atc.Cosmos - Azure Cosmos DB with A Touch of Class

For the past 6 years, I have been using Azure Cosmos DB as my go-to data store. Document databases make so much more sense for the things that I have been building over the past 6 years. The library Atc.Cosmos is the result of years of collective experience solving problems using the same patterns. Atc.Cosmos is a library for configuring containers in Azure Cosmos DB and provides easy, efficient, and convenient ways to read and write document resources.

Using Atc.Cosmos

Here’s an example usage of Atc.Cosmos in a Minimal API project targeting .NET 7.0

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.ConfigureCosmosDb();

var app = builder.Build();
app.MapGet(
    "/foo",
    (
        ICosmosReader<FooResource> reader,
        CancellationToken cancellationToken) =>
            reader
                .ReadAllAsync(FooResource.PartitionKey, cancellationToken)
                .ToBlockingEnumerable(cancellationToken)
                .Select(c => c.Bar))
    .WithName("ListFoo")
    .WithOpenApi();
app.MapGet(
    "/foo/{id}",
    async (
        ICosmosReader<FooResource> reader,
        string id,
        CancellationToken cancellationToken) =>
        {
            var foo = await reader.FindAsync(id, FooResource.PartitionKey, cancellationToken);
            return foo is not null ? Results.Ok(foo.Bar) : Results.NotFound(id);
        })
    .WithName("GetFoo")
    .WithOpenApi();
app.MapPost(
    "/foo",
    async (
        ICosmosWriter<FooResource> writer,
        [FromBody] Dictionary<string, object> data,
        CancellationToken cancellationToken) =>
        {
            var id = Guid.NewGuid().ToString();
            await writer.CreateAsync(
                new FooResource
                {
                    Id = id,
                    Bar = data,
                },
                cancellationToken);
            return Results.CreatedAtRoute("GetFoo", new { id });
        })
    .WithName("PostFoo")
    .WithOpenApi();

app.UseHttpsRedirection();
app.UseSwaggerUI();
app.UseSwagger();
app.Run();

Let’s break that down a bit and start with the IServiceCollection extension method ConfigureCosmosDb().

To use Atc.Cosmos you need to do the following:

  • Implement IConfigureOptions<CosmosOptions> to configure the database itself
  • Define Cosmos resource document types by deriving from CosmosResource or implementing ICosmosResource
  • Implement ICosmosContainerInitialize to define a CosmosDb container for every Cosmos resource document type
public static class ServiceCollectionExtensions
{
    public static void ConfigureCosmosDb(this IServiceCollection services)
    {
        services.ConfigureOptions<ConfigureCosmosOptions>();
        services.ConfigureCosmos(
            cosmosBuilder =>
            {
                cosmosBuilder.AddContainer<FooContainerInitializer, FooResource>("foo");
                cosmosBuilder.UseHostedService();
            });
    }
}

Here’s an example implementation of IConfigureOptions<CosmosOptions>

public class ConfigureCosmosOptions : IConfigureOptions<CosmosOptions>
{
    public void Configure(CosmosOptions options)
    {
        options.UseCosmosEmulator();
        options.DatabaseName = "SampleApi";
        options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    }
}

Here’s an example implementation of the ICosmosContainerInitializer interface for creating a container called foo:

public class FooContainerInitializer : ICosmosContainerInitializer
{
    public Task InitializeAsync(
        Database database,
        CancellationToken cancellationToken) =>
        database.CreateContainerIfNotExistsAsync(
            new ContainerProperties
            {
                PartitionKeyPath = "/pk",
                Id = "foo",
            },
            cancellationToken: cancellationToken);
}

Here’s an example Cosmos resource document type called FooResource that derives from CosmosResource

public class FooResource : CosmosResource
{
    public const string PartitionKey = "foo";
    public string Id { get; set; } = null!;
    public string Pk => PartitionKey;
    public Dictionary<string, object> Bar { get; set; } = new Dictionary<string, object>();
    protected override string GetDocumentId() => Id;
    protected override string GetPartitionKey() => Pk;
}

ICosmosReader< T >

Cosmos DB is very good at point-read operations, and this is cheap to do. The ICosmosReader<T> interface provides the following methods for point read operations:

Task<T> ReadAsync(
    string documentId, 
    string partitionKey, 
    CancellationToken cancellationToken = default);

Task<T?> FindAsync(
    string documentId, 
    string partitionKey, 
    CancellationToken cancellationToken = default);

ReadAsync() does a point read look-up on the document within the specified partition and throws a CosmosException with the Status code NotFound if the resource could not be found. FindAsync() on the other hand will return a null instance of T if the resource count not be found

You will notice that the majority of methods exposed in ICosmosReader<T> require the partition key to be specified. this is because read operations on Azure Cosmos DB are very cheap and efficient as long as you stay within a single partition.

ICosmosReader<T> provides methods for reading multiple documents out. This can be done by reading all the documents within a partition or running a query against the partition. Here are some methods that do exactly that:

IAsyncEnumerable<T> ReadAllAsync(
    string partitionKey, 
    CancellationToken cancellationToken = default);

IAsyncEnumerable<T> QueryAsync(
    QueryDefinition query, 
    string partitionKey, 
    CancellationToken cancellationToken = default);

As the name states, ReadAllAsync() reads all documents from the specified partition and returns an asynchronous stream of individual documents. QueryAsync() executes a QueryDefinition against the specified partition.

When working with large partitions, you will most likely want to use paging to read out data so that you can return a response to the consumer of your system as fast as possible. ICosmosReader<T> provides the following methods for paged queries:

Task<PagedResult<T>> PagedQueryAsync(
    QueryDefinition query,
    string partitionKey, 
    int? pageSize,
    string? continuationToken = default,
    CancellationToken cancellationToken = default);

When working with very large partitions, you might want to parallelize the processing of the documents you read from Cosmos DB, and this can be done by streaming a collection of documents instead of individual ones. ICosmosReader<T> provides the following methods for batch queries

IAsyncEnumerable<IEnumerable<T>> BatchReadAllAsync(
    string partitionKey,
    CancellationToken cancellationToken = default);

IAsyncEnumerable<IEnumerable<T>> BatchQueryAsync(
    QueryDefinition query,
    string partitionKey,
    CancellationToken cancellationToken = default);

Cross-partition queries are normally very inefficient, expensive, and slow. Regardless of these facts, there will be times when you will still need them. ICosmosReader<T> provides the following methods for performing cross-partition read operations. ICosmosReader<T> provides methods for executing a query, a paged query, or a batch query across multiple partitions

IAsyncEnumerable<T> CrossPartitionQueryAsync(
    QueryDefinition query,
    CancellationToken cancellationToken = default);

Task<PagedResult<T>> CrossPartitionPagedQueryAsync(
    QueryDefinition query,
    int? pageSize,
    string? continuationToken = default,
    CancellationToken cancellationToken = default);

IAsyncEnumerable<IEnumerable<T>> BatchCrossPartitionQueryAsync(
    QueryDefinition query,
    CancellationToken cancellationToken = default);

ICosmosWriter < T >

There are multiple ways to write to Cosmos DB and my preferred way is to do upserts. This is to create when not exist, otherwise, update. ICosmosWriter<T> provides methods for simple upsert operations and methods that includes retry attempts.

Task<T> WriteAsync(
    T document,
    CancellationToken cancellationToken = default);

Task<T> UpdateOrCreateAsync(
    Func<T> getDefaultDocument,
    Action<T> updateDocument,
    int retries = 0,
    CancellationToken cancellationToken = default);

Deleting a resource will usually involve knowing what resource to delete. ICosmosWriter<T> provides methods for deleting a resource that MUST exists and another method that returns true if the resource was successfully deleted, otherwise false

Task DeleteAsync(
    string documentId,
    string partitionKey,
    CancellationToken cancellationToken = default);

Task<bool> TryDeleteAsync(
    string documentId,
    string partitionKey,
    CancellationToken cancellationToken = default);

Unit Testing

The ICosmosReader<T> and ICosmosWriter<T> interfaces can easily be mocked, but there might be cases where you would want to fake it instead. For this purpose, you can use the FakeCosmosReader<T> or FakeCosmosWriter<T> classes from the Atc.Cosmos.Testing namespace contains the following fakes. For convenience, Atc.Cosmos.Testing provides the FakeCosmos<T> class which fakes both the reader and writer

Based on the example in the beginning of this post, let’s say we have a component called FooService which can do CRUD operations over the FooResource

public class FooService
{
    private readonly ICosmosReader<FooResource> reader;
    private readonly ICosmosWriter<FooResource> writer;

    public FooService(
        ICosmosReader<FooResource> reader,
        ICosmosWriter<FooResource> writer)
    {
        this.reader = reader;
        this.writer = writer;
    }

    public Task<FooResource?> FindAsync(
        string id,
        CancellationToken cancellationToken = default) =>
        reader.FindAsync(id, FooResource.PartitionKey, cancellationToken);

    public Task UpsertAsync(
        string? id = null,
        Dictionary<string, object>? data = null,
        CancellationToken cancellationToken = default) =>
        writer.UpdateOrCreateAsync(
            () => new FooResource { Id = id ?? Guid.NewGuid().ToString() },
            resource => resource.Data = data ?? new Dictionary<string, object>(),
            retries: 5,
            cancellationToken);
}

Using a combination of Atc.Cosmos.Testing and the Atc.Test library, unit tests using the fakes could look like this:

public class FooServiceTests
{
    [Theory]
    [AutoNSubstituteData]
    public async Task Should_Get_Existing_Data(
        [Frozen(Matching.ImplementedInterfaces)] FakeCosmos<FooResource> fakeCosmos,
        FooService sut,
        FooResource resource)
    {
        fakeCosmos.Documents.Add(resource);
        (await sut.FindAsync(resource.Id)).Should().NotBeNull();
    }

    [Theory]
    [AutoNSubstituteData]
    public async Task Should_Create_New_Data(
        [Frozen(Matching.ImplementedInterfaces)] FakeCosmos<FooResource> fakeCosmos,
        FooService sut,
        Dictionary<string, object> data)
    {
        var count = fakeCosmos.Documents.Count;
        await sut.UpsertAsync(data: data);
        fakeCosmos.Documents.Should().HaveCount(count + 1);
    }

    [Theory]
    [AutoNSubstituteData]
    public async Task Should_Update_Existing_Data(
        [Frozen(Matching.ImplementedInterfaces)] FakeCosmos<FooResource> fakeCosmos,
        FooService sut,
        FooResource resource,
        Dictionary<string, object> data)
    {
        fakeCosmos.Documents.Add(resource);
        await sut.UpsertAsync(resource.Id, data);

        fakeCosmos
            .Documents
            .First(c => c.Id == resource.Id)
            .Data
            .Should()
            .BeEquivalentTo(data);
    }
}

If you’re interested in the full source code then you can grab it here.



Generate REST API Clients using Visual Studio and Microsoft Kiota

A week ago while I was browsing around what’s trending on Github, I stumbled upon something called Microsoft Kiota. Kiota is a command line tool for generating an API client to call any OpenAPI described API you are interested in. Getting started was quite a good experience as documentation from project Kiota is quite decent, especially for Building SDK’s in .NET.

Since Kiota is a .NET Tool and distributed on nuget.org, installation is as simple as

dotnet tool install --global --prerelease Microsoft.OpenApi.Kiota

The command line arguments for Kiota is very straight forward and looks something like this:

kiota generate -d [relative path to OpenAPI spec file] -n [default namespace] -o [relative output path]

The code generated by Microsoft Kiota depends on the following NuGet packages:

Kiota is built to target .NET 7.0 so this is required to run Kiota. The code C# generated by Kiota builds on the following .NET versions:

  • .NET 7.0 and 6.0
  • .NET Standard 2.1 and 2.0
  • .NET Framework 4.8.1, 4.8, 4.7.2, 4.6.2

All this perfectly fits the requirements I have in my Visual Studio extension, REST API Client Code Generator, so I immediately got started with integrating Kiota in my extension

After generating code, the extension will install the required NuGet packages and configure the project to use a Custom Tool on the OpenAPI Swagger file

Currently my tool just installs and runs the Kiota CLI tool, so there is a slight pause in Visual Studio while the custom tool is running, because Visual Studio needs to start an external process and wait for the results

Here’s an example of how to use the Kiota generated code from the Swagger Petstore OpenAPI specifications example.

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

var authProvider = new AnonymousAuthenticationProvider();
var requestAdapter = new HttpClientRequestAdapter(authProvider);
requestAdapter.BaseUrl = "https://petstore3.swagger.io/api/v3";

var client = new ApiClient(requestAdapter);
var pet = await client.Pet["0"].GetAsync(c => c.Headers.Add("accept", "application/json"));

Console.WriteLine($"Name: {pet.Name}");
Console.WriteLine($"Category: {pet.Category.Name}");
Console.WriteLine($"Status: {pet.Status}");

The code above outputs the following:

Name: doggie
Category: Dogs
Status: Available

It is the equivalent of calling the Swagger Petstore API using cURL

curl -X 'GET' 'https://petstore3.swagger.io/api/v3/pet/0' -H 'accept: application/json'

which responds with

{
   "id":0,
   "category":{
      "id":1,
      "name":"Dogs"
   },
   "name":"doggie",
   "photoUrls":[
      "string"
   ],
   "tags":[
      {
         "id":0,
         "name":"string"
      }
   ],
   "status":"available"
}

I think Kiota is really promising and I think that you should also give it a shot. You can use it via my Visual Studio extension REST API Client Code Generator, my CLI tool Rapicgen, or via the Kiota CLI Tool directly