Compartir a través de


Generación de documentos de OpenAPI

El paquete Microsoft.AspNetCore.OpenApi proporciona compatibilidad integrada con la generación de documentos de OpenAPI en ASP.NET Core. El paquete proporciona las características siguientes:

  • Compatibilidad con la generación de documentos de OpenAPI en tiempo de ejecución y acceso a ellos a través de un punto de conexión en la aplicación.
  • Compatibilidad con las API "transformadoras" que permiten modificar el documento generado.
  • Compatibilidad con la generación de varios documentos OpenAPI desde una sola aplicación.
  • Aprovecha la compatibilidad de esquemas JSON proporcionada por System.Text.Json.
  • Es compatible con AoT nativo.

Instalación del paquete

Instala el paquete Microsoft.AspNetCore.OpenApi:

Ejecuta el siguiente comando desde la Consola del Administrador de paquetes:

Install-Package Microsoft.AspNetCore.OpenApi -IncludePrerelease

Para agregar compatibilidad con la generación de documentos OpenAPI en tiempo de compilación, instala el paquete Microsoft.Extensions.ApiDescription.Server:

Ejecuta el siguiente comando desde la Consola del Administrador de paquetes:

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

Configuración de la generación de documentos de OpenAPI

El código siguiente:

  • Agrega servicios de OpenAPI.
  • Habilita el punto de conexión para ver el documento OpenAPI en formato JSON.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

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

app.Run();

Inicia la aplicación y ve a https://localhost:<port>/openapi/v1.json para ver el documento de OpenAPI generado.

Inclusión de metadatos de OpenAPI en una aplicación web de ASP.NET

Inclusión de metadatos de OpenAPI para puntos de conexión

ASP.NET recopila metadatos de los puntos de conexión de la aplicación web y los usa para generar un documento de OpenAPI. En las aplicaciones basadas en controladores, los metadatos se recopilan de atributos como [EndpointDescription], [HttpPost] y [Produces]. En las API mínimas, los metadatos se pueden recopilar de atributos, pero también se pueden establecer mediante métodos de extensión y otras estrategias, como la devolución de TypedResults de controladores de ruta. En la tabla siguiente se proporciona información general sobre los metadatos recopilados y las estrategias para establecerlos.

Metadatos Attribute Método de extensión Otras estrategias
summary [EndpointSummary] WithSummary
descripción [EndpointDescription] WithDescription
etiquetas [Tags] WithTags
operationId [EndpointName] WithName
parámetros [FromQuery], [FromRoute], [FromHeader], [FromForm]
Descripción de los parámetros [Description]
requestBody [FromBody] Accepts
responses [Produces], [ProducesProblem] Produces, ProducesProblem TypedResults
Exclusión de puntos de conexión [ExcludeFromDescription] ExcludeFromDescription

ASP.NET Core no recopila metadatos de comentarios de documentos XML.

En las siguientes secciones se muestra cómo incluir metadatos en una aplicación para personalizar el documento OpenAPI generado.

Resumen y descripción

El resumen y la descripción del punto de conexión se pueden establecer mediante los atributos [EndpointSummary] y [EndpointDescription], o en las API mínimas, mediante los métodos de extensión WithSummary y WithDescription.

La siguiente muestra demuestra las diferentes estrategias para establecer resúmenes y descripciones.

Ten en cuenta que los atributos se colocan en el método delegado y no en el 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!");

etiquetas

OpenAPI admite la especificación de etiquetas en cada punto de conexión como forma de categorización. En las aplicaciones basadas en controlador, el nombre del controlador se agrega automáticamente como una etiqueta en cada uno de sus puntos de conexión, pero esto se puede invalidar mediante el atributo [Tags]. En las API mínimas, las etiquetas se pueden establecer mediante el atributo [Tags] o el método de extensión WithTags.

La siguiente muestra demuestra las diferentes estrategias para establecer etiquetas.

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

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

operationId

