Compartilhar via


Gerar documentos OpenAPI

O pacote Microsoft.AspNetCore.OpenApi fornece suporte interno para a geração de documentos OpenAPI no ASP.NET Core. O pacote fornece os seguintes recursos:

  • Suporte para geração de documentos OpenAPI em tempo de execução e acesso a esses documentos por meio de um ponto de extremidade no aplicativo.
  • Suporte para APIs "transformadoras" que permitem modificar o documento gerado.
  • Suporte para gerar vários documentos OpenAPI a partir de um único aplicativo.
  • Aproveita o suporte ao esquema JSON fornecido por System.Text.Json.
  • É compatível com o AoT nativo.

Instalação do pacote

Instalar o pacote Microsoft.AspNetCore.OpenApi:

Execute o seguinte comando no Console do Gerenciador de Pacotes:

Install-Package Microsoft.AspNetCore.OpenApi -IncludePrerelease

Para adicionar suporte para a geração de documentos OpenAPI no momento da compilação, instale o pacote Microsoft.Extensions.ApiDescription.Server:

Execute o seguinte comando no Console do Gerenciador de Pacotes:

Install-Package Microsoft.Extensions.ApiDescription.Server -IncludePrerelease

Configurar a geração de documentos OpenAPI

O seguinte código:

  • Adiciona serviços OpenAPI.
  • Habilita o ponto de extremidade para exibir o documento OpenAPI no formato JSON.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

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

app.Run();

Inicie o aplicativo e navegue até https://localhost:<port>/openapi/v1.json para exibir o documento OpenAPI gerado.

Incluir metadados OpenAPI em um aplicativo Web ASP.NET

Inclusão de metadados OpenAPI para pontos de extremidade

O ASP.NET coleta metadados dos pontos de extremidade do aplicativo Web e os usa para gerar um documento OpenAPI. Em aplicativos baseados em controlador, os metadados são coletados de atributos como [EndpointDescription], [HttpPost] e [Produces]. Em APIs mínimas, os metadados podem ser coletados de atributos, mas também podem ser definidos usando métodos de extensão e outras estratégias, como retornar TypedResults de manipuladores de rotas. A tabela a seguir fornece uma visão geral dos metadados coletados e das estratégias para configurá-los.

Metadados Atributo Método de extensão Outras estratégias
summary [EndpointSummary] WithSummary
descrição [EndpointDescription] WithDescription
tags [Tags] WithTags
operationId [EndpointName] WithName
parâmetros [FromQuery], [FromRoute], [FromHeader], [FromForm]
Descrição do parâmetro [Description]
requestBody [FromBody] Accepts
responses [Produces], [ProducesProblem] Produces, ProducesProblem TypedResults
Exclusão de pontos de extremidade [ExcludeFromDescription] ExcludeFromDescription

O ASP.NET Core não coleta metadados de comentários de documentação XML.

As seções a seguir demonstram como incluir metadados em um aplicativo para personalizar o documento OpenAPI gerado.

Resumo e descrição

O resumo e a descrição do ponto de extremidade podem ser definidos usando os atributos [EndpointSummary] e [EndpointDescription] ou, em APIs mínimas, usando os métodos de extensão WithSummary e WithDescription.

A amostra a seguir demonstra as diferentes estratégias para configurar resumos e descrições.

Observe que os atributos são colocados no método delegado e não no método app.MapGet.

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

A OpenAPI dá suporte à especificação de marcas em cada ponto de extremidade como uma forma de categorização. Em aplicativos baseados em controlador, o nome do controlador é adicionado automaticamente como uma marca em cada um de seus pontos de extremidade, mas isso pode ser substituído usando o atributo [Tags]. Em APIs mínimas, as marcas podem ser definidas usando o atributo [Tags] ou o método de extensão WithTags.

O exemplo a seguir demonstra as diferentes estratégias para configurar marcas.

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

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

operationId

O OpenAPI oferece suporte a um operationId em cada ponto de extremidade como um identificador ou nome exclusivo para a operação. Em aplicativos baseados em controlador, o operationId pode ser definido usando o atributo [EndpointName]. Em APIs mínimas, o operationId pode ser definido usando o atributo [EndpointName] ou o método de extensão WithName.

