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 -IncludePrerelease

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 -IncludePrerelease

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.

Including OpenAPI metadata in an ASP.NET web app

Including OpenAPI metadata for endpoints

ASP.NET collects metadata from the web app's endpoints and uses it to generate an OpenAPI document. In controller-based apps, metadata is collected from attributes like [EndpointDescription], [HttpPost], and [Produces]. In minimal APIs, metadata can be collected from attributes, but may also be set by using extension methods and other strategies, such as returning TypedResults from route handlers. The following table provides an overview of the metadata collected and the strategies for setting it.

Metadata Attribute Extension method Other strategies
summary [EndpointSummary] WithSummary
description [EndpointDescription] WithDescription
tags [Tags] WithTags
operationId [EndpointName] WithName
parameters [FromQuery], [FromRoute], [FromHeader], [FromForm]
parameter description [Description]
requestBody [FromBody] Accepts
responses [Produces], [ProducesProblem] Produces, ProducesProblem TypedResults
Excluding endpoints [ExcludeFromDescription] ExcludeFromDescription

ASP.NET Core does not collect metadata from XML doc comments.

The following sections demonstrate how to include metadata in an app to customize the generated OpenAPI document.

Summary and description

The endpoint summary and description can be set using the [EndpointSummary] and [EndpointDescription] attributes, or in minimal APIs, using the WithSummary and WithDescription extension methods.

The following sample demonstrates the different strategies for setting summaries and descriptions.

Note that the attributes are placed on the delegate method and not on the app.MapGet method.

app.MapGet("/extension-methods", () => "Hello world!")
  .WithSummary("This is a summary.")
  .WithDescription("This is a description.");

app.MapGet("/attributes",
  [EndpointSummary("This is a summary.")]
  [EndpointDescription("This is a description.")]
  () => "Hello world!");

tags

OpenAPI supports specifying tags on each endpoint as a form of categorization. In controller-based apps, the controller name is automatically added as a tag on each of its endpoints, but this can be overridden using the [Tags] attribute. In minimal APIs, tags can be set using either the [Tags] attribute or the WithTags extension method.

The following sample demonstrates the different strategies for setting tags.

app.MapGet("/extension-methods", () => "Hello world!")
  .WithTags("todos", "projects");

app.MapGet("/attributes",
  [Tags("todos", "projects")]
  () => "Hello world!");

operationId

OpenAPI supports an operationId on each endpoint as a unique identifier or name for the operation. In controller-based apps, the operationId can be set using the [EndpointName] attribute. In minimal APIs, the operationId can be set using either the [EndpointName] attribute or the WithName extension method.

The following sample demonstrates the different strategies for setting the operationId.

app.MapGet("/extension-methods", () => "Hello world!")
  .WithName("FromExtensionMethods");

app.MapGet("/attributes",
  [EndpointName("FromAttributes")]
  () => "Hello world!");

parameters

OpenAPI supports annotating path, query string, header, and cookie parameters that are consumed by an API.

The framework infers the types for request parameters automatically based on the signature of the route handler.

The [Description] attribute can be used to provide a description for a parameter.

The follow sample demonstrates how to set a description for a parameter.

app.MapGet("/attributes",
  ([Description("This is a description.")] string name) => "Hello world!");

requestBody

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 AcceptsMetadata(["application/xml", "text/xml"], typeof(XmlBody)));
    }
}

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 the multipart/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 the FromBody attribute.

Describe response types

OpenAPI supports providing a description of the responses returned from an API. ASP.NET Core provides several strategies for setting the response metadata of an endpoint. Response metadata that can be set includes the status code, the type of the response body, and content type(s) of a response. Responses in OpenAPI may have additional metadata, such as description, headers, links, and examples. This additional metadata can be set with a document transformer or operation transformer.

The specific mechanisms for setting response metadata depend on the type of app being developed.

In Minimal API apps, ASP.NET Core can extract the response metadata added by extension methods on the endpoint, attributes on the route handler, and the return type of the route handler.

  • The Produces extension method can be used on the endpoint to specify the status code, the type of the response body, and content type(s) of a response from an endpoint.
  • The ProducesResponseTypeAttribute or ProducesResponseTypeAttribute<T> attribute can be used to specify the type of the response body.
  • A route handler can be used to return a type that implements IEndpointMetadataProvider to specify the type and content-type(s) of the response body.
  • The ProducesProblem extension method on the endpoint can be used to specify the status code and content-type(s) of an error response.