OpenAPI es compatible con un operationId en cada punto de conexión como un identificador único o un nombre para la operación. En las aplicaciones basadas en controlador, el operationId se puede establecer mediante el atributo [EndpointName]. En las API mínimas, el operationId se puede establecer mediante el atributo [EndpointName] o el método de extensión WithName.

La siguiente muestra demuestra las diferentes estrategias para establecer el operationId.

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

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

parámetros

OpenAPI admite la anotación de la ruta de acceso, la cadena de consulta, el encabezado y los parámetros cookie que consume una API.

El marco deduce automáticamente los tipos de parámetros de solicitud basándose en la signatura del controlador de ruta.

El atributo [Description] se puede usar para proporcionar una descripción para un parámetro.

En el siguiente ejemplo se muestra cómo establecer una descripción de un parámetro.

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

requestBody

Para definir el tipo de entradas que se transmiten como cuerpo de la solicitud, configura las propiedades mediante el método de extensión Accepts para definir el tipo de objeto y el tipo de contenido que espera el controlador de solicitudes. En el ejemplo siguiente, el punto de conexión acepta un objeto Todo en el cuerpo de la solicitud con un elemento content-type esperado de application/xml.

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

Además del método de extensión Accepts, un tipo de parámetro puede describir su propia anotación mediante la implementación de la interfaz IEndpointParameterMetadataProvider. Por ejemplo, el tipo Todo siguiente agrega una anotación que requiere un cuerpo de la solicitud con un elemento content-type 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)));
    }
}

Cuando no se proporciona ninguna anotación explícita, el marco intenta determinar el tipo de solicitud predeterminado si hay un parámetro de cuerpo de la solicitud en el controlador de punto de conexión. La inferencia usa la heurística siguiente para generar la anotación:

  • Los parámetros del cuerpo de la solicitud que se leen desde un formulario mediante el atributo [FromForm] se describen con el elemento content-type multipart/form-data.
  • Todos los demás parámetros del cuerpo de la solicitud se describen con el elemento content-type application/json.
  • El cuerpo de la solicitud se trata como opcional si admite un valor NULL o si la propiedad AllowEmpty se establece en el atributo FromBody.

Describir los tipos de respuesta

OpenAPI permite proporcionar una descripción de las respuestas que devuelve una API. Las API mínimas admiten tres estrategias para establecer el tipo de respuesta de un punto de conexión:

  • Mediante el método de extensión Produces en el punto de conexión.
  • Mediante el atributo ProducesResponseType en el controlador de ruta.
  • Mediante la devolución de TypedResults desde el controlador de ruta.

El método de extensión Produces se puede usar para agregar metadatos de Produces a un punto de conexión. Cuando no se proporciona ningún parámetro, el método de extensión rellena los metadatos del tipo de destino bajo un código de estado 200 y un tipo de contenido application/json.

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

Al usar TypedResults en la implementación del controlador de ruta de un punto de conexión, se incluyen automáticamente los metadatos del tipo de respuesta para dicho punto. Por ejemplo, el código siguiente anota automáticamente una respuesta en el punto de conexión bajo el código de estado 200 con un tipo de contenido application/json.

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

Al establecer el tipo de respuesta para los puntos de conexión que pueden devolver una respuesta ProblemDetails, se puede usar el método de extensión ProducesProblem o ProducesValidationProblem o TypedResults.Problem para agregar la anotación adecuada a los metadatos del punto de conexión.

Cuando las estrategias anteriores no proporcionan anotaciones explícitas, el marco intenta determinar un tipo de respuesta predeterminado mediante el examen de la signatura de la respuesta. Esta respuesta predeterminada se rellena bajo el código de estado 200 en la definición de OpenAPI.

Tipos de respuestas múltiples