O exemplo a seguir demonstra as diferentes estratégias para configurar o operationId.

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

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

parâmetros

O OpenAPI oferece suporte à anotação de caminho, string de consulta, cabeçalho e parâmetros cookie que são consumidos por uma API.

A estrutura infere os tipos de parâmetros de solicitação automaticamente com base na assinatura do manipulador de rotas.

O atributo [Description] pode ser usado para fornecer uma descrição para um parâmetro.

O exemplo a seguir demonstra como definir uma descrição para um parâmetro.

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

requestBody

Para definir o tipo de entradas transmitidas como o corpo da solicitação, configure as propriedades usando o método de extensão Accepts para definir o tipo de objeto e o tipo de conteúdo esperados pelo manipulador de solicitação. No exemplo a seguir, o ponto de extremidade aceita um objeto Todo no corpo da solicitação com um tipo de conteúdo esperado de application/xml.

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

Além do método de extensão Accepts, um tipo de parâmetro pode descrever sua própria anotação implementando a interface IEndpointParameterMetadataProvider. Por exemplo, o tipo Todo a seguir adiciona uma anotação que requer um corpo da solicitação com um tipo de conteúdo application/xml.

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new AcceptsMetadata(["application/xml", "text/xml"], typeof(XmlBody)));
    }
}

Quando nenhuma anotação explícita é fornecida, a estrutura tenta determinar o tipo de solicitação padrão se houver um parâmetro do corpo da solicitação no manipulador de ponto de extremidade. A inferência usa a seguinte heurística para produzir a anotação:

  • Os parâmetros do corpo da solicitação lidos de um formulário por meio do atributo [FromForm] são descritos com o tipo de conteúdo multipart/form-data.
  • Todos os outros parâmetros do corpo da solicitação são descritos com o tipo de conteúdo application/json.
  • O corpo da solicitação será tratado como opcional se for anulável ou se a propriedade AllowEmpty estiver definida no atributo FromBody.

Descrever os tipos de resposta

O OpenAPI dá suporte ao fornecimento de uma descrição das respostas retornadas de uma API. As APIs mínimas dão suporte a três estratégias para definir o tipo de resposta de um ponto de extremidade:

O método de extensão Produces pode ser usado para adicionar metadados Produces a um ponto de extremidade. Quando nenhum parâmetro é fornecido, o método de extensão preenche metadados para o tipo de destino em um código de status 200 e um tipo de conteúdo application/json.

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

Usar TypedResults na implementação do manipulador de rotas de um ponto de extremidade inclui automaticamente os metadados de tipo de resposta para o ponto de extremidade. Por exemplo, o código a seguir anota automaticamente o ponto de extremidade com uma resposta no código de status 200 com um tipo de conteúdo application/json.

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync();
    return TypedResults.Ok(todos);
});
Definir respostas para ProblemDetails

Ao definir o tipo de resposta para pontos de extremidade que podem retornar uma resposta ProblemDetails, o método de extensão ProducesProblem ou ProducesValidationProblem ou TypedResults.Problem pode ser usado para adicionar a anotação apropriada aos metadados do ponto de extremidade.

Quando não há anotações explícitas fornecidas por uma dessas estratégias, a estrutura tenta determinar um tipo de resposta padrão examinando a assinatura da resposta. Essa resposta padrão é preenchida sob o código de status 200 na definição do OpenAPI.

Vários tipos de resposta

Se um ponto de extremidade puder retornar diferentes tipos de resposta em cenários diferentes, você poderá fornecer metadados das seguintes maneiras:

  • Chame o método de extensão Produces várias vezes, conforme mostrado no exemplo a seguir:

    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> na assinatura e TypedResults no corpo do manipulador, conforme mostrado no exemplo a seguir:

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

    Os Results<TResult1,TResult2,TResultN> (tipos de união) declaram que um manipulador de rotas retorna vários tipos concretos de implementação IResult, e qualquer um desses tipos que implementam IEndpointMetadataProvider contribuirá para os metadados do ponto de extremidade.

    Os tipos de união implementam operadores de conversão implícitos. Esses operadores permitem que o compilador converta automaticamente os tipos especificados nos argumentos genéricos em uma instância do tipo de união. Essa funcionalidade tem o benefício adicional de verificar durante o tempo de compilação se um manipulador de rotas retorna apenas os resultados que ele declara retornar. Tentar retornar um tipo que não é declarado como um dos argumentos genéricos para Results<TResult1,TResult2,TResultN> resulta em um erro de compilação.