Note that the Produces and ProducesProblem extension methods are supported on both RouteHandlerBuilder and on RouteGroupBuilder. This allows, for example, a common set of error responses to be defined for all operations in a group.

When not specified by one of the preceding strategies, the:

  • Status code for the response defaults to 200.
  • Schema for the response body can be inferred from the implicit or explicit return type of the endpoint method, for example, from T in Task<TResult>; otherwise, it's considered to be unspecified.
  • Content-type for the specified or inferred response body is "application/json".

In Minimal APIs, the Produces extension method and the ProducesResponseTypeAttribute attribute only set the response metadata for the endpoint. They do not modify or constrain the behavior of the endpoint, which may return a different status code or response body type than specified by the metadata, and the content-type is determined by the return type of the route handler method, irrespective of any content-type specified in attributes or extension methods.

The Produces extension method can specify an endpoint's response type, with a default status code of 200 and a default content type of application/json. The following example illustrates this:

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
  .Produces<IList<Todo>>();

The ProducesResponseTypeAttribute can be used to add response metadata to an endpoint. Note that the attribute is applied to the route handler method, not the method invocation to create the route, as shown in the following example:

app.MapGet("/todos",
    [ProducesResponseType<List<Todo>>(200)]
    async (TodoDb db) => await db.Todos.ToListAsync());

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);
});

Only return types that implement IEndpointMetadataProvider create a responses entry in the OpenAPI document. The following is a partial list of some of the TypedResults helper methods that produce a responses entry:

TypedResults helper method status code
Ok() 200
Created() 201
CreatedAtRoute() 201
Accepted() 202
AcceptedAtRoute() 202
NoContent() 204
BadRequest() 400
ValidationProblem() 400
NotFound() 404
Conflict() 409
UnprocessableEntity() 422

All of these methods except NoContent have a generic overload that specifies the type of the response body.

A class can be implemented to set the endpoint metadata and return it from the route handler.

Set responses for ProblemDetails

When setting the response type for endpoints that may return a ProblemDetails response, the following can be used to add the appropriate response metadata for the endpoint:

For more information on how to configure a Minimal API app to return ProblemDetails responses, see Handle errors in minimal APIs.

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 and TypedResults 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 multiple IResult-implementing concrete types, and any of those types that implement IEndpointMetadataProvider 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.

Excluding endpoints from the generated document

By default, all endpoints that are defined in an app are documented in the generated OpenAPI file, but endpoints can be excluded from the document using attributes or extension methods.

The mechanism for specifying an endpoint that should be excluded depends on the type of app being developed.

Minimal APIs support two strategies for excluding a given endpoint from the OpenAPI document:

The following sample demonstrates the different strategies for excluding a given endpoint from the generated OpenAPI document.

app.MapGet("/extension-method", () => "Hello world!")
  .ExcludeFromDescription();

app.MapGet("/attributes",
  [ExcludeFromDescription]
  () => "Hello world!");

Including OpenAPI metadata for data types

C# classes or records used in request or response bodies are represented as schemas in the generated OpenAPI document. By default, only public properties are represented in the schema, but there are JsonSerializerOptions to also create schema properties for fields.

When the PropertyNamingPolicy is set to camel-case (this is the default in ASP.NET web applications), property names in a schema are the camel-case form of the class or record property name. The JsonPropertyNameAttribute can be used on an individual property to specify the name of the property in the schema.

type and format

The JSON Schema library maps standard C# types to OpenAPI type and format as follows:

C# Type OpenAPI type OpenAPI format
int integer int32
long integer int64
short integer int16
byte integer uint8
float number float
double number double
decimal number double
bool boolean
string string
char string char
byte[] string byte
DateTimeOffset string date-time
DateOnly string date
TimeOnly string time
Uri string uri
Guid string uuid
object omitted
dynamic omitted

Note that object and dynamic types have no type defined in the OpenAPI because these can contain data of any type, including primitive types like int or string.

The type and format can also be set with a Schema Transformer. For example, you may want the format of decimal types to be decimal instead of double.

Using attributes to add metadata

ASP.NET uses metadata from attributes on class or record properties to set metadata on the corresponding properties of the generated schema.

The following table summarizes attributes from the System.ComponentModel namespace that provide metadata for the generated schema:

