Cómo crear respuestas en aplicaciones de API mínimas

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

Los puntos de conexión mínimos admiten los siguientes tipos de valores devueltos:

  1. string: incluye Task<string> y ValueTask<string>.
  2. T (cualquier otro tipo): incluye Task<T> y ValueTask<T>.
  3. Basado en IResult: incluye Task<IResult> y ValueTask<IResult>.

Valores devueltos string

Comportamiento Content-Type
El marco escribe la cadena directamente en la respuesta. text/plain

Tenga en cuenta el siguiente controlador de ruta, que devuelve un texto Hello world.

app.MapGet("/hello", () => "Hello World");

El código de estado 200 se devuelve con el encabezado Content-Type text/plain y el siguiente contenido.

Hello World

Valores devueltos T (cualquier otro tipo)

Comportamiento Content-Type
El marco JSON serializa la respuesta. application/json

Tenga en cuenta el siguiente controlador de ruta, que devuelve un tipo anónimo que contiene una propiedad de cadena Message.

app.MapGet("/hello", () => new { Message = "Hello World" });

El código de estado 200 se devuelve con el encabezado Content-Type application/json y el siguiente contenido.

{"message":"Hello World"}

Valores devueltos IResult

Comportamiento Content-Type
El marco llama a IResult.ExecuteAsync. Decidido por la implementación de IResult.

La interfaz IResult define un contrato que representa el resultado de un punto de conexión HTTP. Las clases estáticas Results y TypedResults se usan para crear distintos objetos IResult que representan diferentes tipos de respuestas.

TypedResults frente a Results

Las clases estáticas Results y TypedResults proporcionan conjuntos de asistentes de resultados similares. La clase TypedResults es el equivalente con tipo de la clase Results. Sin embargo, el tipo de valor devuelto de los asistentes Results es IResult, mientras que cada tipo de valor devuelto del asistente TypedResults es uno de los tipos de implementación IResult. La diferencia significa que para los asistentes Results hace falta una conversión cuando el tipo concreto es necesario, por ejemplo, para las pruebas unitarias. Los tipos de implementación se definen en el espacio de nombres Microsoft.AspNetCore.Http.HttpResults.

Devolver TypedResults en lugar de Results tiene las siguientes ventajas:

Tenga en cuenta el punto de conexión siguiente, para el que se genera un código de estado 200 OK con la respuesta JSON esperada.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

Para documentar este punto de conexión correctamente, se llama al método de extensiones Produces. Sin embargo, no es necesario llamar a Produces si se usa TypedResults en lugar de Results, como se muestra en el código siguiente. TypedResults proporciona automáticamente los metadatos para el punto de conexión.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Para obtener más información sobre cómo describir un tipo de respuesta, consulte Compatibilidad con OpenAPI en API mínimas.

Como se mencionó anteriormente, cuando se usa TypedResults, no se necesita una conversión. Tenga en cuenta la siguiente API mínima que devuelve una clase TypedResults.

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

La prueba siguiente comprueba el tipo concreto completo:

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

Dado que todos los métodos de Results devuelven IResult en su firma, el compilador deduce automáticamente que como el tipo de valor devuelto del delegado de solicitud al devolver resultados diferentes de un único punto de conexión. TypedResults requiere el uso de Results<T1, TN> de dichos delegados.

El método siguiente se compila porque Results.Ok y Results.NotFound se declaran como IResult devueltos, aunque los tipos concretos reales de los objetos devueltos son diferentes:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

El método siguiente no se compila, ya que TypedResults.Ok y TypedResults.NotFound se declaran como devolver tipos diferentes y el compilador no intentará deducir el mejor tipo de coincidencia:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

Para usar TypedResults, el tipo de valor devuelto debe declararse completamente, que cuando es asincrónico requiere el contenedor Task<>. El uso de TypedResults es más detallado, pero es el inconveniente de tener la información de tipo disponible estáticamente y, por lo tanto, capaz de describirse automáticamente en OpenAPI:

app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());

Results<TResult1, TResultN>

Use Results<TResult1, TResultN> como tipo de valor devuelto del controlador de punto de conexión en lugar de IResult cuando:

  • Se devuelvan varios tipos de implementación IResult desde el controlador de punto de conexión.
  • La clase estática TypedResult se use para crear los objetos IResult.

Esta alternativa es mejor que devolver IResult porque los tipos de unión genéricos conservan automáticamente los metadatos del punto de conexión. Y dado que los tipos de unión Results<TResult1, TResultN> implementan operadores de conversión implícitos, el compilador puede 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 realmente los resultados que declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<>, se producirá un error de compilación.

