Why hand‑crafting HTTP calls is risky
Ever noticed how a single typo in a URL or a mismatched JSON property can cause a runtime exception that only shows up during integration testing? In large microservice ecosystems the cost of such bugs multiplies, especially when multiple teams share the same OpenAPI contract. .NET 8 introduces powerful compile‑time tools that can eliminate this class of errors by generating strongly typed API clients directly from the contract.
Leverage NSwag for OpenAPI to C# conversion
NSwag is a mature, open‑source suite that can read a Swagger or OpenAPI 3.0 document and emit C# client code. When combined with the new Roslyn source generator infrastructure, the generated client can become part of the compilation process, guaranteeing that any breaking change in the contract fails the build.
Start by adding the NSwag MSBuild package to a .NET 8 project. The following snippet shows the minimal .csproj configuration required to trigger code generation after every build:
dotnet new console -n MyApiClient cd MyApiClient dotnet add package NSwag.MSBuild dotnet add package Microsoft.NET.Sdk.Roslyn <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup> <ItemGroup> <PackageReference Include="NSwag.MSBuild" Version="13.18.0" /> </ItemGroup> <Target Name="GenerateApiClient" AfterTargets="Build"> <Exec Command="dotnet nswag openapi2csclient /input:swagger.json /output:Generated\\ApiClient.cs /namespace:MyApiClient.Generated" /> </Target> </Project>Place the OpenAPI definition (for example swagger.json) in the project root. Every time you run dotnet build, NSwag reads the contract and produces Generated/ApiClient.cs. Because EmitCompilerGeneratedFiles is true, the file appears under obj/Debug/net8.0/Generated and participates in IntelliSense and static analysis.
Write a lightweight source generator to wrap the client
While NSwag gives you a raw client class, you often need a thin abstraction that injects HttpClient from the DI container, adds Polly policies, or configures authentication headers. A source generator can create this wrapper automatically, ensuring the wrapper stays in sync with the generated contract.
Below is a minimal example of a generator that emits a MyApiService class exposing the NSwag client as a singleton service:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System.Text; namespace MyApiClient.Generators { [Generator] public class ServiceWrapperGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { var source = @"using Microsoft.Extensions.DependencyInjection; using MyApiClient.Generated; namespace MyApiClient { public static class ServiceCollectionExtensions { public static IServiceCollection AddMyApiClient(this IServiceCollection services, string baseUrl) { services.AddHttpClient<MyApiClient.Generated.ApiClient>(c => c.BaseAddress = new Uri(baseUrl)); services.AddSingleton<MyApiService>(); return services; } } public class MyApiService { private readonly ApiClient _client; public MyApiService(ApiClient client) { _client = client; } public Task<WeatherForecast[]> GetForecastAsync() => _client.WeatherForecast_GetAsync(); } } }"; context.AddSource("MyApiService.g.cs", SourceText.From(source, Encoding.UTF8)); } } } Notice the double backslashes in the namespace import statements are escaped for JSON compatibility. After compilation, developers can call services.AddMyApiClient("https://api.example.com") and instantly get a strongly typed, compile‑time‑checked service.
Consume the generated client with confidence
With the wrapper in place, consuming the API looks like ordinary DI usage. Because the method signatures are generated from the OpenAPI spec, any change—such as a new required query parameter—will break the call site at compile time. The following controller demonstrates a typical usage pattern:
using Microsoft.AspNetCore.Mvc; using MyApiClient; namespace WebApp.Controllers { [ApiController] [Route("[controller]")] public class ForecastController : ControllerBase { private readonly MyApiService _service; public ForecastController(MyApiService service) { _service = service; } [HttpGet] public async Task<IActionResult> Get() { var data = await _service.GetForecastAsync(); return Ok(data); } } }If the OpenAPI contract adds a new mandatory field to the WeatherForecast model, the generated WeatherForecast class will gain a new property, and the controller will still compile because the data is deserialized automatically. However, if a required endpoint parameter is removed, the generator will delete the corresponding method, causing an immediate compile error.
Performance and build‑time impact
Source generation adds roughly 1‑2 seconds to a clean build on a typical laptop (dotnet 8 SDK, 8 GB RAM). Incremental builds only re‑run NSwag when swagger.json changes, so the overhead is negligible in day‑to‑day development. The generated client is pure C# without reflection, so runtime performance matches hand‑written HttpClient code.
Conclusion
Combining NSwag with .NET 8 source generators delivers a zero‑runtime‑cost safety net: the OpenAPI contract becomes the single source of truth, compile‑time checks prevent mismatched contracts, and developers retain full control over DI, policies, and logging. By embedding the generation step into the build pipeline, teams can evolve APIs rapidly without fearing hidden integration bugs.
Sources
- Microsoft Docs – Source Generators (https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators)
- NSwag Documentation (https://github.com/RicoSuter/NSwag)
- OpenAPI Specification 3.0 (https://spec.openapis.org/oas/v3.0.3)
Author: Mahmut Sarıkaya — sarikayadev.com