Why OpenAPI and Build-Time Generation Matter
Good API documentation reduces friction — for external consumers, for your team, and even for a Backend-for-Frontend (BFF) that is used only internally.
OpenAPI is the industry standard for describing REST APIs. By providing machine-readable descriptions of your endpoints, OpenAPI enables a rich ecosystem of tools such as:
- Scalar UI, which turns the raw JSON of an OpenAPI document into beautiful, interactive documentation of your API where you can also test the API like you would with Postman or Hoppscotch.
- Spectral, which lints an OpenAPI document for issues and inconsistencies.
- Microsoft Kiota, which generates strongly typed client SDKs so you don't have to write manual HTTP wrapper code.
ASP.NET Core has excellent built-in support for auto-generating OpenAPI documents from your code — an approach known as Code-First (as opposed to Design-First, where you author the document by hand upfront).
In this post I will show you how to set all of this up: OpenAPI document generation, Scalar UI for interactive browsing and testing, and — crucially — build-time generation so the document is produced during every build and can be checked into source control alongside your code.
The last of these — build-time generation — matters more than it might seem, and I'll explain exactly why when we get to it.
Here's what Scalar UI looks like once it's set up:

Sample Code
You can see OpenAPI document generation in action in a .NET 10 minimal API in the sample code repo on GitHub.
To run the project, press F5 in VS Code or GitHub Codespace, or run dotnet run on the command line.
This would launch the Scalar UI shown above in which you would be able to browse the generated OpenAPI document and test the API.
Set up OpenAPI document generation and Scalar UI
In order to set up OpenAPI document generation and Scalar UI in your .NET minimal API project, proceed as follows:
Add a reference to package
Microsoft.AspNetCore.OpenApi.
This package provides classes that enable automatic OpenAPI document generation.-
Add statements
builder.Services.AddOpenApi();andapp.MapOpenApi();toProgram.cs, as described here in MS Docs.
This enables the OpenAPI document for the API to be auto-generated and served at/openapi/v1.json.
var builder = WebApplication.CreateBuilder(args); // adds and registers the OpenAPI document generator and related services in the DI container builder.Services.AddOpenApi(); var app = builder.Build(); // exposes endpoint `/openapi/v1.json` over which the autogenerated OpenAPI document for the API is served app.MapOpenApi(); app.Run(); -
Modify Handlers: Several extension methods provided by the package
Microsoft.AspNetCore.OpenApican be chained to route prefix and handler registrations to provide OpenAPI metadata about them.These augment the metadata that OpenAPI services automatically deduce from handler signatures (parameter lists, return types etc.).
I gather together endpoint handlers for all operations at a given route segment, such as
/product, in a static Handlers class (ProductHandlersin the snippet below; see sample code for the full implementation).In this class I also expose a static
MapRoutesAndDescribemethod that both registers those handlers and chains OpenAPI extension methods like.WithTags(),.WithSummary()and others to declare OpenAPI metadata on both the individual handlers and on the route prefix (/productsbelow):
internal static RouteGroupBuilder MapRoutesAndDescribe(RouteGroupBuilder baseRouteGroup) { // OpenAPI Tag "Product Operations" is added to the group of handlers in this Handlers class under Route prefix `/products` var routeBuilder = baseRouteGroup.MapGroup(RoutePrefix).WithTags("Product Operations"); routeBuilder.MapPost("/", HandleCreateProduct).WithName(HandlerNames.CreateProduct).WithSummary("Creates a new product in the system."); routeBuilder.MapGet("/{id}", HandleGetProduct).WithName(HandlerNames.GetProduct).WithSummary("Fetches the product details for a given product id."); return routeBuilder; }Whether you use Handlers classes like the one given above to register groups of handlers in your API, or register them in some other way, chain
.WithTags(),.WithSummary()and other extension methods given here to add metadata to individual handler registrations.If you create a
RouteGroupBuilderfor the route segment for a group of handlers (as the Handlers class above does), then chain aWithTags()to this also, as I have done above.When viewed in Scalar UI (which we'll set up shortly), the result should look something like the screenshot below. In this, I have highlighted the bits that correspond to the metadata declared in snippet above in red outline:
-
Add XML documentation comments on handlers and on the types of parameters that are bound to HTTP request. For example, on the handler:
/// <summary> /// Creates a new product in the database. /// </summary> /// <param name="p">Details of the product to be created.</param> /// <response code="201">Product created successfully. URL to GET the newly created product is in the <c>Location</c> response header.</response> internal static async Task<Created> HandleCreateProduct(CreateProductArgs p, IProductService productService, LinkGenerator linkGen) {And since
CreateProductArgs p, being the only complex-type parameter in the handler's parameter list, is automatically bound to the JSON body of the HTTP request, we need to provide XML documentation comments for it too:
/// <summary> /// Details of the product to be created /// </summary> public record CreateProductArgs { /// <summary> /// Name of the product /// </summary> public required string Name { get; set; } /// <summary> /// A description for the product /// </summary> public required string Description { get; set; } /// <summary> /// URL of an image of the product /// </summary> public string? ImageUrl { get; set; } /// <summary> /// Price of the product. Must be greater than or equal to zero. /// </summary> public required decimal Price { get; set; } }The XML documentation comments given above generate OpenAPI documentation for the endpoint handler that looks like this:
At this step, please ensure the following:
-
Only provide
<param>tags for those parameters that are bound to request parameters.
For exampleCreateProductArgs pabove, being the only complex type parameter in the handler's parameter list, is automatically bound to JSON request body in a minimal API. So I have included a<param>tag for it.
On the other hand,IProductService productServiceandLinkGenerator linkGenare not bound to contents of the HTTP request (both of these happen to be sourced from DI container). Therefore I have made sure that there is no<param>tag for either of them.
Why do this? because if we include a<param>tag in XML documentation comments for a parameter that is not model-bound (i.e. not bound to any part of the HTTP request) then its documentation shows up spuriously as part of the request for the endpoint in generated OpenAPI spec. Screenshot below shows this happening if I include a<param>tag forLinkGenerator linkGenparameter in the above handler which is resolved from DI container:
-
* **Handler methods should NOT be `private`**. XML documentation comments of `private` handlers are not visible to OpenAPI document generator. This is why I made them `internal` in the code shown above.
-
Configure
.csprojto collect the XML documentation comments in your project and compile them into filebin/<build configuration e.g. Debug>/net10.0/<project name>.xml.
When this file is present, it will be automatically used in generating OpenAPI document.
To configure generation of the XML documentation comments file at build time, place<GenerateDocumentationFile>true</GenerateDocumentationFile>in your API project's.csprojfile within a<PropertyGroup>element, e.g. :
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <!--Turn on generation of .xml into build output folder that would contain all the XML documentation comments in the project--> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup>When you build your API project (e.g. with
dotnet build), because of including the above attribute in.csproj, you would get warnings:These warnings would be of one or both of the following two types:
- CS1591 warns you that XML documentation comments are missing altogether on a type or member that is publicly visible.
-
CS1573 is reported when an XML documentation comment - a
<param>tag - is specified for some but not all parameters on a method. This warning would definitely be reported if you follow the convention described above of always having a<param>XML documentation tag for every parameter in a handler method that is model-bound (i.e. binds to some part of the incoming HTTP request) and always omitting it for every other parameter in the handler.
I personally like to turn both of these off, because:
- CS1573 arises by design. I might as well get rid of the noise by silencing these
- In API project, I do not always have XML documentation comments on every public type or member. I only provide these where they would go into the generated OpenAPI document. Therefore like to turn off CS1591 also.
If you would like to turn these warnings off, add a
<NoWarn>XML element within the same<PropertyGroup>in which you added the<GenerateDocumentationFile>true</GenerateDocumentationFile>MS Build property above:
<PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <!--Turn on generation of .xml into build output folder that would contain all the XML documentation comments in the project--> <GenerateDocumentationFile>true</GenerateDocumentationFile> <!--Switch off compiler warnings: CS1573 that arises due to missing XML documentation comments in handler parameters (I do this deliberately otherwise OpenAPI document spuriously contains documentation comments from non-model-bound handler parameters) and CS1591 because I do not always place XML documentation comments on all publicly visible types and members (except on handlers and on model-bound types in handler parameters) --> <NoWarn>$(NoWarn);1591;1573</NoWarn> </PropertyGroup> -
Add Scalar UI: Add a reference to package
Scalar.AspNetCoreand add lineapp.MapScalarApiReference();toProgram.cs(as described here).
You can place it right after theapp.MapOpenApi();call you add toProgram.csabove.Adding this line is what would serve the Scalar UI to browse the raw JSON of the OpenAPI document in an interactive web app (the Scalar UI) where you can also test your API.
If you have completed the steps above, you can now run the API and navigate to http://localhost:xxxx/scalar to browse the API's OpenAPI documentation and test it (as shown in the screenshot in intro section above).
You can also see the raw JSON OpenAPI document at http://localhost:xxxx/openapi/v1.json.
Note: Other ways of adding OpenAPI metadata to your project
In addition to these ways of adding OpenAPI metadata that you exercised above:
- extension methods to provide metadata on handler and
RouteGroupBuilderregistrations - Starting in .NET 10, XML documentation comments on the handlers and on the types used on those handlers can also be picked up by OpenAPI document generator. This is more powerful now than in the previous incarnation of this facility that existed before .NET 7.
you can use the following to tweak OpenAPI metadata in your API project:
-
C# attributes provided by the same package (
Microsoft.AspNetCore.OpenApi) - If other techniques - extension methods, C# attributes and XML documentation comments - fall short, OpenAPI document generation can be customized using custom transformers provided to
builder.Services.AddOpenApi()call inProgram.cs.
Set up Build-time Generation of OpenAPI JSON document
The Problem with Runtime-Only Generation
The reason the OpenAPI document is only generated at runtime is that the services need to examine OpenAPI metadata explicitly configured in code in addition to other reflection-based sources. However, relying solely on runtime generation creates a gap in the development lifecycle. The document is missing at build time, where it is needed for:
- Linting: Automatically checking the document for issues such as errors and OWASP compliance using a tool like Spectral.
- Review of Changes to the Contract: Inclusion in your Git commit so that any changes to it are visible during the Pull Request (PR) review of the commit.
The second point is vital. Because the document is synthesized from numerous sources of metadata — XML documentation comments, C# attributes, extension method calls and custom configuration code — that are dispersed all over the API code, a small change to one file can quietly alter the OpenAPI document.
But this document is your API contract: any change to it should be reviewed before the corresponding code reaches production.
A natural place to do this review is as part of the PR review for the commit that led to those changes. However, this is only possible if the OpenAPI document (e.g. openapi.json) is generated during a pre-commit build (e.g. to run tests), then checked into source control along with the code changes.
In the section below, I show you how to set up build-time generation for your API's OpenAPI document, so that it is generated whenever the API project is built and subsequently checked into source control along with the code changes.
Set up Build-time Generation
Set up build time generation of OpenAPI document so it can be checked in to source control:
-
In the project folder on the terminal, run:
dotnet add package Microsoft.Extensions.ApiDescription.ServerThis package contributes build assets to the
.csprojthat execute during build of the project. They run the API during the build process, call the/openapi/v1.jsonendpoint to fetch the dynamically generated OpenAPI document and save it asbin/<configuration such as Debug>/netX.0/<Project Name>.json.Clearly the path doesn't work if we are going to check it into source control (
binfolder is rightly excluded from source control in .NET.gitignorefiles). We will fix this in the next step. -
Modify the output path and filename: To put the generated OpenAPI document into project root, place the following in your
.csprojfile.
You have to pass--file-namein the XML tag<OpenApiGenerateDocumentsOptions></OpenApiGenerateDocumentsOptions>in order to change the path. Therefore it is an opportunity to change the name of the document from<ProjectName>.jsonto something else. I have changed it toopenapi.jsonbelow which I prefer to the default as it is more stable (does not depend on project name).
<PropertyGroup> <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory> <OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions> </PropertyGroup> -
Modify
Program.cs: The API is executed during build when configuration information such as database connection strings and API keys would likely not be available (especially in CI/CD pipeline where build is cleanly separated from the execution/deployment jobs in which configuration information is sourced securely).Therefore we want to modify
Program.csso that if it is executed during build - by the OpenAPI document generation step in the build - then services and middlewares that require configuration information are not added.Note: the sample code repo does not include this modification to
Program.cs. However, I regularly use the following technique - from here in MS Docs - to exclude.AddXXX()statements to add those services and.UseXXXstatements to add those middlewares that require configuration information to be available.To understand the condition in the conditional execution (
if) blocks, note that the .NET assembly for command line tool that launches the API during build to fetch the OpenAPI document has assembly nameGetDocument.Insider. Therefore we check for the fact that the API project has been launched from this assembly to deduce that this run is happening at build time.
// Program.cs file // We check the entry assembly name to detect if the app is being executed by the OpenAPI build tool (whose assembly is named "GetDocument.Insider") // See MS Docs: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-10.0&tabs=net-cli%2Cnetcore-cli#customize-runtime-behavior-during-build-time-document-generation var isBuildTime = Assembly.GetEntryAssembly()?.GetName().Name == "GetDocument.Insider"; if (!isBuildTime) { // exclude OpenTelemetry initialization as connection information for telemetry backend is not going to be available at build time when the app is run to generate Open API document builder.AddOpenTelemetry(); } // other statements for adding services to DI container... if (!isBuildTime) { //exclude adding app's DbContext to DI container as database connection string is not going to be available at build time when the app is run to generate Open API document connString = builder.Configuration.GetConnectionString("CloudCartDB") ?? throw new InvalidOperationException("Connection string 'CloudCartDB' is not configured."); builder.Services.AddDbContext<CloudCartDbContext>( options => { options.UseNpgsql(connString); if (builder.Environment.IsDevelopment()) { options.EnableSensitiveDataLogging(); } } ); } var app = builder.Build(); // add middlewares and endpoints... app.Run();
If you have completed the above steps, then whenever your project is built, the OpenAPI document - openapi.json in the API project folder - would be regenerated.
Other things you can do with your OpenAPI document
Now that you have set up OpenAPI document generation in your API, here are some pointers to other things you can do with the document:
-
Lint it with Spectral: Spectral is an amazing tool for linting OpenAPI documents for a host of issues such as correctness, compliance with your organizational best practices and any OWASP issues in API design that may be detectable from its OpenAPI document.
I have not set up Spectral in the sample code as it is a Node.js tool and its setup would rather detract from the .NET focus of this article.
However, if you are interested in setting it up, here are some links:- See this post for setting up Spectral.
The instructions should work fine run from the solution root - some NPM and Node.js files will get created in that folder beside the
sln/slnxfile but it should not interfere with the .NET solution. I haven't tested this specific configuration myself as my own spectral setups have been in polyglot solutions containing .NET APIs where I have installed Spectral one level above the .NET solution folder. - Build project and run Spectral on it at every Git commit - so that the commit is aborted if linting fails - using Husky.NET.
- Check out Spectral's own documentation.
- See this post for setting up Spectral.
The instructions should work fine run from the solution root - some NPM and Node.js files will get created in that folder beside the
Return Problem Details responses and include them in your OpenAPI document: IETF Problem Details RFC 9457 is a superb, really lightweight standard for returning problems and errors from your APIs. It also has pretty good support in .NET Core.
I have an upcoming dev.to post on how to return Problem Details responses from a .NET Core minimal API and include these in the OpenAPI document. If you follow me, you would get notified.
Conclusion
OpenAPI document generation in a .NET minimal API is a matter of assembling the right pieces — the Microsoft.AspNetCore.OpenApi package, a handful of extension methods and XML documentation comments, Scalar UI for interactive browsing and testing, and build-time document generation.
The last of these items is particularly important: once you set up your OpenAPI document to be produced at build time, it can be committed alongside your code, contract changes become visible in PR diffs and linting can run automatically — your API's public contract is transparent throughout the DevOps pipeline and no longer something that only materialises at runtime.




Top comments (0)