Oleksii Nikiforov
Posted on March 24, 2024
This post provides a deep dive into OpenAPI client generation with Kiota, covering topics such as SDK generation, dependency injection, typed HTTP clients, cross-cutting concerns, and testing.
Source code: https://github.com/NikiforovAll/kiota-getting-started
Table of Contents:
- Introduction
- Setup
- Generate HTTP Client for ASP.NET Core applications
- Dependency Injection, Typed HTTP Clients, and
IHttpClientFactory
- Cross-Cutting Concerns and Resilience
- Testing
- Conclusion
- References
Introduction
In the previous blog post, we learned the basics of Kiota. In this post, I want to share more details on how to apply it in production scenarios in a more sophisticated manner.
I will show you the following aspects of successful SDK development:
- Generation of SDKs for ASP.NET Core applications
- Dependency Injection, Typed HTTP Clients, and
IHttpClientFactory
- Cross-Cutting Concerns
- Testing
Setup
We want to create an App.Client
API project that calls the App
trending API, which we've built in a previous post. We will use .NET Aspire to glue everything together.
Aspire is a powerful library for .NET applications that simplifies the process of service discovery, configuration, and registration. It provides a set of tools and abstractions that allow developers to easily connect their services and clients in a decoupled manner.
In the context of our App.Client
API project, we can use Aspire to automatically discover and register the App
trending API. This means that our client application doesn't need to know the exact location or configuration of the App
API - Aspire will handle this for us.
Here is App/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var services = builder.Services;
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapTrendingEndpoints();
app.Run();
And here is App.Client/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var services = builder.Services;
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapGet("/my/trending", () =>
{
// TODO:
});
app.Run();
Generate HTTP Client for ASP.NET Core applications
ASP.NET Core has built-in support for OpenAPI, also known as Swagger. This support allows developers to generate API documentation directly from their code. The quality of the generated OpenAPI specification largely depends on the amount of metadata provided by the developer in the form of attributes and comments. The more detailed and accurate this metadata is, the more precise and useful the generated OpenAPI specification will be. This, in turn, improves the quality of the client SDKs generated from the OpenAPI specification.
Generate OpenAPI specification automatically
We can generate OpenAPI specification at build time from the code in ASP.NET Core by using Microsoft.Extensions.ApiDescription.Server
dotnet add ./src/App package Microsoft.Extensions.ApiDescription.Server
And add configuration to App.csproj
:
<PropertyGroup>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../App.Sdk/OpenApi</OpenApiDocumentsDirectory>
<OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
<OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
</PropertyGroup>
Note, I decided to set up the output location outside of the project - src/App.Sdk/OpenApi
. We can combine the OpenAPI generation with Kiota App.Sdk
client generation:
Add the next Target to App.csproj
. Every time we change App
, the App.Sdk
is regenerated. This makes the process fully automatic. Personally, I like this developer experience because I can always see the changed files in the source code.
<Target Name="OpenAPI" AfterTargets="Build" Condition="$(Configuration)=='Debug'">
<Exec Command="dotnet kiota generate -l CSharp --output ../App.Sdk --namespace-name App.Sdk --class-name AppApiClient --exclude-backward-compatible --openapi ../App.Sdk/OpenApi/App.json" WorkingDirectory="$(ProjectDir)" />
</Target>
Use OpenAPI Specification
The AppApiClient
class is a central part of the generated SDK. It provides methods for making HTTP requests to the API endpoints defined in our application. In the example below, we are using the AppApiClient
to make a GET request to the /trending/{country}
endpoint.
The AppApiClient
takes an IRequestAdapter
as a parameter in its constructor. This adapter is responsible for sending HTTP requests and receiving HTTP responses. In this example, we are using the HttpClientRequestAdapter
.
We also provide an IAuthenticationProvider
to the HttpClientRequestAdapter
. This provider is responsible for providing the necessary authentication credentials for the API requests. In this example, we are using the AnonymousAuthenticationProvider
, which does not provide any authentication credentials.
Finally, we set the BaseUrl
of the HttpClientRequestAdapter
. This URL is used as the base for all API requests made by the AppApiClient
.
// App.Client/Program.cs
app.MapGet("/my/trending", async () =>
{
var authProvider = new AnonymousAuthenticationProvider();
var requestAdapter = new HttpClientRequestAdapter(authProvider, httpClient: httpClient)
{
BaseUrl = "http://app"
};
var client = new AppApiClient(requestAdapter);
var response = await client.Trending["US"].GetAsync();
return response.Value.Select(topic => topic.Query.Text);
});
💡Note, the BaseUrl
is based on Aspire convention. Here is App.AppHost
:
// App.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
var appProject = builder.AddProject<Projects.App>("app");
builder.AddProject<Projects.App_Client>("app-client")
.WithReference(appProject);
builder.Build().Run();
Demo
dotnet run --project ./src/App.AppHost
curl -s http://localhost:5102/my/trending | jq
And here is trace example from Aspire.Dashboard:
💡Note, Http instrumentation/tracing works only for clients resolved through DI container. Later, I will show you how to properly add AppApiClient
so it uses HttpClient
from IHttpClientFactory
.
Dependency Injection, Typed HTTP Clients, and IHttpClientFactory
As you may already know, IHttpClientFactory
in .NET provides a better way to work with HttpClient
, as it addresses well-known client lifetime issues. In this guide, I will demonstrate how to use a client generated by Kiota as a typed client. For more information, refer to the Typed-client approach.
// App.Client/Program.cs
services.AddSingleton<IAuthenticationProvider, AnonymousAuthenticationProvider>(
_ => new AnonymousAuthenticationProvider());
services.AddHttpClient<AppApiClient>()
.AddTypedClient((httpClient, sp) =>
{
var authenticationProvider = sp.GetRequiredService<IAuthenticationProvider>();
var requestAdapter = new HttpClientRequestAdapter(authProvider , httpClient: httpClient)
{
BaseUrl = "http://app"
};
return new AppApiClient(requestAdapter);
})
.ConfigurePrimaryHttpMessageHandler(_ =>
{
var defaultHandlers = KiotaClientFactory.CreateDefaultHandlers();
var defaultHttpMessageHandler = KiotaClientFactory.GetDefaultHttpMessageHandler();
return KiotaClientFactory.ChainHandlersCollectionAndGetFirstLink(
defaultHttpMessageHandler, [.. defaultHandlers])!;
});
The code above is configuring an HttpClient
for the AppApiClient
in a .NET application. The AddTypedClient
method is used to further configure the HttpClient
instance. The advantage of using typed clients is that they provide a clear contract for HTTP interactions and can be easily mocked for testing.
By default, Kiota provides the default list of DelegatingHandler
s and HttpMessageHandler
. It is good idea to include them, but you can definitely opt-out if it interferes with your code.
public static IList<DelegatingHandler> CreateDefaultHandlers()
{
return new List<DelegatingHandler>
{
new RetryHandler(),
new RedirectHandler(),
new ParametersNameDecodingHandler(),
new UserAgentHandler(),
new HeadersInspectionHandler()
};
}
The ConfigurePrimaryHttpMessageHandler
method is used to set up the primary HttpMessageHandler
for the HTTP client. This handler is responsible for sending HTTP requests and receiving HTTP responses.
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(
this IHttpClientBuilder builder,
Func<IServiceProvider, HttpMessageHandler> configureHandler);
Here's how it works:
Create a list of default
DelegatingHandler
instances usingKiotaClientFactory.CreateDefaultHandlers()
. ADelegatingHandler
is a special type ofHttpMessageHandler
that can be used to process or manipulate HTTP requests and responses in some way before they are sent or after they are received.Get the default
HttpMessageHandler
usingKiotaClientFactory.GetDefaultHttpMessageHandler()
. This handler is the one that will actually send the HTTP request and receive the response.Chain these handlers together using
KiotaClientFactory.ChainHandlersCollectionAndGetFirstLink()
. This method takes the defaultHttpMessageHandler
and the list ofDelegatingHandler
instances, and chains them together so that each request or response will pass through each handler in turn. The method returns the first link in this chain, which is then used as the primaryHttpMessageHandler
for the HTTP client.
Finally, here is how to use AppApiClient
from DI:
// App.Client/Program.cs
app.MapGet("/my/trending", async (AppApiClient client) =>
{
var response = await client.Trending["US"].GetAsync();
return response.Value.Select(topic => topic.Query.Text);
});
Cross-Cutting Concerns and Resilience
In distributed applications, communication between services is a critical aspect. However, this communication is not always reliable. Network issues, high latency, or the unavailability of a service can lead to failures. This is where resilience comes into play.
Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. It is a crucial tool for building reliable applications that can withstand the unpredictable nature of the network.
.NET 8, .NET team has made substantial advancements to simplify the integration of resilience into your applications - meet new resilience packages:
# Extensions to the Polly libraries to enrich telemetry with metadata and exception summaries
dotnet add package Microsoft.Extensions.Resilience
# Resilience mechanisms for HttpClient built on the Polly framework
dotnet add package Microsoft.Extensions.Http.Resilience
For an out-of-the-box experience, use the AddStandardResilienceHandler
extension on the IHttpClientBuilder
like this:
IHttpStandardResiliencePipelineBuilder resilienceBuilder = services
.AddHttpClient("my-client")
.AddStandardResilienceHandler(options =>
{
// Configure standard resilience options here
});
IHttpStandardResiliencePipelineBuilder
allows to configure underlying multiple resilience strategies with options to send the requests and handle any transient errors.
In the context of our example, it's worth noting that it already has the StandardResilienceHandler
built-in. This is due to the fact that Aspire has opinionated defaults on how to build distributed applications. This means that it comes with a set of pre-configured settings that are designed to handle common scenarios in a distributed environment.
The StandardResilienceHandler
is a part of these defaults. It is a resilience strategy that includes a combination of retry, circuit breaker, and timeout policies. These policies are designed to handle transient faults in a graceful manner, ensuring that your application remains responsive and reliable in the face of network issues, high latency, or service unavailability.
The StandardResilienceHandler
is automatically applied to all HTTP clients that are created through the IHttpClientFactory
. This means that you don't have to manually configure these resilience policies for each client. Instead, they are applied consistently across your application, ensuring that all HTTP communication is resilient.
Here is partial content from App.ServiceDefaults/Extensions.cs
:
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
http.UseServiceDiscovery();
});
return builder;
}
Here is an example of how to use the AddStandardResilienceHandler
method on top of IHttpClientBuilder
returned by AddTypedClient
method for fine-grained control and customization of resiliency per-client:
services.AddHttpClient<AppApiClient>().AddTypedClient()
.AddStandardResilienceHandler().Configure(cfg =>
{
cfg.Retry.MaxRetryAttempts = 3;
cfg.Retry.UseJitter = true;
cfg.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
💡Note, Microsoft.Extensions.Http.Resilience
allows to build custom pipelines and gives you full control over how to manage resiliency in your applications.
Testing
For unit testing, it's suggested to use mock versions of the HTTP transport layer to manage API responses. In Kiota API clients, this layer is in a request adapter. By mocking the request adapter, you can control the API responses.
public class TrendingTopicTests
{
[Fact]
public async Task TrendingTopic_GetUS_SuccessAsync()
{
// Arrange
var adapter = Substitute.For<IRequestAdapter>();
adapter.SetupSendAsyncWithResponse(new TrendingTopics() { Value = [] });
var newsSearchApiClient = new NewsSearchApiClient(adapter);
// Act
var response = await newsSearchApiClient
.News
.Trendingtopics
.GetAsync(r => r.QueryParameters.Cc = "US");
// Assert
Assert.NotNull(response);
}
}
public static class Utils
{
public static void SetupSendAsyncWithResponse<T>(
this IRequestAdapter adapter, T response) where T : IParsable
{
adapter.SendAsync<T>(
Arg.Any<RequestInformation>(),
Arg.Any<ParsableFactory<T>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.ReturnsForAnyArgs(response);
}
}
💡The process of mocking IRequestAdapter
can be somewhat complex, particularly as it requires reliance on models generated by Kiota. To streamline testing, I recommend encapsulating the use of generated clients within a simple interface and then mocking this interface. This approach not only simplifies testing but also aligns well with the principles of Clean Architecture. By doing so, we avoid the need to mock the Kiota code directly, enhancing the maintainability and readability of our tests.
Conclusion
In conclusion, Kiota is not just a powerful tool, but a practical solution for modern development challenges. It's ready to be integrated into your production code. It's time to embrace Kiota and let it transform your development workflow.
References
- https://nikiforovall.github.io/dotnet/aspnetcore/2024/03/22/kiota-guide-introduction.html
- https://www.meziantou.net/generate-openapi-specification-at-build-time-from-the-code-in-asp-net-core.htm
- https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-simplifying-cloud-native-development-with-dotnet-8/
- https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/
- https://learn.microsoft.com/en-us/openapi/kiota/testing
Posted on March 24, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.