Si un punto de conexión puede devolver diferentes tipos de respuesta en escenarios distintos, puedes proporcionar metadatos de las siguientes maneras:

  • Llama al método de extensión Produces varias veces, como se muestra en el ejemplo siguiente:

    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);
    
  • Usa Results<TResult1,TResult2,TResultN> en la firma y TypedResults en el cuerpo del controlador, como se muestra en el ejemplo siguiente:

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

    Los tipos de unión Results<TResult1,TResult2,TResultN> declaran que un controlador de ruta devuelve varios tipos de elementos IResult que implementan tipos concretos, y cualquiera de esos tipos que implementa IEndpointMetadataProvider contribuirá a los metadatos del punto de conexión.

    Los tipos de unión implementan operadores de conversión implícitos. Estos operadores habilitan el compilador para convertir automáticamente los tipos especificados en los argumentos genéricos en una instancia del tipo de unión. Esto tiene la ventaja adicional de proporcionar la comprobación en tiempo de compilación de que un controlador de ruta solo devuelve los resultados que sí declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<TResult1,TResult2,TResultN>, se producirá un error de compilación.

Exclusión de puntos de conexión del documento generado

De forma predeterminada, todos los puntos de conexión definidos en una aplicación se documentan en el archivo OpenAPI generado. Las API mínimas admiten dos estrategias para excluir un punto de conexión determinado del documento OpenAPI, mediante lo siguiente:

La siguiente muestra presenta las diferentes estrategias para excluir un punto de conexión determinado del documento OpenAPI generado.

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

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

Inclusión de metadatos de OpenAPI para tipos de datos

Las clases o registros de C# usados en los cuerpos de solicitud o respuesta se representan como esquemas en el documento OpenAPI generado. De forma predeterminada, solo las propiedades públicas se representan en el esquema, pero también hay JsonSerializerOptions para crear propiedades de esquema para los campos.

Cuando PropertyNamingPolicy se establece en camel-case (este es el valor predeterminado en las aplicaciones web ASP.NET), los nombres de propiedad de un esquema son la forma de mayúsculas y minúsculas de la clase o el nombre de la propiedad de registro. JsonPropertyNameAttribute se puede usar en una propiedad individual para especificar el nombre de la propiedad en el esquema.

tipo y formato

La biblioteca de esquemas JSON asigna los tipos estándar de C# a OpenAPI type y format de la siguiente manera:

Tipo de C# OpenAPI type OpenAPI format
int integer int32
long integer int64
short integer int16
byte integer uint8
FLOAT number float
double number doble
decimal number doble
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

Ten en cuenta que los tipos dinámicos y de objeto no tienen ningún tipo definido en OpenAPI porque pueden contener datos de cualquier tipo, incluidos tipos primitivos como int o string.

type y format también se pueden establecer con un transformador de esquema. Por ejemplo, puede que quieras que format de los tipos decimales sean decimal en lugar de double.

Uso de atributos para agregar metadatos

ASP.NET usa metadatos de atributos de propiedades de clase o registro para establecer metadatos en las propiedades correspondientes del esquema generado.

En la tabla siguiente se resumen los atributos del espacio de nombres System.ComponentModel que proporcionan metadatos para el esquema generado:

Atributo Descripción
DescriptionAttribute Establece description de una propiedad en el esquema.
RequiredAttribute Marca una propiedad como required en el esquema.
DefaultValueAttribute Establece el valor default de una propiedad en el esquema.
RangeAttribute Establece el valor minimum y maximum de un número o entero.
MinLengthAttribute Establece minLength de una cadena.
MaxLengthAttribute Establece maxLength de una cadena.
RegularExpressionAttribute Establece pattern de una cadena.

Ten en cuenta que en las aplicaciones basadas en el controlador, estos atributos agregan filtros a la operación para validar que los datos entrantes satisfacen las restricciones. En las API mínimas, estos atributos establecen los metadatos en el esquema generado, pero la validación debe realizarse explícitamente a través de un filtro de punto de conexión, en la lógica del controlador de ruta o a través de un paquete de terceros.

Otros orígenes de metadatos para esquemas generados

requerido

Las propiedades también se pueden marcar como required con el modificador necesario.

enum

Los tipos de enumeración de C# se basan en enteros, pero se pueden representar como cadenas en JSON con JsonConverterAttribute y JsonStringEnumConverter. Cuando un tipo de enumeración se representa como una cadena en JSON, el esquema generado tendrá una propiedad enum con los valores de cadena de la enumeración. Un tipo de enumeración sin JsonConverterAttribute se definirá como type: integer en el esquema generado.