Excluindo pontos de extremidade do documento gerado

Por padrão, todos os pontos de extremidade definidos em um aplicativo são documentados no arquivo OpenAPI gerado. As APIs mínimas oferecem suporte a duas estratégias para excluir um determinado ponto de extremidade do documento OpenAPI, usando:

A amostra a seguir demonstra as diferentes estratégias para excluir um determinado ponto de extremidade do documento OpenAPI gerado.

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

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

Inclusão de metadados OpenAPI para tipos de dados

As classes ou registros C# usados em corpos de solicitação ou resposta são representados como esquemas no documento OpenAPI gerado. Por padrão, somente as propriedades públicas são representadas no esquema, mas há JsonSerializerOptions para criar propriedades de esquema para campos.

Quando PropertyNamingPolicy é definido como minúsculas concatenadas (esse é o padrão em aplicativos Web ASP.NET), os nomes de propriedade em um esquema estão na forma de minúsculas concatenadas do nome da propriedade de classe ou registro. O JsonPropertyNameAttribute pode ser usado em uma propriedade individual para especificar o nome da propriedade no esquema.

tipo e formato

A biblioteca de esquema JSON mapeia tipos C# padrão para OpenAPI type e format da seguinte maneira:

Tipo de C# OpenAPI type OpenAPI format
INT Número inteiro int32
longo Número inteiro int64
short Número inteiro int16
byte Número inteiro uint8
float número FLOAT
double número duplo
decimal número duplo
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
objeto omitted
dinâmico omitted

Observe que os tipos de objeto e de dinâmica não têm nenhum tipo definido no OpenAPI porque podem conter dados de qualquer tipo, incluindo tipos primitivos como números inteiros ou cadeias de caracteres.

O type e format também podem ser definidos com um Transformador de esquema. Por exemplo, você pode querer que o format de tipos decimais sejam decimal em vez de double.

Usar atributos para adicionar metadados

ASP.NET usa metadados de atributos em propriedades de classe ou registro para definir metadados nas propriedades correspondentes do esquema gerado.

A tabela a seguir resume os atributos do namespace System.ComponentModel que fornecem metadados para o esquema gerado:

Atributo Descrição
DescriptionAttribute Define o description de uma propriedade no esquema.
RequiredAttribute Marca uma propriedade como required no esquema.
DefaultValueAttribute Define o valor default de uma propriedade no esquema.
RangeAttribute Define o valor minimum e maximum de um número inteiro ou número.
MinLengthAttribute Define o minLength de uma cadeia de caracteres.
MaxLengthAttribute Define o maxLength de uma cadeia de caracteres.
RegularExpressionAttribute Define o pattern de uma cadeia de caracteres.

Observe que, em aplicativos baseados em controlador, esses atributos adicionam filtros à operação para validar se os dados de entrada atendem às restrições. Nas APIs mínimas, esses atributos definem os metadados no esquema gerado, mas a validação deve ser executada explicitamente por meio de um filtro de ponto de extremidade, na lógica do manipulador de rotas ou por meio de um pacote de terceiros.

Outras fontes de metadados para esquemas gerados

obrigatório

As propriedades também podem ser marcadas como required com o modificador obrigatório.

enum

Os tipos de enumeração em C# são baseados em inteiros, mas podem ser representados como cadeias de caracteres em JSON com um JsonConverterAttribute e um JsonStringEnumConverter. Quando um tipo enumerado é representado como uma cadeia de caracteres em JSON, o esquema gerado terá uma propriedade enum com os valores de cadeia de caracteres da enumeração. Um tipo enumerado sem um JsonConverterAttribute será definido como type: integer no esquema gerado.

Observação: o AllowedValuesAttribute não define os valores enum da propriedade.

nullable

