Build-Time OpenAPI Generation Meets the Real World
After you get past the greenfield, “hello world” example of generating OpenAPI specifications at build time, things start to get more interesting. And by interesting, I mean builds start failing because dependencies can’t be resolved.
This is where the clean demo setups fall apart and real applications show up.
In a real service, you’re not just mapping a couple of routes and calling it a day. You’re wiring up telemetry, databases, messaging, authentication, background workers — sometimes all of them at once. And the moment you turn on build-time OpenAPI generation, you discover an uncomfortable truth: the OpenAPI generator actually has to run your app.
I know. That sounds like I’m contradicting myself from the first article. It’s only half a lie.
What “Running the App” Actually Means
When .NET generates the OpenAPI specification during build, it doesn’t run your application the way you normally run it. There’s no Kestrel listening on a port, no traffic flowing in, no long-lived process. Instead, it spins up your app in a special host process and runs it just long enough to discover controllers or mapped routes.
That makes sense. If you’re using controllers or the newer lightweight REST APIs in ASP.NET (which I now love), the framework has to execute your startup logic to know what endpoints exist.
Unfortunately, that means all of your startup logic runs.
When Dependencies Blow Up the Build
Let’s say you’re one of those weird people who wants to wire up OpenTelemetry with Azure Monitor. Your service registration might look something like this:
services.AddOpenTelemetry()
.ConfigureResource(rb =>
{
rb.Clear(); // wipe defaults so nothing overwrites your service.name
rb.AddService(
serviceName: serviceName,
serviceNamespace: serviceNamespace,
serviceVersion: serviceVersion,
serviceInstanceId: instanceId);
})
// Optional: add common auto-instrumentations
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation();
tracing.AddHttpClientInstrumentation();
})
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation();
metrics.AddHttpClientInstrumentation();
metrics.AddRuntimeInstrumentation();
metrics.AddProcessInstrumentation();
})
.WithLogging() // enables OTel logs pipeline
.UseAzureMonitor(ao =>
{
// Prefer explicit config; Azure SDK also honors APPLICATIONINSIGHTS_CONNECTION_STRING env var
ao.ConnectionString = opts.ConnectionString ?? configuration["AzureMonitor:ConnectionString"];
});
Or maybe you’re one of those other weirdos who actually wants to save data to a database, so you wire up Cosmos DB:
// Create credential
TokenCredential credential;
var managedIdentityClientId = configuration["AZURE_CLIENT_ID"];
if (!string.IsNullOrWhiteSpace(managedIdentityClientId))
{
credential = new ManagedIdentityCredential(
ManagedIdentityId.FromUserAssignedClientId(managedIdentityClientId));
}
else
{
credential = new DefaultAzureCredential();
}
// Client options
var clientOptions = new CosmosClientOptions
{
AllowBulkExecution = cosmosOptions.AllowBulkExecution,
UseSystemTextJsonSerializerWithOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
}
};
// Register CosmosClient singleton
if (!string.IsNullOrWhiteSpace(cosmosOptions.AccessKey))
{
var keyCredential = new AzureKeyCredential(cosmosOptions.AccessKey);
services.AddSingleton(_ => new CosmosClient(cosmosOptions.Endpoint!, keyCredential, clientOptions));
}
else
{
services.AddSingleton(_ => new CosmosClient(cosmosOptions.Endpoint!, credential, clientOptions));
}
// Also register options for DI
services.AddSingleton(cosmosOptions);
// Optionally register Database
services.AddSingleton(sp =>
{
var opts = sp.GetRequiredService<CosmosOptions>();
if (string.IsNullOrWhiteSpace(opts.Database))
return null!;
var client = sp.GetRequiredService<CosmosClient>();
return client.GetDatabase(opts.Database);
});
Weird? Maybe. Common in real services? Absolutely.
The problem is that all of this blows a gasket during build-time OpenAPI generation. Credentials aren’t available. Connection strings are missing. Managed identity can’t be resolved. Background services start up when they shouldn’t.
And suddenly your nice, clean “generate the spec on build” pipeline is dead in the water.
The Naive Solution (and Why It’s Wrong)
At first glance, it feels like you need to mock everything, sprinkle compile-time flags everywhere, or maintain a separate startup path just for OpenAPI generation.
That sounds awful — and fortunately, it mostly is.
The good news is you don’t need to rip apart your dependency injection configuration or duplicate your route mappings. The trick is simply knowing when you’re running in OpenAPI generation mode and selectively disabling what you don’t need.
Detecting an OpenAPI Build
The trick starts with a single line of code. This is the key line:
var isOpenApiBuild = Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";
When .NET generates OpenAPI documents at build time, it does so inside a small host executable with this assembly name. Once you know that, you can make smart decisions during startup.
Disabling Validation During OpenAPI Generation
One of the first things you need to do is disable DI validation. Otherwise, services that aren’t meant to be resolved during spec generation will fail the build.
var builder = WebApplication.CreateSlimBuilder(args);
var isOpenApiBuild = Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider";
builder.Host.UseDefaultServiceProvider((ctx, opts) =>
{
opts.ValidateScopes = !isOpenApiBuild;
opts.ValidateOnBuild = !isOpenApiBuild;
});
This alone removes a lot of friction.
Turning Off Unnecessary Dependencies
Now comes the surprisingly simple part. Anything that isn’t required to define your routes can be skipped during OpenAPI generation.
For example:
// Yes, this is outside
builder.Services.AddOpenApi();
if (!isOpenApiBuild)
{
// Auth0 Authentication
builder.Services.AddAuth0Authentication(builder.Configuration);
// Azure Storage
builder.Services.AddAzureBlobStorage(builder.Configuration);
builder.Services.AddAzureQueueStorage(builder.Configuration);
// EventGrid
builder.Services.AddEventGridPublisher(builder.Configuration);
// Azure Cosmos DB
builder.Services.AddCosmosDb(builder.Configuration);
// Azure Open AI
builder.Services.AddAzureOpenAI(builder.Configuration);
}
In my case, that list includes Auth0, Azure Monitor, Azure Storage, Cosmos DB, Azure OpenAI, Event Grid, and SignalR. That’s a lot of stuff — but none of it is required to describe the API surface.
The beauty here is that all of your routing and endpoint definitions stay exactly the same.
Don’t Start Background Workers Either
If you have background workers, make sure they stay off as well:
if (!isOpenApiBuild && builder.Environment.IsProduction())
{
builder.Services.AddHostedService<QueueProcessorWorker>();
}
There’s no reason for workers to spin up just to emit a JSON document.
Disabling Authentication Middleware
Finally, authentication and authorization middleware should also be skipped. This is separate from configuring Auth0 itself — it happens later in the pipeline.
if (!isOpenApiBuild)
{
app.UseAuthentication();
app.UseAuthorization();
}
Leave the OpenAPI Setup Alone
At the end, your OpenAPI setup remains unchanged:
var openApi = app.MapOpenApi();
openApi.AllowAnonymous();
And that’s it.
Conclusion
Once you move past toy examples, build-time OpenAPI generation forces you to confront how much work your application does during startup. The trick isn’t mocking everything or maintaining a parallel bootstrapping path — it’s recognizing when the app is running in OpenAPI generation mode and turning off anything that isn’t strictly necessary.
With a single detection flag and a few well-placed conditionals, you can keep your dependency injection, route mappings, and overall architecture intact — while still enjoying fully automated OpenAPI generation as part of your build pipeline.
And once you have that, automated client generation across platforms becomes a lot more realistic.