Nota: AllowedValuesAttribute no establece los valores enum de una propiedad.

nullable

Las propiedades definidas como un valor que admite un valor NULL o un tipo de referencia tienen nullable: true en el esquema generado. Esto es coherente con el comportamiento predeterminado del deserializador System.Text.Json, que acepta null como un valor válido para una propiedad que admite valores NULL.

additionalProperties

Los esquemas se generan sin una aserción additionalProperties de forma predeterminada, lo que implica el valor predeterminado de true. Esto es coherente con el comportamiento predeterminado del deserializador System.Text.Json, que omite silenciosamente las propiedades adicionales de un objeto JSON.

Si las propiedades adicionales de un esquema solo deben tener valores de un tipo específico, define la propiedad o la clase como Dictionary<string, type>. El tipo de clave del diccionario debe ser string. Esto genera un esquema con additionalProperties que especifica el esquema para "type" como los tipos de valor necesarios.

Metadatos para tipos polimórficos

Usa los atributos JsonPolymorphicAttribute y JsonDerivedTypeAttribute de una clase primaria para especificar el campo discriminador y los subtipos para un tipo polimórfico.

JsonDerivedTypeAttribute agrega el campo discriminador al esquema de cada subclase, con una enumeración que especifica el valor discriminador específico para la subclase. Este atributo también modifica el constructor de cada clase derivada para establecer el valor discriminador.

Una clase abstracta con un atributo JsonPolymorphicAttribute tiene un campo discriminator en el esquema, pero una clase concreta con un atributo JsonPolymorphicAttribute no tiene un campo discriminator. OpenAPI requiere que la propiedad Discriminator sea una propiedad necesaria en el esquema, pero dado que la propiedad Discriminator no está definida en la clase base concreta, el esquema no puede incluir un campo discriminator.

Adición de metadatos con un transformador de esquema

Un transformador de esquema se puede usar para invalidar los metadatos predeterminados o agregar metadatos adicionales, como valores example, al esquema generado. Para obtener más información, consulta Uso de transformadores de esquema.

Opciones para personalizar la generación de documentos de OpenAPI

En las secciones siguientes, se muestra cómo personalizar la generación de documentos de OpenAPI.

Personalización del nombre del documento de OpenAPI

Cada documento de OpenAPI en una aplicación tiene un nombre único. El nombre de documento predeterminado que está registrado es v1.

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

El nombre del documento se puede modificar pasando el nombre como parámetro a la llamada AddOpenApi.

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

El nombre del documento aparece en varios lugares de la implementación de OpenAPI.

Al capturar el documento de OpenAPI generado, el nombre del documento se proporciona como argumento de parámetro documentName en la solicitud. Las siguientes solicitudes resuelven los documentos v1 y internal.

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

Personalización de la versión de OpenAPI de un documento generado

De forma predeterminada, la generación de documentos de OpenAPI crea un documento compatible con v3.0 de la especificación OpenAPI. En el código siguiente se muestra cómo modificar la versión predeterminada del documento de OpenAPI:

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

Personalización de la ruta del punto de conexión de OpenAPI

De forma predeterminada, el punto de conexión de OpenAPI registrado a través de una llamada a MapOpenApi expone el documento en el punto de conexión de /openapi/{documentName}.json. En el código siguiente se muestra cómo personalizar la ruta en la que se registra el documento de OpenAPI:

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

Es posible, pero no se recomienda, quitar el parámetro de ruta documentName de la ruta del punto de conexión. Cuando se quita el parámetro de ruta documentName de la ruta del punto de conexión, el marco intenta resolver el nombre del documento del parámetro de consulta. No proporcionar documentName en la ruta o la consulta puede dar lugar a un comportamiento inesperado.

Personalización del punto de conexión de OpenAPI

Dado que el documento OpenAPI se sirve a través de un punto de conexión de controlador de ruta, cualquier personalización disponible para los puntos de conexión mínimos estándar está disponible para el punto de conexión de OpenAPI.