Tenga en cuenta el siguiente punto de conexión, para el que se devuelve un código de estado 400 BadRequest cuando orderId es mayor que 999. De lo contrario, genera una respuesta de que todo está correcto (200 OK) con el contenido esperado.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Para documentar este punto de conexión correctamente, se llama al método de extensión Produces. Sin embargo, dado que el asistente TypedResults incluye automáticamente los metadatos para el punto de conexión, puede devolver el tipo de unión Results<T1, Tn> en su lugar, como se muestra en el código siguiente.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Resultados integrados

Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults es preferible a devolver Results. Para más información, consulte TypedResults frente a Results.

En las secciones siguientes se muestra el uso de los asistentes de resultados comunes.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync es una manera alternativa de devolver JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Código de estado personalizado

app.MapGet("/405", () => Results.StatusCode(405));

Internal Server Error

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

En el ejemplo anterior se devuelve un código de estado 500.

Texto

app.MapGet("/text", () => Results.Text("This is some text"));

STREAM

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream las sobrecargas permiten el acceso al flujo de respuesta HTTP subyacente sin almacenamiento en búfer. En el ejemplo siguiente se usa ImageSharp para devolver un tamaño reducido de la imagen especificada:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

En el ejemplo siguiente se transmite una imagen desde Azure Blob Storage:

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

En el ejemplo siguiente se transmite un vídeo desde un blob de Azure:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Redirect

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Archivo

app.MapGet("/download", () => Results.File("myfile.text"));

Interfaces HttpResult

Las interfaces siguientes del espacio de nombres Microsoft.AspNetCore.Http proporcionan una manera de detectar el tipo IResult en tiempo de ejecución, que es un patrón común en las implementaciones de filtros:

Este es un ejemplo de un filtro que usa una de estas interfaces:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

Para obtener más información, consulte Filtros en las aplicaciones de API mínimas y Tipos de implementación de IResult.

Personalización de respuestas

Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Además, un tipo IResult personalizado puede proporcionar su propia anotación implementando la interfaz IEndpointMetadataProvider. Por ejemplo, el código siguiente agrega una anotación al tipo HtmlResult anterior que describe la respuesta generada por el punto de conexión.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata es una implementación de IProducesResponseTypeMetadata que define el tipo de contenido de respuesta generada text/html y el código de estado 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Un enfoque alternativo consiste en usar Microsoft.AspNetCore.Mvc.ProducesAttribute para describir la respuesta generada. El código siguiente cambia el método PopulateMetadata para usar ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Configuración de las opciones de serialización de JSON

De manera predeterminada, las aplicaciones de API mínimas usan las opciones Web defaults durante la serialización y deserialización JSON.

Configuración global de las opciones de serialización de JSON

Las opciones se pueden configurar globalmente para una aplicación mediante la invocación de ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Dado que se incluyen campos, el código anterior lee NameField y lo incluye en el JSON de salida.

Configuración de las opciones de serialización de JSON para un punto de conexión

Para configurar las opciones de serialización para un punto de conexión, invoque Results.Json y páselo a un objeto JsonSerializerOptions, como se muestra en el ejemplo siguiente:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Como alternativa, use una sobrecarga de WriteAsJsonAsync que acepte un objeto JsonSerializerOptions. En el ejemplo siguiente se usa esta sobrecarga para dar formato al JSON de salida:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Recursos adicionales

Los puntos de conexión mínimos admiten los siguientes tipos de valores devueltos:

  1. string: incluye Task<string> y ValueTask<string>.
  2. T (cualquier otro tipo): incluye Task<T> y ValueTask<T>.
  3. Basado en IResult: incluye Task<IResult> y ValueTask<IResult>.

Valores devueltos string

Comportamiento Content-Type
El marco escribe la cadena directamente en la respuesta. text/plain

Tenga en cuenta el siguiente controlador de ruta, que devuelve un texto Hello world.

app.MapGet("/hello", () => "Hello World");

El código de estado 200 se devuelve con el encabezado Content-Type text/plain y el siguiente contenido.

Hello World

Valores devueltos T (cualquier otro tipo)

Comportamiento Content-Type
El marco JSON serializa la respuesta. application/json

Tenga en cuenta el siguiente controlador de ruta, que devuelve un tipo anónimo que contiene una propiedad de cadena Message.

