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 .DbMigrator projects have been replaced by a centralized db-manager component. 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

  1. Always regenerate clients when APIs change
  2. Use operation IDs in OpenAPI for better method names
  3. Include descriptions for IntelliSense documentation
  4. Version clients alongside API versions
  5. Mock in tests rather than using real HTTP
  6. Handle errors gracefully with typed exceptions
  7. Add telemetry for distributed tracing
  • 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)