Limitación del acceso al documento de OpenAPI a usuarios autorizados

El punto de conexión de OpenAPI no habilita ninguna comprobación de autorización de forma predeterminada. Sin embargo, las comprobaciones de autorización se pueden aplicar al documento de OpenAPI. En el código siguiente, el acceso al documento de OpenAPI se limita a los que tienen el rol de 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 de OpenAPI generado por caché

El documento de OpenAPI se vuelve a generar cada vez que se envía una solicitud al punto de conexión de OpenAPI. La regeneración permite a los transformadores incorporar el estado dinámico de la aplicación en su funcionamiento. Por ejemplo, regeneración de una solicitud con detalles del contexto HTTP. Si procede, el documento OpenAPI se puede almacenar en caché para evitar ejecutar la canalización de generación del documento en cada solicitud 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 documentos de OpenAPI

En esta sección, se muestra cómo personalizar documentos de OpenAPI con transformadores.

Personalización de documentos de OpenAPI con transformadores

Los transformadores proporcionan una API para modificar el documento de OpenAPI con personalizaciones definidas por el usuario. Los transformadores son útiles para escenarios como:

  • Agregar parámetros a todas las operaciones de un documento.
  • Modificar descripciones para parámetros u operaciones.
  • Agregar información de nivel superior al documento de OpenAPI.

Los transformadores se dividen en tres categorías:

  • Los transformadores de documentos tienen acceso a todo el documento de OpenAPI. Se pueden usar para realizar modificaciones globales en el documento.
  • Los transformadores de operación se aplican a cada operación individual. Cada operación individual es una combinación de ruta de acceso y método HTTP. Se pueden usar para modificar parámetros o respuestas en puntos de conexión.
  • Los transformadores de esquema se aplican a cada esquema del documento. Se pueden usar para modificar el esquema de los cuerpos de solicitud o respuesta, o cualquier esquema anidado.

Los transformadores se pueden registrar en el documento a través de la llamada al método AddDocumentTransformer en el objeto OpenApiOptions. En el fragmento de código siguiente se muestran diferentes formas de registrar transformadores en el documento:

  • Registrar un transformador de documentos mediante un delegado.
  • Registrar un transformador de documentos mediante una instancia de IOpenApiDocumentTransformer.
  • Registrar un transformador de documentos mediante IOpenApiDocumentTransformer activado para DI.
  • Registrar un transformador de operación mediante un delegado.
  • Registrar un transformador de operación mediante una instancia de IOpenApiOperationTransformer.
  • Registrar un transformador de operación mediante IOpenApiOperationTransformer activado para DI.
  • Registrar un transformador de esquemas mediante un delegado.
  • Registrar un transformador de esquemas mediante una instancia de IOpenApiSchemaTransformer.
  • Registrar un transformador de esquemas mediante IOpenApiSchemaTransformer activado para 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();

Orden de ejecución para los transformadores

Los transformadores se ejecutan en el orden “primero en entrar, primero en salir” en función del registro. En el fragmento de código siguiente, el transformador de documentos tiene acceso a las modificaciones realizadas por el transformador de operación:

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

Uso de transformadores de documentos

Los transformadores de documentos tienen acceso a un objeto de contexto que incluye lo siguiente:

  • Nombre del documento que se va a modificar.
  • Lista de ApiDescriptionGroups asociada a ese documento.
  • El IServiceProvider usado en la generación de documentos.

Los transformadores de documentos también pueden mutar el documento de OpenAPI que se genera. En el ejemplo siguiente se muestra un transformador de documentos que agrega información sobre la API al documento de 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();

Los transformadores de documentos activados por el servicio pueden usar instancias de DI para modificar la aplicación. En el ejemplo siguiente se muestra un transformador de documentos que usa el servicio IAuthenticationSchemeProvider de la capa de autenticación. Comprueba si se registran esquemas relacionados con el portador JWT en la aplicación y los agrega al nivel superior del documento de 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;
        }
    }
}