As propriedades definidas como um valor anulável ou tipo de referência têm nullable: true no esquema gerado. Isso é consistente com o comportamento padrão do desserializador System.Text.Json, que aceita null como um valor válido para uma propriedade que permite valor nulo.

additionalProperties

Os esquemas são gerados sem uma declaração additionalProperties por padrão, o que implica o padrão de true. Isso é consistente com o comportamento padrão do desserializador System.Text.Json, que ignora silenciosamente propriedades adicionais em um objeto JSON.

Se as propriedades adicionais de um esquema tiverem apenas valores de um tipo específico, defina a propriedade ou classe como um Dictionary<string, type>. O tipo de chave para o dicionário deve ser string. Isso gera um esquema com additionalProperties especificando o esquema para "type" como os tipos de valor necessários.

Metadados para tipos polimórficos

Use os atributos JsonPolymorphicAttribute e JsonDerivedTypeAttribute em uma classe primária para especificar o campo discriminador e os subtipos para um tipo polimórfico.

O JsonDerivedTypeAttribute adiciona o campo discriminador ao esquema para cada subclasse, com uma enumeração especificando o valor discriminador específico para a subclasse. Esse atributo também modifica o construtor de cada classe derivada para definir o valor do discriminador.

Uma classe abstrata com um atributo JsonPolymorphicAttribute tem um campo discriminator no esquema, mas uma classe concreta com um atributo JsonPolymorphicAttribute não tem um campo discriminator. O OpenAPI requer que a propriedade discriminatória seja uma propriedade obrigatória no esquema, mas como a propriedade discriminatória não está definida na classe base concreta, o esquema não pode incluir um campo discriminator.

Adicionar metadados com um transformador de esquema

Um transformador de esquema pode ser usado para substituir quaisquer metadados padrão ou adicionar metadados adicionais, como valores example, ao esquema gerado. Para obter mais informações, confira Usar transformadores de esquema.

Opções para personalizar a geração de documentos OpenAPI

As seções a seguir demonstram como personalizar a geração de documentos OpenAPI.

Personalizar o nome do documento OpenAPI

Cada documento OpenAPI em um aplicativo tem um nome exclusivo. O nome do documento padrão registrado é v1.

builder.Services.AddOpenApi(); // Document name is v1

Para modificar o nome do documento, passe o nome como um parâmetro para a chamada AddOpenApi.

builder.Services.AddOpenApi("internal"); // Document name is internal

O nome do documento aparece em vários locais na implementação do OpenAPI.

Ao buscar o documento OpenAPI gerado, o nome do documento é fornecido como o argumento de parâmetro documentName na solicitação. As solicitações a seguir resolvem os documentos v1 e internal.

GET http://localhost:5000/openapi/v1.json
GET http://localhost:5000/openapi/internal.json

Personalizar a versão do OpenAPI de um documento gerado

Por padrão, a geração de documentos OpenAPI cria um documento compatível com a v3.0 da especificação OpenAPI. O código a seguir demonstra como modificar a versão padrão do documento OpenAPI:

builder.Services.AddOpenApi(options =>
{
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0;
});

Personalizar a rota do ponto de extremidade OpenAPI

Por padrão, o ponto de extremidade OpenAPI registrado por meio de uma chamada para MapOpenApi expõe o documento no ponto de extremidade /openapi/{documentName}.json. O código a seguir demonstra como personalizar a rota na qual o documento OpenAPI está registrado:

app.MapOpenApi("/openapi/{documentName}/openapi.json");

É possível, mas não recomendado, remover o parâmetro de rota documentName da rota do ponto de extremidade. Quando o parâmetro de rota documentName é removido da rota do ponto de extremidade, a estrutura tenta resolver o nome do documento a partir do parâmetro de consulta. Não fornecer o documentName na rota ou na consulta pode resultar em um comportamento inesperado.

Personalizar o ponto de extremidade OpenAPI

Como o documento OpenAPI é servido por meio de um ponto de extremidade do manipulador de rota, qualquer personalização disponível para ponto de extremidade mínimos padrão estará disponível para o ponto de extremidade OpenAPI.

Limitar o acesso a documentos OpenAPI a usuários autorizados

