Compatibilidad con OpenAPI en aplicaciones de API mínimas

La especificación OpenAPI es un estándar independiente del lenguaje de programación para documentar las API HTTP. Este estándar se admite en las API mínimas mediante una combinación de API integradas y bibliotecas de código abierto. Existen tres aspectos clave para la integración de OpenAPI en una aplicación:

  • Generación de información sobre los puntos de conexión de la aplicación.
  • Recopilación de la información en un formato que se ajuste al esquema OpenAPI.
  • Exposición del esquema OpenAPI generado a través de una interfaz de usuario visual o de un archivo serializado.

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.

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 en modo de desarrollo.
  • 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, establezca 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 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, puede proporcionar metadatos de las siguientes maneras:

  • Llame 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);
    
  • Use 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 de Results<TResult1,TResult2,TResultN> declaran que un controlador de ruta devuelve varios tipos concretos de implementación de IResult, y cualquiera de esos tipos que implementan 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

La especificación OpenAPI es un estándar independiente del lenguaje de programación para documentar las API HTTP. Este estándar se admite en las API mínimas mediante una combinación de API integradas y bibliotecas de código abierto. Existen tres aspectos clave para la integración de OpenAPI en una aplicación:

  • Generación de información sobre los puntos de conexión de la aplicación.
  • Recopilación de la información en un formato que se ajuste al esquema OpenAPI.
  • Exposición del esquema OpenAPI generado a través de una interfaz de usuario visual o de un archivo serializado.

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.

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, establezca 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 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, puede proporcionar metadatos de las siguientes maneras:

  • Llame 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);
    
  • Use 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 de Results<TResult1,TResult2,TResultN> declaran que un controlador de ruta devuelve varios tipos concretos de implementación de IResult, y cualquiera de esos tipos que implementan 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 puede describir la especificación de OpenAPI para controladores de rutas mediante Swashbuckle.

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