app.MapGet("/hello", () => new { Message = "Hello World" });

El código de estado 200 se devuelve con el encabezado Content-Type application/json y el siguiente contenido.

{"message":"Hello World"}

Valores devueltos IResult

Comportamiento Content-Type
El marco llama a IResult.ExecuteAsync. Decidido por la implementación de IResult.

La interfaz IResult define un contrato que representa el resultado de un punto de conexión HTTP. Las clases estáticas Results y TypedResults se usan para crear distintos objetos IResult que representan diferentes tipos de respuestas.

TypedResults frente a Results

Las clases estáticas Results y TypedResults proporcionan conjuntos de asistentes de resultados similares. La clase TypedResults es el equivalente con tipo de la clase Results. Sin embargo, el tipo de valor devuelto de los asistentes Results es IResult, mientras que cada tipo de valor devuelto del asistente TypedResults es uno de los tipos de implementación IResult. La diferencia significa que para los asistentes Results hace falta una conversión cuando el tipo concreto es necesario, por ejemplo, para las pruebas unitarias. Los tipos de implementación se definen en el espacio de nombres Microsoft.AspNetCore.Http.HttpResults.

Devolver TypedResults en lugar de Results tiene las siguientes ventajas:

Tenga en cuenta el punto de conexión siguiente, para el que se genera un código de estado 200 OK con la respuesta JSON esperada.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

Para documentar este punto de conexión correctamente, se llama al método de extensiones Produces. Sin embargo, no es necesario llamar a Produces si se usa TypedResults en lugar de Results, como se muestra en el código siguiente. TypedResults proporciona automáticamente los metadatos para el punto de conexión.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Para obtener más información sobre cómo describir un tipo de respuesta, consulte Compatibilidad con OpenAPI en API mínimas.

Como se mencionó anteriormente, cuando se usa TypedResults, no se necesita una conversión. Tenga en cuenta la siguiente API mínima que devuelve una clase TypedResults.

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

La prueba siguiente comprueba el tipo concreto completo:

[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title 1",
        Description = "Test description 1",
        IsDone = false
    });

    context.Todos.Add(new Todo
    {
        Id = 2,
        Title = "Test title 2",
        Description = "Test description 2",
        IsDone = true
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetAllTodos(context);

    //Assert
    Assert.IsType<Ok<Todo[]>>(result);
    
    Assert.NotNull(result.Value);
    Assert.NotEmpty(result.Value);
    Assert.Collection(result.Value, todo1 =>
    {
        Assert.Equal(1, todo1.Id);
        Assert.Equal("Test title 1", todo1.Title);
        Assert.False(todo1.IsDone);
    }, todo2 =>
    {
        Assert.Equal(2, todo2.Id);
        Assert.Equal("Test title 2", todo2.Title);
        Assert.True(todo2.IsDone);
    });
}

Dado que todos los métodos de Results devuelven IResult en su firma, el compilador deduce automáticamente que como el tipo de valor devuelto del delegado de solicitud al devolver resultados diferentes de un único punto de conexión. TypedResults requiere el uso de Results<T1, TN> de dichos delegados.

El método siguiente se compila porque Results.Ok y Results.NotFound se declaran como IResult devueltos, aunque los tipos concretos reales de los objetos devueltos son diferentes:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

El método siguiente no se compila, ya que TypedResults.Ok y TypedResults.NotFound se declaran como devolver tipos diferentes y el compilador no intentará deducir el mejor tipo de coincidencia:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

Para usar TypedResults, el tipo de valor devuelto debe declararse completamente, que cuando es asincrónico requiere el contenedor Task<>. El uso de TypedResults es más detallado, pero es el inconveniente de tener la información de tipo disponible estáticamente y, por lo tanto, capaz de describirse automáticamente en OpenAPI:

app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
   await db.Todos.FindAsync(id)
    is Todo todo
       ? TypedResults.Ok(todo)
       : TypedResults.NotFound());

Results<TResult1, TResultN>

Use Results<TResult1, TResultN> como tipo de valor devuelto del controlador de punto de conexión en lugar de IResult cuando:

  • Se devuelvan varios tipos de implementación IResult desde el controlador de punto de conexión.
  • La clase estática TypedResult se use para crear los objetos IResult.

Esta alternativa es mejor que devolver IResult porque los tipos de unión genéricos conservan automáticamente los metadatos del punto de conexión. Y dado que los tipos de unión Results<TResult1, TResultN> implementan operadores de conversión implícitos, el compilador puede 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 realmente los resultados que declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<>, se producirá un error de compilación.