O ponto de extremidade OpenAPI não permite nenhuma verificação de autorização por padrão. No entanto, as verificações de autorização podem ser aplicadas ao documento OpenAPI. No código a seguir, o acesso ao documento OpenAPI é limitado àqueles com a função tester:

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

Documento OpenAPI gerado pelo cache

O documento OpenAPI é gerado novamente sempre que uma solicitação ao ponto de extremidade OpenAPI é enviada. A regeneração permite que os transformadores incorporem o estado dinâmico do aplicativo em sua operação. Por exemplo, regenerar uma solicitação com detalhes do contexto HTTP. Quando aplicável, o documento OpenAPI pode ser armazenado em cache para evitar a execução do pipeline de geração de documentos em cada solicitação HTTP.

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

Transformadores de documento OpenAPI

Esta seção demonstra como personalizar documentos OpenAPI com transformadores.

Personalizar documentos OpenAPI com transformadores

Os transformadores fornecem uma API para modificar o documento OpenAPI com personalizações definidas pelo usuário. Transformadores são úteis para cenários como:

  • Adicionar parâmetros a todas as operações em um documento.
  • Modificar descrições para parâmetros ou operações.
  • Adicionar informações de nível superior ao documento OpenAPI.

Os transformadores se enquadram em três categorias:

  • Os transformadores de documento têm acesso a todo o documento OpenAPI. Eles podem ser usados para fazer modificações globais no documento.
  • Os transformadores de operação se aplicam a cada operação individual. Cada operação individual é uma combinação de caminho e método HTTP. Elas podem ser usadas para modificar parâmetros ou respostas em pontos de extremidade.
  • Os transformadores de esquema se aplicam a cada esquema no documento. Eles podem ser usados para modificar o esquema de corpos de solicitação ou resposta ou quaisquer esquemas aninhados.

Os transformadores podem ser registrados no documento chamando o metodo AddDocumentTransformer no objeto OpenApiOptions. O seguinte trecho de código mostra diferentes maneiras de registrar transformadores no documento:

  • Registre um transformador de documento usando um delegado.
  • Registre um transformador de documento usando uma instância de IOpenApiDocumentTransformer.
  • Registre um transformador de documento usando um IOpenApiDocumentTransformer ativado por DI.
  • Registrar um transformador de operação usando um delegado.
  • Registrar um transformador de operação usando uma instância de IOpenApiOperationTransformer.
  • Registrar um transformador de operação usando um IOpenApiOperationTransformer ativado por DI.
  • Registrar um transformador de esquema usando um delegado.
  • Registrar um transformador de esquema usando uma instância de IOpenApiSchemaTransformer.
  • Registrar um transformador de esquema usando um IOpenApiSchemaTransformer ativado por DI.
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();

Ordem de execução para transformadores

Os transformadores são executados na ordem "primeiro a entrar, primeiro a sair" com base no registro. No trecho de código a seguir, o transformador de documento tem acesso às modificações feitas pelo transformador de operação:

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

Usar transformadores de documento

Os transformadores de documento têm acesso a um objeto de contexto que inclui:

  • O nome do documento que está sendo modificado.
  • A lista de ApiDescriptionGroups associados a esse documento.
  • O IServiceProvider usado na geração de documentos.

Os transformadores de documento também podem alterar o documento OpenAPI gerado. O exemplo a seguir demonstra um transformador de documento que adiciona algumas informações sobre a API ao documento OpenAPI.

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

Os transformadores de documento ativados pelo serviço podem utilizar instâncias de DI para modificar o aplicativo. O exemplo a seguir demonstra um transformador de documento que usa o serviço IAuthenticationSchemeProvider da camada de autenticação. Ele verifica se algum esquema relacionado ao portador JWT está registrado no aplicativo e os adiciona ao nível superior do documento OpenAPI:

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

Os transformadores de documento são exclusivos da instância do documento à qual estão associados. No exemplo a seguir, um transformador:

  • Registra requisitos relacionados à autenticação no documento internal.
  • Deixa o documento public não modificado.
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>()
                });
            }
        }
    }
}

Usar transformadores de operação

