My new years resolution is to generate HTTP clients for services with an Open API spec. Microsoft released Kiota, a tool that generates C# classes from specs. In this blog, I'll be looking at dependency injection and resilience.
Caveats
There are many (many!) ways to implement and configure Kiota clients. In this blog I'll limit myself to the following:
- Services don't use any form of authentication.
- My clients are generated into the same folder as sub folders.
- The HTTP clients must use the Microsoft.Extensions.Http.Resilience package to implement resilience. This is borrowed from Implementing HTTP Resilience by Microsoft.
- I want a cookie cutter approach that makes it easier to replicate the most common pattern.
One script to rule them all
First, we'll create a script to generate our clients. In this example we'll generate 2 clients; one based on the Swagger IO Pet Store example and one based on a custom spec for the httpstat.us service.
#! "dotnet-script"
#nullable enable
// 1. Install Kiota
Console.WriteLine("🔄 Installing (or updating) Kiota tool...");
Run("dotnet tool install --global Microsoft.OpenApi.Kiota", Environment.CurrentDirectory, quiet: false);
// 2. Make sure HTTP project is present
Console.WriteLine("🔄 Ensure HTTP projects is configured correctly...");
if (!Directory.Exists(HttpClientsDir))
throw new DirectoryNotFoundException($"❌ HTTP client project folder not found at '{HttpClientsDir}'");
// 3. Install packages
Run("dotnet add package Microsoft.Extensions.DependencyInjection");
Run("dotnet add package Microsoft.Extensions.DependencyInjection.Abstractions");
Run("dotnet add package Microsoft.Extensions.Options");
Run("dotnet add package Microsoft.Extensions.Http.Resilience");
Run("dotnet add package Microsoft.Extensions.Resilience");
Run("dotnet add package Microsoft.Kiota.Bundle");
// 4. Generate Kiota clients
Console.WriteLine("🔄 Generating Kiota clients...");
GenerateKiotaClient(
openApiUrl: "https://petstore.swagger.io/v2/swagger.json",
clientName: "PetStore",
includePath: "/pet/**"
);
GenerateKiotaClient(
openApiUrl: "httpstatus-open-api.yml",
clientName: "HttpStatus"
);
// -- Config ---
static string Namespace => "Ktt.Resilience.Clients.Kiota";
static string HttpClientsDir => Path.GetFullPath(Namespace);
static string Folder = "HttpClients";
// --- Helpers ---
void Run(string fullCommand, string? workingDir = null, bool quiet = true)
{
var parts = fullCommand.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var psi = new ProcessStartInfo
{
FileName = parts[0],
Arguments = parts.Length > 1 ? parts[1] : "",
WorkingDirectory = workingDir ?? HttpClientsDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var proc = Process.Start(psi)
?? throw new Exception($"Failed to start: {parts[0]}");
string stdout = proc.StandardOutput.ReadToEnd();
string stderr = proc.StandardError.ReadToEnd();
proc.WaitForExit();
if (!quiet && !string.IsNullOrWhiteSpace(stdout))
Console.WriteLine(stdout);
if (!string.IsNullOrWhiteSpace(stderr))
Console.Error.WriteLine(stderr);
if (proc.ExitCode != 0)
throw new Exception($"{parts[0]} exited with code {proc.ExitCode}");
}
void GenerateKiotaClient(string openApiUrl, string clientName, string includePath = "/**")
{
if (!openApiUrl.Contains("://"))
openApiUrl = Path.Combine("..", openApiUrl);
var outputDir = string.IsNullOrEmpty(Folder) ? clientName : $"{Folder}/{clientName}";
var @namespace = string.IsNullOrEmpty(Folder) ? Namespace : $"{Namespace}.{Folder}";
Console.WriteLine($"▶ Generating client '{clientName}'...");
Run($"""
kiota generate
--openapi {openApiUrl}
--language CSharp
--output {outputDir}
--namespace-name {@namespace}.{clientName}
--class-name {clientName}Client
--exclude-backward-compatible
--clean-output
--clear-cache
--include-path {includePath}
""".ReplaceLineEndings(" ").Trim(), quiet: false);
Console.WriteLine($"✔ Generated client: '{clientName}'");
}
We can generate the client with the .NET Script tool:
dotnet tool install -g dotnet-script
dotnet script ./generate-kiota-clients.csxI've used a Powershell script in a previous version of this article. The main drawback is the dependency upon PowerShell which is technically cross-platform, but functionally a big hurdle.
Kiota & Dependency Injection
Now, the official guide on dependency injection states that we should add extension methods and create a factory to create our final client. As most of my services will not include authentication, it does not make sense to create a factory for every client. Instead I'll provide some reusable extension methods.
First, we'll need to add the extension methods mentioned in the docs:
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;
}
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;
}The script in the docs references some obsolete methods. I've replaced them with the recommended methods.
More extension methods ahead
Now, we can create 2 extension methods:
- A method that creates HTTP client with configuration, resilience and Kiota handlers added to them. Let's name it
AddKiotaHttpClientWithResilienceHandler(sorry for the long name). - A method that takes the HTTP client and creates a
HttpClientRequestAdapter, namedAddKiotaClient.
A named HTTP client
I have not found a way to bind the HTTP client directly to Kiota client class. The next best thing is to created a named HTTP client that we can use later on.
public static IHttpStandardResiliencePipelineBuilder AddKiotaHttpClientWithResilienceHandler(
this IServiceCollection services,
string sectionName
)
{
services.AddKiotaHandlers();
var httpClientBuilder = services
// bind the configuration section
.AddNamedOptionsForHttpClient<HttpClientOptions>(sectionName)
// add the HttpClient itself, configure the base URL
.AddHttpClient(sectionName, (serviceProvider, client) =>
{
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<HttpClientOptions>>();
var config = monitor.Get(sectionName);
if (config?.BaseUrl != null)
{
client.BaseAddress = new Uri(config.BaseUrl);
}
});
var resilienceBuilder = httpClientBuilder.AddStandardResilienceHandler();
httpClientBuilder.AttachKiotaHandlers();
return resilienceBuilder;
}This code looks very similar to the code of Dependency injection 2: http client with resilience pipeline. Here we do not provide a generic class to the .AddHttpClient, we just add a name to the client. Note how we AttachKiotaHandlers to our resilienceBuilder.
This code allows us to get the client from IServiceProvider sp:
var factory = sp.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient(sectionName);A Kiota Client factory
Next, we can build a Kiota client with an anonymous authentication provider:
public static IHttpStandardResiliencePipelineBuilder AddKiotaClient<TClass>(
this IServiceCollection services,
string sectionName
)
where TClass : class
{
var resilienceBuilder = services.AddKiotaHttpClientWithResilienceHandler(sectionName);
services
.AddSingleton<AnonymousAuthenticationProvider>()
.AddTransient(sp =>
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient(sectionName);
var provider = sp.GetRequiredService<AnonymousAuthenticationProvider>();
var adapter = new HttpClientRequestAdapter(provider, httpClient: client);
var instance = Activator.CreateInstance(typeof(TClass), adapter);
return (TClass)instance!;
});
return resilienceBuilder;
}Unfortunately, it is not possible to constraint the generic TClass to have a constructor that takes a IRequestAdapter. Let's do the next best thing and use the Activator that uses reflection to create the object. If the constructor is missing on the object, we'll get a System.MissingMethodException.
Hook it up
The last things we should do is configure those classes in our dependency injection:
services.AddKiotaClient<PetStoreClient>("HttpClients:PetStore");
services.AddKiotaClient<HttpStatusClient>("HttpClients:HttpStatus")
.Configure(config =>
{
config.Retry.OnRetry = async args =>
{
Console.WriteLine($"Retry {args.AttemptNumber}: Retrying after {args.RetryDelay} due to {args.Outcome.Result?.StatusCode}");
await Task.CompletedTask;
};
});And we can add the following to our configuration:
{
"HttpClients": {
"PetStore": {
"BaseUrl": "https://petstore.swagger.io/v2"
},
"HttpStatus": {
"BaseUrl": "https://httpstat.us",
"Retry": {
"MaxRetryAttempts": 4
}
}
}
}Conclusion
It is not very hard to generate your C# client based on Open API specs and add it to your application in such a way that you have both configuration and resilience. I'm still a bit on the fence when I look at the result that is generated by Kiota. It looks pretty verbose. I agree with some of the hate Kiota gets online.
Others point out that the way the generated code looks, might have more to do with the specification you used. I ended up adding more definitions to my YML and that already made the client look way better 😮💨.
In Node.js / TypeScript I use swagger-typescript-api, which for now feels a bit easier to use.
The code is in GitHub, so check it out: code gallery / 12. HTTP and resilience.
Changelog
- Improved the client generation script. It is now cross-platform using a csx-script.
- Initial article.