Generate OpenAPI documents
The Microsoft.AspNetCore.OpenApi
package provides built-in support for OpenAPI document generation in ASP.NET Core. The package provides the following features:
- Support for generating OpenAPI documents at run time and accessing them via an endpoint on the application.
- Support for "transformer" APIs that allow modifying the generated document.
- Support for generating multiple OpenAPI documents from a single app.
- Takes advantage of JSON schema support provided by
System.Text.Json
. - Is compatible with native AoT.
Package installation
Install the Microsoft.AspNetCore.OpenApi
package:
Run the following command from the Package Manager Console:
Install-Package Microsoft.AspNetCore.OpenApi
Configure OpenAPI document generation
The following code:
- Adds OpenAPI services.
- Enables the endpoint for viewing the OpenAPI document in JSON format.
var builder = WebApplication.CreateBuilder();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapGet("/", () => "Hello world!");
app.Run();
Launch the app and navigate to https://localhost:<port>/openapi/v1.json
to view the generated OpenAPI document.
Options to Customize OpenAPI document generation
The following sections demonstrate how to customize OpenAPI document generation.
Customize the OpenAPI document name
Each OpenAPI document in an app has a unique name. The default document name that is registered is v1
.
builder.Services.AddOpenApi(); // Document name is v1
The document name can be modified by passing the name as a parameter to the AddOpenApi
call.
builder.Services.AddOpenApi("internal"); // Document name is internal
The document name surfaces in several places in the OpenAPI implementation.
When fetching the generated OpenAPI document, the document name is provided as the documentName
parameter argument in the request. The following requests resolve the v1
and internal
documents.
GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json
Customize the OpenAPI version of a generated document
By default, OpenAPI document generation creates a document that is compliant with v3.0 of the OpenAPI specification. The following code demonstrates how to modify the default version of the OpenAPI document:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;
});
Customize the OpenAPI endpoint route
By default, the OpenAPI endpoint registered via a call to MapOpenApi exposes the document at the /openapi/{documentName}.json
endpoint. The following code demonstrates how to customize the route at which the OpenAPI document is registered:
app.MapOpenApi("/openapi/{documentName}/openapi.json");
It's possible, but not recommended, to remove the documentName
route parameter from the endpoint route. When the documentName
route parameter is removed from the endpoint route, the framework attempts to resolve the document name from the query parameter. Not providing the documentName
in either the route or query can result in unexpected behavior.
Customize the OpenAPI endpoint
Because the OpenAPI document is served via a route handler endpoint, any customization that is available to standard minimal endpoints is available to the OpenAPI endpoint.
Limit OpenAPI document access to authorized users
The OpenAPI endpoint doesn't enable any authorization checks by default. However, authorization checks can be applied to the OpenAPI document. In the following code, access to the OpenAPI document is limited to those with the tester
role:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("ApiTesterPolicy", b => b.RequireRole("tester"));
});
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi()
.RequireAuthorization("ApiTesterPolicy");
app.MapGet("/", () => "Hello world!");
app.Run();
Cache generated OpenAPI document
The OpenAPI document is regenerated every time a request to the OpenAPI endpoint is sent. Regeneration enables transformers to incorporate dynamic application state into their operation. For example, regenerating a request with details of the HTTP context. When applicable, the OpenAPI document can be cached to avoid executing the document generation pipeline on each HTTP request.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder();
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromMinutes(10)));
});
builder.Services.AddOpenApi();
var app = builder.Build();
app.UseOutputCache();
app.MapOpenApi()
.CacheOutput();
app.MapGet("/", () => "Hello world!");
app.Run();
Generate OpenAPI documents at build-time
In typical web applications, OpenAPI documents are generated at run-time and served via an HTTP request to the application server.
In some scenarios, it's helpful to generate the OpenAPI document during the application's build step. These scenarios include:
- Generating OpenAPI documentation that is committed into source control.
- Generating OpenAPI documentation that is used for spec-based integration testing.
- Generating OpenAPI documentation that is served statically from the web server.
To add support for generating OpenAPI documents at build time, install the Microsoft.Extensions.ApiDescription.Server
package:
Run the following command from the Package Manager Console:
Install-Package Microsoft.Extensions.ApiDescription.Server
Upon installation, this package will automatically generate the Open API document(s) associated with the application during build and populate them into the application's output directory.
$ dotnet build
$ cat bin/Debug/net9.0/{ProjectName}.json
Customizing build-time document generation
Modifying the output directory of the generated Open API file
By default, the generated OpenAPI document will be emitted to the application's output directory. To modify the location of the emitted file, set the target path in the OpenApiDocumentsDirectory
property.
<PropertyGroup>
<OpenApiDocumentsDirectory>./</OpenApiDocumentsDirectory>
</PropertyGroup>
The value of OpenApiDocumentsDirectory
is resolved relative to the project file. Using the ./
value above will emit the OpenAPI document in the same directory as the project file.
Modifying the output file name
By default, the generated OpenAPI document will have the same name as the application's project file. To modify the name of the emitted file, set the --file-name
argument in the OpenApiGenerateDocumentsOptions
property.
<PropertyGroup>
<OpenApiGenerateDocumentsOptions>--file-name my-open-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
Selecting the OpenAPI document to generate
Some applications may be configured to emit multiple OpenAPI documents, for various versions of an API or to distinguish between public and internal APIs. By default, the build-time document generator will emit files for all documents that are configured in an application. To only emit for a single document name, set the --document-name
argument in the OpenApiGenerateDocumentsOptions
property.
<PropertyGroup>
<OpenApiGenerateDocumentsOptions>--document-name v2</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
Customizing run-time behavior during build-time document generation
Under the hood, build-time OpenAPI document generation functions by launching the application's entrypoint with an inert server implementation. This is a requirement to produce accurate OpenAPI documents since all information in the OpenAPI document cannot be statically analyzed. Because the application's entrypoint is invoked, any logic in the applications' startup will be invoked. This includes code that injects services into the DI container or reads from configuration. In some scenarios, it's necessary to restrict the codepaths that will run when the application's entry point is being invoked from build-time document generation. These scenarios include:
- Not reading from certain configuration strings.
- Not registering database-related services.
In order to restrict these codepaths from being invoked by the build-time generation pipeline, they can be conditioned behind a check of the entry assembly like so:
using System.Reflection;
var builder = WebApplication.CreateBuilder();
if (Assembly.GetEntryAssembly()?.GetName().Name != "GetDocument.Insider")
{
builder.Services.AddDefaults();
}
Minimal APIs provide built-in support for generating information about endpoints in an app via the Microsoft.AspNetCore.OpenApi
package. Exposing the generated OpenAPI definition via a visual UI requires a third-party package. For information about support for OpenAPI in controller-based APIs, see the .NET 9 version of this article.
The following code is generated by the ASP.NET Core minimal web API template and uses OpenAPI:
using Microsoft.AspNetCore.OpenApi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
In the preceding highlighted code:
Microsoft.AspNetCore.OpenApi
is explained in the next section.- AddEndpointsApiExplorer : Configures the app to use the API Explorer to discover and describe endpoints with default annotations.
WithOpenApi
overrides matching, default annotations generated by the API Explorer with those produced from theMicrosoft.AspNetCore.OpenApi
package. UseSwagger
adds the Swagger middleware.- `UseSwaggerUI` enables an embedded version of the Swagger UI tool.
- WithName: The IEndpointNameMetadata on the endpoint is used for link generation and is treated as the operation ID in the given endpoint's OpenAPI specification.
WithOpenApi
is explained later in this article.
Microsoft.AspNetCore.OpenApi
NuGet package
ASP.NET Core provides the Microsoft.AspNetCore.OpenApi
package to interact with OpenAPI specifications for endpoints. The package acts as a link between the OpenAPI models that are defined in the Microsoft.AspNetCore.OpenApi
package and the endpoints that are defined in Minimal APIs. The package provides an API that examines an endpoint's parameters, responses, and metadata to construct an OpenAPI annotation type that is used to describe an endpoint.
Microsoft.AspNetCore.OpenApi
is added as a PackageReference to a project file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>
When using Swashbuckle.AspNetCore
with Microsoft.AspNetCore.OpenApi
, Swashbuckle.AspNetCore
6.4.0 or later must be used. Microsoft.OpenApi
1.4.3 or later must be used to leverage copy constructors in WithOpenApi
invocations.
Add OpenAPI annotations to endpoints via WithOpenApi
Calling WithOpenApi
on the endpoint adds to the endpoint's metadata. This metadata can be:
- Consumed in third-party packages like Swashbuckle.AspNetCore.
- Displayed in the Swagger user interface or in YAML or JSON generated to define the API.
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
Modify the OpenAPI annotation in WithOpenApi
The WithOpenApi
method accepts a function that can be used to modify the OpenAPI annotation. For example, in the following code, a description is added to the first parameter of the endpoint:
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
var parameter = generatedOperation.Parameters[0];
parameter.Description = "The ID associated with the created Todo";
return generatedOperation;
});
Add operation IDs to OpenAPI
Operation IDs are used to uniquely identify a given endpoint in OpenAPI. The WithName
extension method can be used to set the operation ID used for a method.
app.MapGet("/todoitems2", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithName("GetToDoItems");
Alternatively, the OperationId
property can be set directly on the OpenAPI annotation.
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.WithOpenApi(operation => new(operation)
{
OperationId = "GetTodos"
});
Add tags to the OpenAPI description
OpenAPI supports using tag objects to categorize operations. These tags are typically used to group operations in the Swagger UI. These tags can be added to an operation by invoking the WithTags extension method on the endpoint with the desired tags.
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithTags("TodoGroup");
Alternatively, the list of OpenApiTags
can be set on the OpenAPI annotation via the WithOpenApi
extension method.
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.WithOpenApi(operation => new(operation)
{
Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
});
Add endpoint summary or description
The endpoint summary and description can be added by invoking the WithOpenApi
extension method. In the following code, the summaries are set directly on the OpenAPI annotation.
app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())
.WithOpenApi(operation => new(operation)
{
Summary = "This is a summary",
Description = "This is a description"
});
Exclude OpenAPI description
In the following sample, the /skipme
endpoint is excluded from generating an OpenAPI description:
using Microsoft.AspNetCore.OpenApi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/swag", () => "Hello Swagger!")
.WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
Mark an API as obsolete
To mark an endpoint as obsolete, set the Deprecated
property on the OpenAPI annotation.
app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.WithOpenApi(operation => new(operation)
{
Deprecated = true
});
Describe response types
OpenAPI supports providing a description of the responses returned from an API. Minimal APIs support three strategies for setting the response type of an endpoint:
- Via the
Produces
extension method on the endpoint - Via the
ProducesResponseType
attribute on the route handler - By returning
TypedResults
from the route handler
The Produces
extension method can be used to add Produces
metadata to an endpoint. When no parameters are provided, the extension method populates metadata for the targeted type under a 200
status code and an application/json
content type.
app
.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
.Produces<IList<Todo>>();
Using TypedResults
in the implementation of an endpoint's route handler automatically includes the response type metadata for the endpoint. For example, the following code automatically annotates the endpoint with a response under the 200
status code with an application/json
content type.
app.MapGet("/todos", async (TodoDb db) =>
{
var todos = await db.Todos.ToListAsync());
return TypedResults.Ok(todos);
});
Set responses for ProblemDetails
When setting the response type for endpoints that may return a ProblemDetails response, the ProducesProblem extension method, ProducesValidationProblem, or TypedResults.Problem
can be used to add the appropriate annotation to the endpoint's metadata. Note that the ProducesProblem
and ProducesValidationProblem
extension methods can't be used with route groups in .NET 8 and earlier.
When there are no explicit annotations provided by one of the strategies above, the framework attempts to determine a default response type by examining the signature of the response. This default response is populated under the 200
status code in the OpenAPI definition.
Multiple response types
If an endpoint can return different response types in different scenarios, you can provide metadata in the following ways:
Call the
Produces
extension method multiple times, as shown in the following example:app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) => await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound()) .Produces<Todo>(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound);
Use
Results<TResult1,TResult2,TResultN>
in the signature andTypedResults
in the body of the handler, as shown in the following example:app.MapGet("/book/{id}", Results<Ok<Book>, NotFound> (int id, List<Book> bookList) => { return bookList.FirstOrDefault((i) => i.Id == id) is Book book ? TypedResults.Ok(book) : TypedResults.NotFound(); });
The
Results<TResult1,TResult2,TResultN>
union types declare that a route handler returns multipleIResult
-implementing concrete types, and any of those types that implementIEndpointMetadataProvider
will contribute to the endpoint’s metadata.The union types implement implicit cast operators. These operators enable the compiler to automatically convert the types specified in the generic arguments to an instance of the union type. This capability has the added benefit of providing compile-time checking that a route handler only returns the results that it declares it does. Attempting to return a type that isn't declared as one of the generic arguments to
Results<TResult1,TResult2,TResultN>
results in a compilation error.
Describe request body and parameters
In addition to describing the types that are returned by an endpoint, OpenAPI also supports annotating the inputs that are consumed by an API. These inputs fall into two categories:
- Parameters that appear in the path, query string, headers, or cookies
- Data transmitted as part of the request body
The framework infers the types for request parameters in the path, query, and header string automatically based on the signature of the route handler.
To define the type of inputs transmitted as the request body, configure the properties by using the Accepts
extension method to define the object type and content type that are expected by the request handler. In the following example, the endpoint accepts a Todo
object in the request body with an expected content-type of application/xml
.
app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
.Accepts<Todo>("application/xml");
In addition to the Accepts
extension method, A parameter type can describe its own annotation by implementing the IEndpointParameterMetadataProvider
interface. For example, the following Todo
type adds an annotation that requires a request body with an application/xml
content-type.
public class Todo : IEndpointParameterMetadataProvider
{
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
{
builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional: false, "application/xml"));
}
}
When no explicit annotation is provided, the framework attempts to determine the default request type if there's a request body parameter in the endpoint handler. The inference uses the following heuristics to produce the annotation:
- Request body parameters that are read from a form via the
[FromForm]
attribute are described with themultipart/form-data
content-type. - All other request body parameters are described with the
application/json
content-type. - The request body is treated as optional if it's nullable or if the
AllowEmpty
property is set on theFromBody
attribute.
Support API versioning
Minimal APIs support API versioning via the Asp.Versioning.Http package. Examples of configuring versioning with minimal APIs can be found in the API versioning repo.
ASP.NET Core OpenAPI source code on GitHub
Additional Resources
A minimal API app can describe the OpenAPI specification for route handlers using Swashbuckle.
For information about support for OpenAPI in controller-based APIs, see the .NET 9 version of this article.
The following code is a typical ASP.NET Core app with OpenAPI support:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = builder.Environment.ApplicationName,
Version = "v1" });
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger(); // UseSwaggerUI Protected by if (env.IsDevelopment())
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
$"{builder.Environment.ApplicationName} v1"));
}
app.MapGet("/swag", () => "Hello Swagger!");
app.Run();
Exclude OpenAPI description
In the following sample, the /skipme
endpoint is excluded from generating an OpenAPI description:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(); // UseSwaggerUI Protected by if (env.IsDevelopment())
}
app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
Describe response types
The following example uses the built-in result types to customize the response:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
Add operation ids to OpenAPI
app.MapGet("/todoitems2", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithName("GetToDoItems");
Add tags to the OpenAPI description
The following code uses an OpenAPI grouping tag:
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithTags("TodoGroup");
ASP.NET Core