As operações são combinações exclusivas de métodos e caminhos HTTP em um documento OpenAPI. Os transformadores de operação são úteis quando uma modificação:

  • Deve ser feita para cada ponto de extremidade em um aplicativo ou
  • Aplicada condicionalmente a determinadas rotas.

Os transformadores de operação têm acesso a um objeto de contexto que contém:

  • O nome do documento ao qual a operação pertence.
  • O ApiDescription associado à operação.
  • O IServiceProvider usado na geração de documentos.

Por exemplo, o transformador de operação a seguir adiciona 500 como um código de status de resposta compatível com todas as operações no documento.

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

Usar transformadores de esquema

Esquemas são os modelos de dados usados em corpos de solicitação e resposta em um documento OpenAPI. Os transformadores de esquema são úteis quando uma modificação:

  • Deve ser feito para cada esquema no documento, ou
  • Aplicada condicionalmente a determinados esquemas.

Os transformadores de esquema têm acesso a um objeto de contexto que contém:

  • O nome do documento ao qual o esquema pertence.
  • As informações de tipo JSON associadas ao esquema de destino.
  • O IServiceProvider usado na geração de documentos.

Por exemplo, o transformador de esquema a seguir define o format de tipos decimais como decimal em vez de 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; }
}

Recursos adicionais

As APIs mínimas fornecem suporte interno para gerar informações sobre pontos de extremidade em um aplicativo por meio do pacote Microsoft.AspNetCore.OpenApi. Expor a definição do OpenAPI gerada por meio de uma interface do usuário visual requer um pacote de terceiros. Para obter informações sobre o suporte para OpenAPI em APIs baseadas em controlador, consulte a versão .NET 9 deste artigo.

O código a seguir é gerado pelo modelo de API Web mínima do ASP.NET Core e usa o 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);
}

No código realçado anterior:

  • O Microsoft.AspNetCore.OpenApi é explicado na próxima seção.
  • AddEndpointsApiExplorer: configura o aplicativo para usar a API Explorer para descobrir e descrever pontos de extremidade com anotações padrão. O WithOpenApisubstitui as anotações padrão e correspondentes geradas pela API Explorer com aquelas produzidas no pacote Microsoft.AspNetCore.OpenApi.
  • O UseSwaggeradiciona o middleware Swagger.
  • “UseSwaggerUI” permite uma versão incorporada da ferramenta Swagger UI.
  • WithName: o IEndpointNameMetadata no ponto de extremidade é usado para geração de links e é tratado como a ID da operação na especificação do OpenAPI do ponto de extremidade fornecida.
  • O WithOpenApi é explicado posteriormente neste artigo.

Pacote NuGet Microsoft.AspNetCore.OpenApi

O ASP.NET Core fornece o pacote Microsoft.AspNetCore.OpenApi para interagir com as especificações do OpenAPI para pontos de extremidade. O pacote atua como um link entre os modelos do OpenAPI definidos no pacote Microsoft.AspNetCore.OpenApi e os pontos de extremidade definidos em APIs Mínimas. O pacote fornece uma API que examina parâmetros, respostas e metadados de um ponto de extremidade para construir um tipo de anotação do OpenAPI usado para descrever um ponto de extremidade.

O Microsoft.AspNetCore.OpenApi é adicionado como um PackageReference a um arquivo de projeto:

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

Ao usar Swashbuckle.AspNetCore com Microsoft.AspNetCore.OpenApi, a versão 6.4.0 do Swashbuckle.AspNetCore ou posterior deve ser usada. A versão 1.4.3 do Microsoft.OpenApi ou posterior deve ser usada para aproveitar os construtores de cópia nas invocações do WithOpenApi.

Adicionar anotações do OpenAPI a pontos de extremidade por meio do WithOpenApi

Chamar o WithOpenApi no ponto de extremidade complementa os metadados do ponto de extremidade. Esses metadados podem ser:

  • Consumidos em pacotes de terceiros, como Swashbuckle.AspNetCore.
  • Exibidos na interface do usuário do Swagger, no YAML ou no JSON gerado para definir a 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();

Modificar a anotação do OpenAPI no WithOpenApi