Los transformadores de documentos son únicos para la instancia de documento a la que están asociados. En el ejemplo siguiente, un transformador:

  • Registra los requisitos relacionados con la autenticación en el documento internal.
  • Deja sin modificar el documento public.
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>()
                });
            }
        }
    }
}

Uso de transformadores de operación

Las operaciones son combinaciones únicas de rutas de acceso y métodos HTTP en un documento de OpenAPI. Los transformadores de operación son útiles cuando una modificación:

  • Debe realizarse en cada punto de conexión de una aplicación o
  • Se aplica condicionalmente a determinadas rutas.

Los transformadores de operación tienen acceso a un objeto de contexto que contiene lo siguiente:

  • Nombre del documento al que pertenece la operación.
  • ApiDescription asociado con la operación.
  • El IServiceProvider usado en la generación de documentos.

Por ejemplo, el siguiente transformador de operación agrega 500 como código de estado de respuesta admitido por todas las operaciones del 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();

Uso de transformadores de esquema

Los esquemas son los modelos de datos que se usan en los cuerpos de solicitud y respuesta de un documento de OpenAPI. Los transformadores de esquema son útiles cuando una modificación:

  • Se debe realizar en cada esquema del documento o
  • Se aplica condicionalmente a determinados esquemas.

Los transformadores de esquema tienen acceso a un objeto de contexto que contiene lo siguiente:

  • El nombre del documento al que pertenece el esquema.
  • La información de tipo JSON asociada al esquema de destino.
  • El IServiceProvider usado en la generación de documentos.

Por ejemplo, el siguiente transformador de esquema establece format de tipos decimales en decimal en lugar 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 adicionales

Las API mínimas proporcionan compatibilidad integrada para generar información sobre los puntos de conexión de una aplicación mediante el paquete Microsoft.AspNetCore.OpenApi. La exposición de la definición de OpenAPI generada a través de una interfaz de usuario visual requiere un paquete de terceros. Para obtener información sobre la compatibilidad con OpenAPI en API basadas en controladores, consulta la versión .NET 9 de este artículo.

El siguiente código se genera mediante la plantilla de API web mínima de ASP.NET Core y usa 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);
}

En el código resaltado anterior:

  • Microsoft.AspNetCore.OpenApi se explica en la sección siguiente.
  • AddEndpointsApiExplorer: configura la aplicación para usar el Explorador de API para detectar y describir puntos de conexión con anotaciones predeterminadas. WithOpenApi reemplaza las anotaciones coincidentes predeterminadas generadas por el Explorador de API por las generadas desde el paquete Microsoft.AspNetCore.OpenApi.
  • UseSwagger agrega el middleware de Swagger.
  • "UseSwaggerUI" habilita una versión insertada de la herramienta de interfaz de usuario de Swagger.
  • WithName: el objeto IEndpointNameMetadata del punto de conexión se usa para la generación de vínculos y se trata como identificador de operación en la especificación de OpenAPI del punto de conexión dado.
  • WithOpenApi se explica posteriormente en este artículo.

Paquete NuGet Microsoft.AspNetCore.OpenApi

ASP.NET Core proporciona el paquete Microsoft.AspNetCore.OpenApi para interactuar con las especificaciones de OpenAPI de los puntos de conexión. El paquete actúa como vínculo entre los modelos de OpenAPI definidos en el paquete Microsoft.AspNetCore.OpenApi y los puntos de conexión definidos en las API mínimas. El paquete proporciona una API que examina los parámetros, las respuestas y los metadatos de un punto de conexión para construir un tipo de anotación de OpenAPI que se usa para describir un punto de conexión.

Microsoft.AspNetCore.OpenApi se agrega como PackageReference a un archivo de proyecto:

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

Cuando se usa Swashbuckle.AspNetCore con Microsoft.AspNetCore.OpenApi, se debe usar Swashbuckle.AspNetCore 6.4.0 y versiones posteriores. Se debe usar Microsoft.OpenApi 1.4.3 o versiones posteriores para aprovechar los constructores de copias en las invocaciones de WithOpenApi.