Tenga en cuenta el siguiente punto de conexión, para el que se devuelve un código de estado 400 BadRequest cuando orderId es mayor que 999. De lo contrario, genera una respuesta de que todo está correcto (200 OK) con el contenido esperado.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Para documentar este punto de conexión correctamente, se llama al método de extensión Produces. Sin embargo, dado que el asistente TypedResults incluye automáticamente los metadatos para el punto de conexión, puede devolver el tipo de unión Results<T1, Tn> en su lugar, como se muestra en el código siguiente.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Resultados integrados

Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults es preferible a devolver Results. Para más información, consulte TypedResults frente a Results.

En las secciones siguientes se muestra el uso de los asistentes de resultados comunes.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync es una manera alternativa de devolver JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Código de estado personalizado

app.MapGet("/405", () => Results.StatusCode(405));

Texto

app.MapGet("/text", () => Results.Text("This is some text"));

STREAM

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();

Results.Stream las sobrecargas permiten el acceso al flujo de respuesta HTTP subyacente sin almacenamiento en búfer. En el ejemplo siguiente se usa ImageSharp para devolver un tamaño reducido de la imagen especificada:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
    return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});

async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
    var strPath = $"wwwroot/img/{strImage}";
    using var image = await Image.LoadAsync(strPath, token);
    int width = image.Width / 2;
    int height = image.Height / 2;
    image.Mutate(x =>x.Resize(width, height));
    await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}

En el ejemplo siguiente se transmite una imagen desde Azure Blob Storage:

app.MapGet("/stream-image/{containerName}/{blobName}", 
    async (string blobName, string containerName, CancellationToken token) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});

En el ejemplo siguiente se transmite un vídeo desde un blob de Azure:

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Redirect

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Archivo

app.MapGet("/download", () => Results.File("myfile.text"));

Interfaces HttpResult

Las interfaces siguientes del espacio de nombres Microsoft.AspNetCore.Http proporcionan una manera de detectar el tipo IResult en tiempo de ejecución, que es un patrón común en las implementaciones de filtros:

Este es un ejemplo de un filtro que usa una de estas interfaces:

app.MapGet("/weatherforecast", (int days) =>
{
    if (days <= 0)
    {
        return Results.BadRequest();
    }

    var forecast = Enumerable.Range(1, days).Select(index =>
       new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
        .ToArray();
    return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
    var result = await next(context);

    return result switch
    {
        IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
        _ => result
    };
});

Para obtener más información, consulte Filtros en las aplicaciones de API mínimas y Tipos de implementación de IResult.

Personalización de respuestas

Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Además, un tipo IResult personalizado puede proporcionar su propia anotación implementando la interfaz IEndpointMetadataProvider. Por ejemplo, el código siguiente agrega una anotación al tipo HtmlResult anterior que describe la respuesta generada por el punto de conexión.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata es una implementación de IProducesResponseTypeMetadata que define el tipo de contenido de respuesta generada text/html y el código de estado 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Un enfoque alternativo consiste en usar Microsoft.AspNetCore.Mvc.ProducesAttribute para describir la respuesta generada. El código siguiente cambia el método PopulateMetadata para usar ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Configuración de las opciones de serialización de JSON

De manera predeterminada, las aplicaciones de API mínimas usan las opciones Web defaults durante la serialización y deserialización JSON.

Configuración global de las opciones de serialización de JSON

Las opciones se pueden configurar globalmente para una aplicación mediante la invocación de ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Dado que se incluyen campos, el código anterior lee NameField y lo incluye en el JSON de salida.

Configuración de las opciones de serialización de JSON para un punto de conexión

Para configurar las opciones de serialización para un punto de conexión, invoque Results.Json y páselo a un objeto JsonSerializerOptions, como se muestra en el ejemplo siguiente:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };

app.MapGet("/", () => 
    Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Como alternativa, use una sobrecarga de WriteAsJsonAsync que acepte un objeto JsonSerializerOptions. En el ejemplo siguiente se usa esta sobrecarga para dar formato al JSON de salida:

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
    WriteIndented = true };

app.MapGet("/", (HttpContext context) =>
    context.Response.WriteAsJsonAsync<Todo>(
        new Todo { Name = "Walk dog", IsComplete = false }, options));

app.Run();

class Todo
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
//   "name":"Walk dog",
//   "isComplete":false
// }

Recursos adicionales