O método WithOpenApi aceita uma função que pode ser usada para modificar a anotação do OpenAPI. Por exemplo, no código a seguir, uma descrição é adicionada ao primeiro parâmetro do ponto de extremidade:

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

Adicionar IDs de operação ao OpenAPI

As IDs de operação são usadas para identificar exclusivamente um determinado ponto de extremidade no OpenAPI. O método de extensão WithName pode ser usado para definir a ID da operação usada para um método .

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

Como alternativa, a propriedade OperationId pode ser definida diretamente na anotação do OpenAPI.

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        OperationId = "GetTodos"
    });

Adicionar marcas à descrição do OpenAPI

O OpenAPI dá suporte ao uso de objetos de marca para categorizar operações. Normalmente, essas marcas são usadas para agrupar operações na interface do usuário do Swagger. Essas marcas podem ser adicionadas a uma operação invocando o método de extensão WithTags no ponto de extremidade com as marcas desejadas.

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");

Como alternativa, a lista de OpenApiTags pode ser definida na anotação do OpenAPI por meio do método de extensão WithOpenApi.

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Tags = new List<OpenApiTag> { new() { Name = "Todos" } }
    });

Adicionar resumo ou descrição do ponto de extremidade

O resumo e a descrição do ponto de extremidade podem ser adicionados invocando o método de extensão WithOpenApi. No código a seguir, os resumos são definidos diretamente na anotação do OpenAPI.

app.MapGet("/todoitems2", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Summary = "This is a summary",
        Description = "This is a description"
    });

Excluir descrição do OpenAPI

No exemplo a seguir, o ponto de extremidade /skipme é excluído da geração de uma descrição do 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();

app.MapGet("/swag", () => "Hello Swagger!")
    .WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
                    .ExcludeFromDescription();

app.Run();

Marcar uma API como obsoleta

Para marcar um ponto de extremidade como obsoleto, defina a propriedade Deprecated na anotação do OpenAPI.

app.MapGet("/todos", async (TodoDb db) => await db.Todos.ToListAsync())
    .WithOpenApi(operation => new(operation)
    {
        Deprecated = true
    });

Descrever os tipos de resposta

O OpenAPI dá suporte ao fornecimento de uma descrição das respostas retornadas de uma API. As APIs mínimas dão suporte a três estratégias para definir o tipo de resposta de um ponto de extremidade:

O método de extensão Produces pode ser usado para adicionar metadados Produces a um ponto de extremidade. Quando nenhum parâmetro é fornecido, o método de extensão preenche metadados para o tipo de destino em um código de status 200 e um tipo de conteúdo application/json.

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

Usar TypedResults na implementação do manipulador de rotas de um ponto de extremidade inclui automaticamente os metadados de tipo de resposta para o ponto de extremidade. Por exemplo, o código a seguir anota automaticamente o ponto de extremidade com uma resposta no código de status 200 com um tipo de conteúdo application/json.

app.MapGet("/todos", async (TodoDb db) =>
{
    var todos = await db.Todos.ToListAsync());
    return TypedResults.Ok(todos);
});

Definir respostas para ProblemDetails

Ao definir o tipo de resposta para pontos de extremidade que podem retornar uma resposta ProblemDetails, o método de extensão ProducesProblem, ProducesValidationProblem ou TypedResults.Problem pode ser usado para adicionar a anotação apropriada aos metadados do ponto de extremidade. Observe que os métodos de extensão ProducesProblem e ProducesValidationProblem não podem ser usados com grupos de rotas no .NET 8 e anteriores.

Quando não há anotações explícitas fornecidas por uma das estratégias acima, a estrutura tenta determinar um tipo de resposta padrão examinando a assinatura da resposta. Essa resposta padrão é preenchida sob o código de status 200 na definição do OpenAPI.

Vários tipos de resposta