Adición de anotaciones de OpenAPI a los puntos de conexión mediante WithOpenApi

La llamada a WithOpenApi en el punto de conexión se agrega a los metadatos del punto de conexión. Estos metadatos presentan las siguientes características:

  • Se pueden consumir en paquetes de terceros, como Swashbuckle.AspNetCore.
  • Se muestran en la interfaz de usuario de Swagger o en el código YAML o JSON generado para definir la 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();

Modificación de la anotación de OpenAPI en WithOpenApi

El método WithOpenApi acepta una función que se puede usar para modificar la anotación de OpenAPI. Por ejemplo, en el código siguiente, se agrega una descripción al primer parámetro del punto de conexión:

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

Adición de identificadores de operación a OpenAPI

Los identificadores de operación se usan para identificar de forma única un punto de conexión determinado en OpenAPI. El método de extensión WithName se puede usar para establecer el identificador de operación utilizado para un método.

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

Como alternativa, puede establecerse directamente la propiedad OperationId en la anotación de OpenAPI.

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

Adición de etiquetas a la descripción de OpenAPI

OpenAPI admite el uso de objetos de etiqueta para clasificar las operaciones. Estas etiquetas se suelen usar para agrupar operaciones en la interfaz de usuario de Swagger. Las etiquetas se pueden agregar a una operación mediante la invocación del método de extensión WithTags en el punto de conexión con las etiquetas deseadas.

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

Como alternativa, se puede establecer la lista de OpenApiTags en la anotación de OpenAPI mediante el método de extensión WithOpenApi.

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

Agregar resumen o descripción del punto de conexión

El resumen y la descripción del punto de conexión se pueden agregar mediante la invocación del método de extensiónWithOpenApi. En el código siguiente, los resúmenes se establecen directamente en la anotación de 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"
    });

Exclusión de la descripción de OpenAPI

En el ejemplo siguiente, el punto de conexión /skipme se excluye de la generación de una descripción de 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();

Marcado de una API como obsoleta

Para marcar un punto de conexión como obsoleto, establece la propiedad Deprecated en la anotación de OpenAPI.

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

Describir los tipos de respuesta

OpenAPI permite proporcionar una descripción de las respuestas que devuelve una API. Las API mínimas admiten tres estrategias para establecer el tipo de respuesta de un punto de conexión:

  • Mediante el método de extensión Produces en el punto de conexión
  • Mediante el atributo ProducesResponseType en el controlador de ruta
  • Mediante la devolución de TypedResults desde el controlador de ruta

El método de extensión Produces se puede usar para agregar metadatos de Produces a un punto de conexión. Cuando no se proporciona ningún parámetro, el método de extensión rellena los metadatos del tipo de destino bajo un código de estado 200 y un tipo de contenido application/json.

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

Al usar TypedResults en la implementación del controlador de ruta de un punto de conexión, se incluyen automáticamente los metadatos del tipo de respuesta para dicho punto. Por ejemplo, el código siguiente anota automáticamente una respuesta en el punto de conexión bajo el código de estado 200 con un tipo de contenido application/json.

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

Establecimiento de respuestas para ProblemDetails

Al establecer el tipo de respuesta para los puntos de conexión que pueden devolver una respuesta ProblemDetails, se puede usar el método de extensión ProducesProblem, ProducesValidationProblem o TypedResults.Problem para agregar la anotación adecuada a los metadatos del punto de conexión. Ten en cuenta que los métodos de extensión ProducesProblem y ProducesValidationProblem no se pueden usar con los grupos de rutas en .NET 8 y versiones anteriores.

Cuando las estrategias anteriores no proporcionan anotaciones explícitas, el marco intenta determinar un tipo de respuesta predeterminado mediante el examen de la signatura de la respuesta. Esta respuesta predeterminada se rellena bajo el código de estado 200 en la definición de OpenAPI.

Tipos de respuestas múltiples

