You have an OpenAPI spec. You need a typed client. The question is how you get from one to the other without drowning in boilerplate or chaining yourself to a runtime reflection library that fights your trimming and AOT goals.
For years, the .NET ecosystem defaulted to NSwag or AutoRest for client generation, or to Refit for a more declarative approach. They all work, but they each carry trade-offs: NSwag produces monolithic single-file clients that balloon for large APIs, AutoRest targets Azure-shaped services and struggles with arbitrary specs, and Refit requires you to hand-author the interface contract yourself. Microsoft's answer to this fragmentation is Kiota -- a code generator that takes an OpenAPI description and produces a lightweight, strongly typed client with a fluent request builder API, built-in middleware support, and first-class trimming compatibility.
With over 100 million NuGet downloads of its abstractions library and backing from the same team that builds the Microsoft Graph SDKs, Kiota has quietly become the default choice for generating API clients in .NET. If you haven't tried it yet, here's what you need to know.
What Kiota actually generates
Most OpenAPI generators take the flat list of path operations and produce a single class with one method per endpoint. For a small API that's fine. For something like the GitHub API or Microsoft Graph, you end up with a client class containing hundreds of methods and no meaningful way to discover the right one.
Kiota takes a different approach. It parses the OpenAPI document's path hierarchy and generates a tree of request builder classes. Each segment of the URL gets its own class, and you navigate the API using a fluent, chainable syntax that mirrors the resource structure:
public class GitHubService(GitHubClient client)
{
public async Task<Release?> GetLatestReleaseAsync(string owner, string repo)
{
// The fluent API mirrors the URL: /repos/{owner}/{repo}/releases/latest
return await client.Repos[owner][repo].Releases["latest"].GetAsync();
}
}
This isn't just syntactic sugar. The request builder pattern means your IDE's autocomplete can guide you through the API's resource hierarchy. Type client.Repos["dotnet"]["runtime"]. and you'll see exactly which sub-resources and operations are available -- no documentation tab-switching required.
The generated code itself is intentionally thin. Kiota doesn't embed HTTP logic, serialisation, or authentication into the generated files. Instead, it generates classes that depend on a small set of core abstractions (Microsoft.Kiota.Abstractions), with concrete implementations provided by separate packages. This separation means the generated code is stable, testable, and swappable.
Getting started
Install Kiota as a .NET global tool:
dotnet tool install -g Microsoft.OpenApi.Kiota
For a quick test, create a console project and add the bundle package, which pulls in all the runtime dependencies you'll need:
dotnet new console -o WeatherClient
cd WeatherClient
dotnet add package Microsoft.Kiota.Bundle
Now generate a client from an OpenAPI spec. Suppose you have a weather-api.yml describing a simple forecast service:
kiota generate -l CSharp -d ./weather-api.yml -c WeatherApiClient -n WeatherClient.Api -o ./Api
This produces a set of files under ./Api:
- A root client class (
WeatherApiClient) - Request builder classes for each path segment
- Model classes for request and response schemas
- A
kiota-lock.jsonfile that records the generation parameters
// TIP
The lock file should be committed to source control alongside the generated code. On subsequent runs, Kiota skips regeneration if neither the spec nor the parameters have changed.
Selective generation with path filters
One of Kiota's most practical features is the ability to generate clients for only the endpoints you actually use. For large APIs, this dramatically reduces the generated code surface:
kiota generate -l CSharp \
-d https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json \
-c GitHubClient \
-n MyApp.GitHub \
-o ./GitHub \
--include-path "/repos/{owner}/{repo}/releases/**" \
--include-path "/repos/{owner}/{repo}/issues/**" \
--exclude-backward-compatible
The --include-path flag accepts glob patterns that match against the OpenAPI path structure. You can combine multiple includes and excludes, and even filter by HTTP method by appending #METHOD to the pattern (e.g. **/releases/**#GET to generate only GET operations).
The --exclude-backward-compatible flag strips out obsolete compatibility code that Kiota emits by default for existing clients. For new projects, always use this flag -- it produces cleaner, smaller output.
// NOTE
You can preview the path tree before generating anything using kiota show -d <spec-url>. This displays the full resource hierarchy so you can decide which paths to include.
Wiring up with dependency injection
In a real ASP.NET Core application, you'll want the generated client registered in the DI container and backed by IHttpClientFactory for proper connection management. This requires a bit of plumbing, but the pattern is straightforward.
First, create extension methods that register Kiota's default middleware handlers:
using Microsoft.Kiota.Http.HttpClientLibrary;
public static class KiotaServiceCollectionExtensions
{
public static IServiceCollection AddKiotaHandlers(this IServiceCollection services)
{
var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
foreach (var handler in kiotaHandlers)
{
services.AddTransient(handler);
}
return services;
}
public static IHttpClientBuilder AttachKiotaHandlers(this IHttpClientBuilder builder)
{
var kiotaHandlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
foreach (var handler in kiotaHandlers)
{
builder.AddHttpMessageHandler(sp => (DelegatingHandler)sp.GetRequiredService(handler));
}
return builder;
}
}
Then create a factory that produces the typed client from the injected HttpClient:
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;
public class GitHubClientFactory(HttpClient httpClient)
{
private readonly IAuthenticationProvider _authProvider = new AnonymousAuthenticationProvider();
public GitHubClient GetClient()
=> new(new HttpClientRequestAdapter(_authProvider, httpClient: httpClient));
}
Finally, wire everything together in Program.cs:
builder.Services.AddKiotaHandlers();
builder.Services.AddHttpClient<GitHubClientFactory>((sp, client) =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
}).AttachKiotaHandlers();
builder.Services.AddTransient(sp => sp.GetRequiredService<GitHubClientFactory>().GetClient());
Now you can inject GitHubClient directly into your services or minimal API handlers:
app.MapGet("/releases/latest", async (GitHubClient client) =>
{
var release = await client.Repos["dotnet"]["runtime"].Releases["latest"].GetAsync();
return Results.Ok(new { release?.TagName, release?.Name, release?.PublishedAt });
});
The middleware pipeline
Kiota doesn't just generate typed wrappers around HttpClient.SendAsync. The generated client is backed by a middleware pipeline that handles cross-cutting concerns out of the box:
- RetryHandler -- automatically retries failed requests with exponential back-off (configurable, defaults to 3 retries)
- RedirectHandler -- follows HTTP redirects transparently
- ParametersNameDecodingHandler -- decodes URL-encoded parameter names that some APIs require
- UserAgentHandler -- sets a consistent user-agent header
- HeadersInspectionHandler -- provides access to request and response headers for debugging
Because these are standard DelegatingHandler instances, you can customise the pipeline by adding your own handlers or replacing the defaults. The DI registration pattern shown above makes this trivial -- just insert additional handlers in the IHttpClientBuilder chain.
Authentication
For APIs that require authentication, Kiota provides a BaseBearerTokenAuthenticationProvider that expects an IAccessTokenProvider implementation. This separates token acquisition from the HTTP pipeline, which is exactly the right boundary for most OAuth and Azure AD scenarios:
using Microsoft.Kiota.Abstractions.Authentication;
public class ServiceTokenProvider(ITokenService tokenService) : IAccessTokenProvider
{
public AllowedHostsValidator AllowedHostsValidator { get; } = new();
public async Task<string> GetAuthorizationTokenAsync(
Uri uri,
Dictionary<string, object>? additionalAuthenticationContext = null,
CancellationToken cancellationToken = default)
{
return await tokenService.GetAccessTokenAsync(cancellationToken);
}
}
For Azure-hosted APIs, the Microsoft.Kiota.Authentication.Azure package integrates directly with Azure.Identity, so you can pass a TokenCredential and let the library handle token caching and refresh.
Searching and discovering APIs
Kiota includes a built-in registry search that can locate OpenAPI descriptions from public repositories:
# Search for an API by name
kiota search github
# Get details about a specific result
kiota search apisguru::github.com
# Download the spec locally for inspection
kiota download apisguru::github.com -o ./github-api.json
This is surprisingly useful for prototyping. Rather than hunting for API documentation, you can search, preview the path tree with kiota show, and generate a focused client in minutes.
How it compares to the alternatives
Kiota vs NSwag: NSwag generates a single monolithic client class from the flat operation list. Kiota generates a hierarchical request builder tree. For small APIs the difference is cosmetic; for large APIs, Kiota's approach is dramatically more navigable. NSwag offers more configuration knobs and template customisation, but that flexibility comes with complexity. Kiota is deliberately opinionated -- fewer settings, one correct way to generate.
Kiota vs Refit: Refit requires you to define the API interface yourself using attributes. This works well when you want fine-grained control or when you're consuming a small, stable API. Kiota generates everything from the spec, which eliminates the manual maintenance burden but means you're trusting the OpenAPI document to be accurate. If your API has a good spec, Kiota wins on productivity. If the spec is unreliable, Refit's manual approach gives you more control.
Kiota vs AutoRest: AutoRest is the predecessor, purpose-built for Azure SDKs. It's powerful but heavy, generates substantial boilerplate, and is less suited to arbitrary third-party APIs. Kiota is its spiritual successor with a much lighter footprint.
Common pitfalls
Forgetting --exclude-backward-compatible on new projects. Without this flag, Kiota generates extra obsolete members for backward compatibility with older Kiota versions. For new projects, this is pure noise. Always include --ebc (the short form) when generating a fresh client.
Not using path filters for large APIs. Generating a client for the entire Microsoft Graph or GitHub API produces thousands of files. Use --include-path to scope generation to just the endpoints you need. You can always regenerate with additional paths later.
Registering the client as a singleton. The generated client wraps an HttpClientRequestAdapter which wraps an HttpClient. If you're using IHttpClientFactory (and you should be), register the client factory as a typed HTTP client and resolve the client as transient, not singleton. Singleton registration defeats the purpose of IHttpClientFactory's connection pooling and DNS rotation.
Assuming the generated models handle all edge cases. Kiota generates models from the OpenAPI schema, but many real-world APIs have schemas that are incomplete or inaccurate. If deserialization fails on a particular response, check the spec first -- you may need to fix the schema or add a custom serialiser for that type.
Ignoring the lock file. The kiota-lock.json file records your generation parameters. Commit it to source control. It enables kiota update to regenerate the client with identical settings when the upstream spec changes, and it lets your team reproduce the exact same generation output.
Summary
- Kiota generates a tree of request builder classes that mirrors the API's URL hierarchy, making large APIs navigable through IDE autocomplete
- Install via
dotnet tool install -g Microsoft.OpenApi.Kiotaand addMicrosoft.Kiota.Bundleto your project - Use
--include-pathand--exclude-pathto generate clients for only the endpoints you need - The generated code is backed by a middleware pipeline with retry, redirect, and header inspection handlers built in
- Wire into ASP.NET Core DI using
IHttpClientFactoryfor proper connection lifecycle management - Always use
--exclude-backward-compatiblefor new projects and commit thekiota-lock.jsonfile - Authentication is cleanly separated via
IAccessTokenProvider, with first-party Azure Identity support available