Se um ponto de extremidade puder retornar diferentes tipos de resposta em cenários diferentes, você poderá fornecer metadados das seguintes maneiras:

  • Chame o método de extensão Produces várias vezes, conforme mostrado no exemplo a seguir:

    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> na assinatura e TypedResults no corpo do manipulador, conforme mostrado no exemplo a seguir:

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

    Os Results<TResult1,TResult2,TResultN> (tipos de união) declaram que um manipulador de rotas retorna vários tipos concretos de implementação IResult, e qualquer um desses tipos que implementam IEndpointMetadataProvider contribuirá para os metadados do ponto de extremidade.

    Os tipos de união implementam operadores de conversão implícitos. Esses operadores permitem que o compilador converta automaticamente os tipos especificados nos argumentos genéricos em uma instância do tipo de união. Essa funcionalidade tem o benefício adicional de verificar durante o tempo de compilação se um manipulador de rotas retorna apenas os resultados que ele declara retornar. Tentar retornar um tipo que não é declarado como um dos argumentos genéricos para Results<TResult1,TResult2,TResultN> resulta em um erro de compilação.

Descrever o corpo da solicitação e os parâmetros

Além de descrever os tipos retornados por um ponto de extremidade, o OpenAPI também dá suporte à anotação das entradas consumidas por uma API. Essas entradas se enquadram em duas categorias:

  • Parâmetros que aparecem no caminho, cadeia de caracteres de consulta, cabeçalhos ou cookies
  • Dados transmitidos como parte do corpo da solicitação

A estrutura infere os tipos de parâmetros de solicitação no caminho, na consulta e na cadeia de caracteres de cabeçalho automaticamente com base na assinatura do manipulador de rotas.

Para definir o tipo de entradas transmitidas como o corpo da solicitação, configure as propriedades usando o método de extensão Accepts para definir o tipo de objeto e o tipo de conteúdo esperados pelo manipulador de solicitação. No exemplo a seguir, o ponto de extremidade aceita um objeto Todo no corpo da solicitação com um tipo de conteúdo esperado de application/xml.

app.MapPost("/todos/{id}", (int id, Todo todo) => ...)
  .Accepts<Todo>("application/xml");

Além do método de extensão Accepts, um tipo de parâmetro pode descrever sua própria anotação implementando a interface IEndpointParameterMetadataProvider. Por exemplo, o tipo Todo a seguir adiciona uma anotação que requer um corpo da solicitação com um tipo de conteúdo application/xml.

public class Todo : IEndpointParameterMetadataProvider
{
    public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ConsumesAttribute(typeof(Todo), isOptional: false, "application/xml"));
    }
}

Quando nenhuma anotação explícita é fornecida, a estrutura tenta determinar o tipo de solicitação padrão se houver um parâmetro do corpo da solicitação no manipulador de ponto de extremidade. A inferência usa a seguinte heurística para produzir a anotação:

  • Os parâmetros do corpo da solicitação lidos de um formulário por meio do atributo [FromForm] são descritos com o tipo de conteúdo multipart/form-data.
  • Todos os outros parâmetros do corpo da solicitação são descritos com o tipo de conteúdo application/json.
  • O corpo da solicitação será tratado como opcional se for anulável ou se a propriedade AllowEmpty estiver definida no atributo FromBody.

Suporte ao controle de versão da API

As APIs mínimas dão suporte ao controle de versão da API por meio do pacote Asp.Versioning.Http. Exemplos de configuração de controle de versão com APIs mínimas podem ser encontrados no repositório de controle de versão da API.

Código-fonte do ASP.NET Core OpenAPI no GitHub

Recursos adicionais

Um aplicativo de API mínima pode descrever a especificação do OpenAPI para manipuladores de rota usando o Swashbuckle.

Para obter informações sobre o suporte para OpenAPI em APIs baseadas em controlador, consulte a versão .NET 9 deste artigo.

O código a seguir é um aplicativo do ASP.NET Core típico com suporte ao OpenAPI:

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

Excluir descrição do OpenAPI

No exemplo a seguir, o ponto de extremidade /skipme é excluído da geração de uma descrição do OpenAPI:

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

Descrever os tipos de resposta

O exemplo a seguir usa os tipos de resultados internos para personalizar a resposta:

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

Adicionar IDs de operação ao OpenAPI

app.MapGet("/todoitems2", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithName("GetToDoItems");

Adicionar marcas à descrição do OpenAPI

O código a seguir usa uma marca de agrupamento do OpenAPI:

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.ToListAsync())
    .WithTags("TodoGroup");