Attribute Description
DescriptionAttribute Sets the description of a property in the schema.
RequiredAttribute Marks a property as required in the schema.
DefaultValueAttribute Sets the default value of a property in the schema.
RangeAttribute Sets the minimum and maximum value of an integer or number.
MinLengthAttribute Sets the minLength of a string.
MaxLengthAttribute Sets the maxLength of a string.
RegularExpressionAttribute Sets the pattern of a string.

Note that in controller-based apps, these attributes add filters to the operation to validate that any incoming data satisfies the constraints. In Minimal APIs, these attributes set the metadata in the generated schema but validation must be performed explicitly via an endpoint filter, in the route handler's logic, or via a third-party package.

Other sources of metadata for generated schemas

required

Properties can also be marked as required with the required modifier.

enum

Enum types in C# are integer-based, but can be represented as strings in JSON with a JsonConverterAttribute and a JsonStringEnumConverter. When an enum type is represented as a string in JSON, the generated schema will have an enum property with the string values of the enum. An enum type without a JsonConverterAttribute will be defined as type: integer in the generated schema.

Note: The AllowedValuesAttribute does not set the enum values of a property.

nullable

Properties defined as a nullable value or reference type have nullable: true in the generated schema. This is consistent with the default behavior of the System.Text.Json deserializer, which accepts null as a valid value for a nullable property.

additionalProperties

Schemas are generated without an additionalProperties assertion by default, which implies the default of true. This is consistent with the default behavior of the System.Text.Json deserializer, which silently ignores additional properties in a JSON object.

If the additional properties of a schema should only have values of a specific type, define the property or class as a Dictionary<string, type>. The key type for the dictionary must be string. This generates a schema with additionalProperties specifying the schema for "type" as the required value types.

Metadata for polymorphic types

Use the JsonPolymorphicAttribute and JsonDerivedTypeAttribute attributes on a parent class to to specify the discriminator field and subtypes for a polymorphic type.

The JsonDerivedTypeAttribute adds the discriminator field to the schema for each subclass, with an enum specifying the specific discriminator value for the subclass. This attribute also modifies the constructor of each derived class to set the discriminator value.

An abstract class with a JsonPolymorphicAttribute attribute has a discriminator field in the schema, but a concrete class with a JsonPolymorphicAttribute attribute doesn't have a discriminator field. OpenAPI requires that the discriminator property be a required property in the schema, but since the discriminator property isn't defined in the concrete base class, the schema cannot include a discriminator field.

Adding metadata with a schema transformer

A schema transformer can be used to override any default metadata or add additional metadata, such as example values, to the generated schema. See Use schema transformers for more information.

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();

OpenAPI document transformers

This section demonstrates how to customize OpenAPI documents with transformers.

Customize OpenAPI documents with transformers

Transformers provide an API for modifying the OpenAPI document with user-defined customizations. Transformers are useful for scenarios like:

  • Adding parameters to all operations in a document.
  • Modifying descriptions for parameters or operations.
  • Adding top-level information to the OpenAPI document.

Transformers fall into three categories:

  • Document transformers have access to the entire OpenAPI document. These can be used to make global modifications to the document.
  • Operation transformers apply to each individual operation. Each individual operation is a combination of path and HTTP method. These can be used to modify parameters or responses on endpoints.
  • Schema transformers apply to each schema in the document. These can be used to modify the schema of request or response bodies, or any nested schemas.

Transformers can be registered onto the document by calling the AddDocumentTransformer method on the OpenApiOptions object. The following snippet shows different ways to register transformers onto the document:

  • Register a document transformer using a delegate.
  • Register a document transformer using an instance of IOpenApiDocumentTransformer.
  • Register a document transformer using a DI-activated IOpenApiDocumentTransformer.
  • Register an operation transformer using a delegate.
  • Register an operation transformer using an instance of IOpenApiOperationTransformer.
  • Register an operation transformer using a DI-activated IOpenApiOperationTransformer.
  • Register a schema transformer using a delegate.
  • Register a schema transformer using an instance of IOpenApiSchemaTransformer.
  • Register a schema transformer using a DI-activated IOpenApiSchemaTransformer.
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken)
                             => Task.CompletedTask);
    options.AddDocumentTransformer(new MyDocumentTransformer());
    options.AddDocumentTransformer<MyDocumentTransformer>();
    options.AddOperationTransformer((operation, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddOperationTransformer(new MyOperationTransformer());
    options.AddOperationTransformer<MyOperationTransformer>();
    options.AddSchemaTransformer((schema, context, cancellationToken)
                            => Task.CompletedTask);
    options.AddSchemaTransformer(new MySchemaTransformer());
    options.AddSchemaTransformer<MySchemaTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

Execution order for transformers

Transformers execute in first-in first-out order based on registration. In the following snippet, the document transformer has access to the modifications made by the operation transformer:

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken)
                                     => Task.CompletedTask);
    options.AddDocumentTransformer((document, context, cancellationToken)
                                     => Task.CompletedTask);
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

