Documentation
adrs/021-kiota-api-client-generation.md
ADR-021: Microsoft Kiota for API Client Generation
Status
Accepted
Updated: 2026-01-08 (db-manager consolidation)
Context
In the Dynaplex microservices architecture, services need to communicate with each other through HTTP APIs. Manually writing API clients is error-prone, time-consuming, and leads to inconsistencies. We need an automated approach to generate strongly-typed, maintainable API clients from OpenAPI specifications.
Microsoft Kiota is a modern API client generator that:
- Generates lightweight, strongly-typed clients
- Supports multiple languages (C#, TypeScript, Python, etc.)
- Creates minimal dependencies
- Provides excellent IDE support
- Integrates well with .NET ecosystem
Decision
We will use Microsoft Kiota to automatically generate API clients for all inter-service communication in the Dynaplex architecture. Each service will have a corresponding .ApiClient project containing the generated client code.
Project structure:
engines/catalog/
├── src/
│ ├── Acsis.Dynaplex.Engines.Catalog/ # Service implementation
│ ├── Acsis.Dynaplex.Engines.Catalog.Abstractions/ # Shared models
│ ├── Acsis.Dynaplex.Engines.Catalog.Database/ # EF Core DbContext and entities
│ └── Acsis.Dynaplex.Engines.Catalog.ApiClient/ # Kiota-generated client
Note (2026-01-08): Per-component
.DbMigratorprojects have been replaced by a centralizeddb-managercomponent. See ADR-036.
Consequences
Positive
- Type Safety: Strongly-typed clients with compile-time checking
- Consistency: Uniform client code across all services
- Productivity: No manual client code writing
- Maintenance: Clients automatically updated with API changes
- Documentation: IntelliSense from OpenAPI descriptions
- Testing: Generated clients are easily mockable
Negative
- Build Complexity: Additional build step for generation
- Debugging: Generated code can be harder to debug
- Customization Limits: Less flexibility than hand-written clients
- Learning Curve: Team must understand Kiota patterns
- Regeneration: Must regenerate when APIs change
Neutral
- File Size: Generated files can be large
- Source Control: Must decide whether to commit generated code
- Versioning: Client versions tied to API versions
Implementation Notes
Kiota Configuration (.kiota.json)
{
"descriptionLocation": "https://localhost:7001/swagger/v1/swagger.json",
"outputPath": "./Generated",
"clientClassName": "CatalogApiClient",
"clientNamespaceName": "Acsis.Dynaplex.Engines.Catalog.ApiClient",
"language": "csharp",
"usesBackingStore": false,
"includeAdditionalData": true,
"serializers": [
"Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory"
],
"deserializers": [
"Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory"
],
"structuredMimeTypes": [
"application/json"
],
"includePatterns": [
"/api/catalog/**"
],
"excludePatterns": [
"/health/**",
"/swagger/**"
]
}
Project File Configuration
<!-- Acsis.Dynaplex.Engines.Catalog.ApiClient.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Kiota dependencies -->
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.x.x" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.x.x" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.x.x" />
</ItemGroup>
<ItemGroup>
<!-- Reference shared abstractions -->
<ProjectReference Include="../Acsis.Dynaplex.Engines.Catalog.Abstractions/Acsis.Dynaplex.Engines.Catalog.Abstractions.csproj" />
</ItemGroup>
<!-- Kiota generation target -->
<Target Name="GenerateClient" BeforeTargets="Build">
<Exec Command="kiota generate -c .kiota.json" />
</Target>
</Project>
Client Usage Example
// Service registration
builder.Services.AddScoped<CatalogApiClient>(provider =>
{
var httpClient = provider.GetRequiredService<IHttpClientFactory>()
.CreateClient("catalog");
var authProvider = new BearerTokenAuthenticationProvider(
provider.GetRequiredService<ITokenService>());
var requestAdapter = new HttpClientRequestAdapter(authProvider, httpClient);
return new CatalogApiClient(requestAdapter);
});
// Usage in another service
public class OrderService
{
private readonly CatalogApiClient _catalogClient;
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// Get asset details from Catalog service
var asset = await _catalogClient.Api.Catalog.Assets[request.AssetId]
.GetAsync();
if (asset == null)
throw new NotFoundException($"Asset {request.AssetId} not found");
// Create order with asset details
var order = new Order
{
AssetId = asset.Id,
AssetName = asset.Name,
// ...
};
return order;
}
}
Authentication Provider
public class BearerTokenAuthenticationProvider : IAuthenticationProvider
{
private readonly ITokenService _tokenService;
public BearerTokenAuthenticationProvider(ITokenService tokenService)
{
_tokenService = tokenService;
}
public async Task AuthenticateRequestAsync(RequestInformation request)
{
var token = await _tokenService.GetServiceTokenAsync();
request.Headers.Add("Authorization", $"Bearer {token}");
}
}
Generation Script
#!/bin/bash
# generate-clients.sh
echo "Generating API clients..."
# Catalog Service
cd engines/catalog/src/Acsis.Dynaplex.Engines.Catalog.ApiClient
kiota generate
# Identity Service
cd ../../../identity/src/Acsis.Dynaplex.Engines.Identity.ApiClient
kiota generate
# Workflow Service
cd ../../../workflow/src/Acsis.Dynaplex.Engines.Workflow.ApiClient
kiota generate
echo "Client generation complete!"
CI/CD Integration
# azure-pipelines.yml
- task: UseDotNetCore@2
inputs:
packageType: 'tool'
version: '1.x.x'
- script: dotnet tool install -g Microsoft.OpenApi.Kiota
displayName: 'Install Kiota'
- script: ./generate-clients.sh
displayName: 'Generate API Clients'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.ApiClient.csproj'
Request Customization
// Add custom headers or modify requests
public class CustomRequestAdapter : HttpClientRequestAdapter
{
public CustomRequestAdapter(IAuthenticationProvider authProvider, HttpClient httpClient)
: base(authProvider, httpClient)
{
}
public override async Task<T> SendAsync<T>(RequestInformation requestInfo, ...)
{
// Add correlation ID
requestInfo.Headers.Add("X-Correlation-Id", Activity.Current?.Id ?? Guid.NewGuid().ToString());
// Add custom telemetry
using var activity = Activity.StartActivity("ApiClient.Request");
activity?.SetTag("api.endpoint", requestInfo.Uri);
return await base.SendAsync<T>(requestInfo, ...);
}
}
Error Handling
public class ResilientCatalogClient
{
private readonly CatalogApiClient _client;
private readonly ILogger<ResilientCatalogClient> _logger;
public async Task<Asset?> GetAssetAsync(int id)
{
try
{
return await _client.Api.Catalog.Assets[id].GetAsync();
}
catch (ApiException ex) when (ex.StatusCode == 404)
{
_logger.LogWarning("Asset {AssetId} not found", id);
return null;
}
catch (ApiException ex) when (ex.StatusCode >= 500)
{
_logger.LogError(ex, "Server error getting asset {AssetId}", id);
throw new ServiceUnavailableException("Catalog service unavailable", ex);
}
}
}
Testing with Generated Clients
[Test]
public async Task GetAsset_ReturnsAsset()
{
// Arrange
var mockAdapter = Substitute.For<IRequestAdapter>();
mockAdapter.SendAsync<Asset>(Arg.Any<RequestInformation>(), ...)
.Returns(new Asset { Id = 1, Name = "Test Asset" });
var client = new CatalogApiClient(mockAdapter);
// Act
var asset = await client.Api.Catalog.Assets[1].GetAsync();
// Assert
asset.Should().NotBeNull();
asset.Name.Should().Be("Test Asset");
}
OpenAPI Annotations for Better Generation
group.MapGet("/assets/{id}", GetAssetById)
.WithName("GetAssetById") // Becomes method name in client
.WithDisplayName("Get Asset by ID")
.WithDescription("Retrieves an asset by its unique identifier")
.Produces<Asset>(200)
.ProducesProblem(404)
.WithOpenApi(operation =>
{
operation.OperationId = "GetAssetById"; // Explicit operation ID
operation.Parameters[0].Description = "Asset ID";
return operation;
});
Best Practices
- Always regenerate clients when APIs change
- Use operation IDs in OpenAPI for better method names
- Include descriptions for IntelliSense documentation
- Version clients alongside API versions
- Mock in tests rather than using real HTTP
- Handle errors gracefully with typed exceptions
- Add telemetry for distributed tracing
Related ADRs
- ADR-007: Migration to .NET Aspire Microservices (service communication)
- ADR-011: Flexible API Versioning Strategy (client versioning)
- ADR-017: OpenAPI Metadata Enrichment (source for generation)
- ADR-019: OpenTelemetry Integration (client telemetry)