Si un punto de conexión puede devolver diferentes tipos de respuesta en escenarios distintos, puedes proporcionar metadatos de las siguientes maneras:

  • Llama al método de extensión Produces varias veces, como se muestra en el ejemplo siguiente:

    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);
    
  • Usa Results<TResult1,TResult2,TResultN> en la firma y TypedResults en el cuerpo del controlador, como se muestra en el ejemplo siguiente:

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

    Los tipos de unión Results<TResult1,TResult2,TResultN> declaran que un controlador de ruta devuelve varios tipos de elementos IResult que implementan tipos concretos, y cualquiera de esos tipos que implementa IEndpointMetadataProvider contribuirá a los metadatos del punto de conexión.

    Los tipos de unión implementan operadores de conversión implícitos. Estos operadores habilitan el compilador para convertir automáticamente los tipos especificados en los argumentos genéricos en una instancia del tipo de unión. Esto tiene la ventaja adicional de proporcionar la comprobación en tiempo de compilación de que un controlador de ruta solo devuelve los resultados que sí declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<TResult1,TResult2,TResultN>, se producirá un error de compilación.

Descripción de los parámetros y el cuerpo de la solicitud

Además de describir los tipos devueltos por un punto de conexión, OpenAPI también admite la anotación de las entradas que consume una API. Estas entradas se dividen en dos categorías:

  • Parámetros que aparecen en la ruta de acceso, cadena de consulta, encabezados o cookies
  • Datos transmitidos como parte del cuerpo de la solicitud

El marco deduce automáticamente los tipos de parámetros de solicitud en la cadena de encabezado, consulta y ruta de acceso en función de la signatura del controlador de ruta.

Para definir el tipo de entradas que se transmiten como cuerpo de la solicitud, configure las propiedades mediante el método de extensión Accepts para definir el tipo de objeto y el tipo de contenido que espera el controlador de solicitudes. En el ejemplo siguiente, el punto de conexión acepta un objeto Todo en el cuerpo de la solicitud con un elemento content-type esperado de application/xml.

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

Además del método de extensión Accepts, es posible que un tipo de parámetro describa su propia anotación mediante la implementación de la interfaz IEndpointParameterMetadataProvider. Por ejemplo, el tipo Todo siguiente agrega una anotación que requiere un cuerpo de la solicitud con un elemento content-type 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"));
    }
}

Cuando no se proporciona ninguna anotación explícita, el marco intenta determinar el tipo de solicitud predeterminado si hay un parámetro de cuerpo de la solicitud en el controlador de punto de conexión. La inferencia usa la heurística siguiente para generar la anotación:

  • Los parámetros del cuerpo de la solicitud que se leen desde un formulario mediante el atributo [FromForm] se describen con el elemento content-type multipart/form-data.
  • Todos los demás parámetros del cuerpo de la solicitud se describen con el elemento content-type application/json.
  • El cuerpo de la solicitud se trata como opcional si admite un valor NULL o si la propiedad AllowEmpty se establece en el atributo FromBody.

Compatibilidad con el control de versiones de API

Las API mínimas admiten el control de versiones de API mediante el paquete Asp.Versioning.Http. Puede encontrar ejemplos de configuración del control de versiones con API mínimas en el repositorio de control de versiones de API.

Código fuente de OpenAPI de ASP.NET Core en GitHub

Recursos adicionales

Una aplicación de API mínima puede describir la especificación de OpenAPI para controladores de rutas mediante Swashbuckle.

Para obtener información sobre la compatibilidad con OpenAPI en API basadas en controladores, consulta la versión .NET 9 de este artículo.

El código siguiente es una aplicación ASP.NET Core típica compatible con 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();

Exclusión de la descripción de OpenAPI

En el ejemplo siguiente, el punto de conexión /skipme se excluye de la generación de una descripción de 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();

Describir los tipos de respuesta

En el ejemplo siguiente se usan los tipos de resultados integrados para personalizar la respuesta:

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

Adición de identificadores de operación a OpenAPI

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

Adición de etiquetas a la descripción de OpenAPI

En el código siguiente se usa una etiqueta de agrupación de OpenAPI:

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