Use document transformers

Document transformers have access to a context object that includes:

  • The name of the document being modified.
  • The list of ApiDescriptionGroups associated with that document.
  • The IServiceProvider used in document generation.

Document transformers can also mutate the OpenAPI document that is generated. The following example demonstrates a document transformer that adds some information about the API to the OpenAPI document.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        document.Info = new()
        {
            Title = "Checkout API",
            Version = "v1",
            Description = "API for processing checkouts from cart."
        };
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

Service-activated document transformers can utilize instances from DI to modify the app. The following sample demonstrates a document transformer that uses the IAuthenticationSchemeProvider service from the authentication layer. It checks if any JWT bearer-related schemes are registered in the app and adds them to the OpenAPI document's top level:

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.AddOpenApi(options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;
        }
    }
}

Document transformers are unique to the document instance they're associated with. In the following example, a transformer:

  • Registers authentication-related requirements to the internal document.
  • Leaves the public document unmodified.
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.AddOpenApi("internal", options =>
{
    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
});
builder.Services.AddOpenApi("public");

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/world", () => "Hello world!")
    .WithGroupName("internal");
app.MapGet("/", () => "Hello universe!")
    .WithGroupName("public");

app.Run();

internal sealed class BearerSecuritySchemeTransformer(IAuthenticationSchemeProvider authenticationSchemeProvider) : IOpenApiDocumentTransformer
{
    public async Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
        if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer"))
        {
            // Add the security scheme at the document level
            var requirements = new Dictionary<string, OpenApiSecurityScheme>
            {
                ["Bearer"] = new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer", // "bearer" refers to the header name here
                    In = ParameterLocation.Header,
                    BearerFormat = "Json Web Token"
                }
            };
            document.Components ??= new OpenApiComponents();
            document.Components.SecuritySchemes = requirements;

            // Apply it as a requirement for all operations
            foreach (var operation in document.Paths.Values.SelectMany(path => path.Operations))
            {
                operation.Value.Security.Add(new OpenApiSecurityRequirement
                {
                    [new OpenApiSecurityScheme { Reference = new OpenApiReference { Id = "Bearer", Type = ReferenceType.SecurityScheme } }] = Array.Empty<string>()
                });
            }
        }
    }
}

Use operation transformers

Operations are unique combinations of HTTP paths and methods in an OpenAPI document. Operation transformers are helpful when a modification:

  • Should be made to each endpoint in an app, or
  • Conditionally applied to certain routes.

Operation transformers have access to a context object which contains:

  • The name of the document the operation belongs to.
  • The ApiDescription associated with the operation.
  • The IServiceProvider used in document generation.

For example, the following operation transformer adds 500 as a response status code supported by all operations in the document.

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.AddOpenApi(options =>
{
    options.AddOperationTransformer((operation, context, cancellationToken) =>
    {
        operation.Responses.Add("500", new OpenApiResponse { Description = "Internal server error" });
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

Use schema transformers

Schemas are the data models that are used in request and response bodies in an OpenAPI document. Schema transformers are useful when a modification:

  • Should be made to each schema in the document, or
  • Conditionally applied to certain schemas.

Schema transformers have access to a context object which contains:

  • The name of the document the schema belongs to.
  • The JSON type information associated with the target schema.
  • The IServiceProvider used in document generation.

For example, the following schema transformer sets the format of decimal types to decimal instead of double:

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options => {
    // Schema transformer to set the format of decimal to 'decimal'
    options.AddSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.JsonTypeInfo.Type == typeof(decimal))
        {
            schema.Format = "decimal";
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => new Body { Amount = 1.1m });

app.Run();

public class Body {
    public decimal Amount { get; set; }
}

Additional resources

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 the Microsoft.AspNetCore.OpenApi package.
  • UseSwaggeradds 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:

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 and TypedResults 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 multiple IResult-implementing concrete types, and any of those types that implement IEndpointMetadataProvider 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 the multipart/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 the FromBody 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");