Compartilhar via


Como criar respostas em aplicativos de APIs Mínimas

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Os pontos de extremidade mínimos oferecem suporte aos seguintes tipos de valores de retorno:

  1. string - Isso inclui Task<string> e ValueTask<string>.
  2. T (Qualquer outro tipo) – Isso inclui Task<T> e ValueTask<T>.
  3. Baseado em IResult - Isso inclui Task<IResult> e ValueTask<IResult>.

Valores de retorno string

Comportamento Tipo de conteúdo
A estrutura grava a cadeia de caracteres diretamente na resposta. text/plain

Considere o manipulador de rotas a seguir, que retorna um texto Hello world.

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

O código status 200 é retornado com o cabeçalho Content-Type text/plain e o conteúdo a seguir.

Hello World

T (Qualquer outro tipo) retorna valores

Comportamento Tipo de conteúdo
A estrutura JSON serializa a resposta. application/json

Considere o manipulador de rotas a seguir, que retorna um tipo anônimo que contém uma propriedade de cadeia de caracteres Message.

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

O código status 200 é retornado com o cabeçalho Content-Type application/json e o conteúdo a seguir.

{"message":"Hello World"}

Valores de retorno IResult

Comportamento Tipo de conteúdo
A estrutura chama IResult.ExecuteAsync. Decidido pela implementação de IResult.

A interface IResult define um contrato que representa o resultado de um ponto de extremidade HTTP. A classe estática Resultados o TypedResults estático são usados para criar vários objetos IResult que representam diferentes tipos de respostas.

TypedResults x Results

As classes estáticas Results e TypedResults fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é o equivalente tipado da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar de TypedResults é um dos tipos de implementação de IResult. A diferença significa que, para auxiliares Results, uma conversão é necessária quando o tipo concreto é necessário, por exemplo, para teste de unidade. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.

Retornar TypedResults em vez de Results tem as seguintes vantagens:

Considere o seguinte endpoint, para o qual o código de status 200 OK com a resposta JSON esperada é produzido.

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

Para documentar este endpoint corretamente, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, conforme mostrado no código a seguir. TypedResults fornece os metadados do ponto de extremidade automaticamente.

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

Para obter mais informações sobre como descrever um tipo de resposta, consulte Suporte a OpenAPI em APIs mínimas.

Conforme mencionado anteriormente, ao usar TypedResults, uma conversão não é necessária. Considere a API mínima a seguir que retorna uma classe TypedResults

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

O teste a seguir verifica o tipo completo de concreto:

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

Como todos os métodos em Results retornaam IResult em sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado de solicitação ao retornar resultados diferentes de um único ponto de extremidade. TypedResults requer o uso de Results<T1, TN> desses delegados.

O método a seguir é compilado porque Results.Ok e Results.NotFound são declarados como retornando IResult, mesmo que os tipos concretos reais dos objetos retornados sejam diferentes:

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

O método a seguir não é compilado, pois TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo de correspondência:

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, o tipo de retorno deve ser totalmente declarado; quando o método é assíncrono, a declaração requer encapsular o tipo de retorno em um Task<>. O uso de TypedResults é mais detalhado, mas essa é a compensação por ter as informações de tipo estaticamente disponíveis e, portanto, capazes de auto-descrever para o 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());

Resulta em <TResult1, TResultN>

Use Results<TResult1, TResultN> como o tipo de retorno do manipulador de ponto de extremidade em vez de IResult quando:

  • Vários tipos de implementação IResult são retornados do manipulador de ponto de extremidade.
  • A classe estática TypedResult é usada para criar os objetos IResult.

Essa alternativa é melhor do que retornar IResult porque os tipos de união genéricos retêm automaticamente os metadados do ponto de extremidade. E como os tipos de união Results<TResult1, TResultN> implementam operadores de conversão implícita, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos em uma instância do tipo de união.

Essa funcionalidade tem o benefício adicional de verificar durante o tempo de compilação se um manipulador de rotas retorna apenas os resultados que ele declara retornar. Tentar retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.

Considere o ponto de extremidade a seguir, para o qual um código de status 400 BadRequest é retornado quando orderId é maior que 999. Caso contrário, ele produzirá um 200 OK com o conteúdo esperado.

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

Para documentar esse ponto de extremidade corretamente, o método de extensão Produces é chamado. No entanto, como o auxiliar de TypedResults inclui automaticamente os metadados para o endpoint, você pode retornar o tipo de união Results<T1, Tn> em vez disso, conforme mostrado no código a seguir.

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

Resultados internos

Auxiliares de resultados comuns existem nas classes estáticas Results e TypedResults. É preferível retornar TypedResults a retornar Results. Para obter mais informações, consulte TypedResults vs Results.

As seções a seguir demonstram o uso dos auxiliares de resultados comuns.

JSON

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

WriteAsJsonAsync é uma maneira alternativa de retornar JSON:

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

Código de status personalizado

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

Erro interno do servidor

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

O exemplo anterior retorna um código de status 500.

Problema e ValidationProblem

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Texto

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

Fluxo

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

as sobrecargas de Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem buffer. O exemplo a seguir usa ImageSharp para retornar um tamanho reduzido da imagem 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);
}

O exemplo a seguir transmite uma imagem a partir do Armazenamento de Blobs do Azure:

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

O exemplo a seguir transmite um vídeo de um Blob do 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);
});

Eventos enviados pelo servidor (SSE)

A API TypedResults.ServerSentEvents dá suporte ao retorno de um resultado serverSentEvents .

Server-Sent Eventos é uma tecnologia de push de servidor que permite que um servidor envie um fluxo de mensagens de evento para um cliente por meio de uma única conexão HTTP. No .NET, as mensagens de evento são representadas como SseItem<T> objetos, que podem conter um tipo de evento, uma ID e um conteúdo de dados do tipo T.

A classe TypedResults tem um método estático chamado ServerSentEvents que pode ser usado para retornar um ServerSentEvents resultado. O primeiro parâmetro para esse método é um IAsyncEnumerable<SseItem<T>> que representa o fluxo de mensagens de evento a serem enviadas ao cliente.

O exemplo a seguir ilustra como usar a TypedResults.ServerSentEvents API para retornar um fluxo de eventos de freqüência cardíaca como objetos JSON para o cliente:

app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});

Para obter mais informações, consulte o aplicativo de exemplo de API mínima usando a TypedResults.ServerSentEvents API para retornar um fluxo de eventos de freqüência cardíaca como cadeia de caracteres ServerSentEventse objetos JSON para o cliente.

Redirecionar

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

Arquivo

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

Interfaces de HttpResult

As interfaces abaixo no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detectar o tipo IResult em runtime, o que é um padrão comum em implementações de filtro:

Aqui está um exemplo de um filtro que usa uma destas 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 obter mais informações, consulte Filtros em aplicativos de APIs mínimas e tipos de implementação IResult.

Modificando cabeçalhos

Use o objeto HttpResponse para modificar cabeçalhos de resposta:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

Personalizando respostas

Aplicativos podem controlar as respostas implementando um tipo IResult personalizado. O código a seguir é um exemplo de um 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);
    }
}

Recomenda-se adicionar um método de extensão a Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detectáveis.

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

Além disso, um tipo personalizado IResult pode fornecer sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código a seguir adiciona uma anotação ao tipo precedente HtmlResult que descreve a resposta produzida pelo endpoint.

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

O ProducesHtmlMetadata é uma implementação de IProducesResponseTypeMetadata que define o tipo de conteúdo de resposta produzido text/html e o código de status 200 OK.

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

    public int StatusCode => 200;

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

Uma abordagem alternativa é usar o Microsoft.AspNetCore.Mvc.ProducesAttribute para descrever a resposta produzida. O código a seguir altera o método PopulateMetadata para usar ProducesAttribute.

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

Configurar opções de serialização JSON

Por padrão, aplicativos de APIs mínimas usam opções Web defaults durante a serialização e desserialização JSON.

Configurar opções de serialização JSON globalmente

As opções de um aplicativo podem ser configuradas globalmente ao invocar ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formata a saída 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
// }

Como os campos são incluídos, o código anterior lê NameField e o inclui na saída JSON.

Configurar as opções de desserialização JSON de um ponto de extremidade

Para configurar as opções de serialização de um ponto de extremidade, invoque Results.Json e passe para ele um objeto JsonSerializerOptions, conforme mostrado no exemplo a seguir:

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 uma sobrecarga de WriteAsJsonAsync que aceite um objeto JsonSerializerOptions. O exemplo a seguir usa essa sobrecarga para formatar a saída JSON:

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 adicionais

Os pontos de extremidade mínimos oferecem suporte aos seguintes tipos de valores de retorno:

  1. string - Isso inclui Task<string> e ValueTask<string>.
  2. T (Qualquer outro tipo) – Isso inclui Task<T> e ValueTask<T>.
  3. Baseado em IResult - Isso inclui Task<IResult> e ValueTask<IResult>.

Valores de retorno string

Comportamento Tipo de conteúdo
A estrutura grava a cadeia de caracteres diretamente na resposta. text/plain

Considere o manipulador de rotas a seguir, que retorna um texto Hello world.

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

O código status 200 é retornado com o cabeçalho Content-Type text/plain e o conteúdo a seguir.

Hello World

T (Qualquer outro tipo) retorna valores

Comportamento Tipo de conteúdo
A estrutura JSON serializa a resposta. application/json

Considere o manipulador de rotas a seguir, que retorna um tipo anônimo que contém uma propriedade de cadeia de caracteres Message.

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

O código status 200 é retornado com o cabeçalho Content-Type application/json e o conteúdo a seguir.

{"message":"Hello World"}

Valores de retorno IResult

Comportamento Tipo de conteúdo
A estrutura chama IResult.ExecuteAsync. Decidido pela implementação de IResult.

A interface IResult define um contrato que representa o resultado de um ponto de extremidade HTTP. A classe estática Resultados o TypedResults estático são usados para criar vários objetos IResult que representam diferentes tipos de respostas.

TypedResults x Results

As classes estáticas Results e TypedResults fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é o equivalente tipado da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar de TypedResults é um dos tipos de implementação de IResult. A diferença significa que, para auxiliares Results, uma conversão é necessária quando o tipo concreto é necessário, por exemplo, para teste de unidade. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.

Retornar TypedResults em vez de Results tem as seguintes vantagens:

Considere o seguinte endpoint, para o qual o código de status 200 OK com a resposta JSON esperada é produzido.

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

Para documentar este endpoint corretamente, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, conforme mostrado no código a seguir. TypedResults fornece os metadados do ponto de extremidade automaticamente.

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

Para obter mais informações sobre como descrever um tipo de resposta, consulte Suporte a OpenAPI em APIs mínimas.

Conforme mencionado anteriormente, ao usar TypedResults, uma conversão não é necessária. Considere a API mínima a seguir que retorna uma classe TypedResults

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

O teste a seguir verifica o tipo completo de concreto:

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

Como todos os métodos em Results retornaam IResult em sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado de solicitação ao retornar resultados diferentes de um único ponto de extremidade. TypedResults requer o uso de Results<T1, TN> desses delegados.

O método a seguir é compilado porque Results.Ok e Results.NotFound são declarados como retornando IResult, mesmo que os tipos concretos reais dos objetos retornados sejam diferentes:

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

O método a seguir não é compilado, pois TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo de correspondência:

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, o tipo de retorno deve ser totalmente declarado, o que quando assíncrono requer o encapsulador Task<>. O uso de TypedResults é mais detalhado, mas essa é a compensação por ter as informações de tipo estaticamente disponíveis e, portanto, capazes de auto-descrever para o 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());

Resulta em <TResult1, TResultN>

Use Results<TResult1, TResultN> como o tipo de retorno do manipulador de ponto de extremidade em vez de IResult quando:

  • Vários tipos de implementação IResult são retornados do manipulador de ponto de extremidade.
  • A classe estática TypedResult é usada para criar os objetos IResult.

Essa alternativa é melhor do que retornar IResult porque os tipos de união genéricos retêm automaticamente os metadados do ponto de extremidade. E como os tipos de união Results<TResult1, TResultN> implementam operadores de conversão implícita, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos em uma instância do tipo de união.

Essa funcionalidade tem o benefício adicional de verificar durante o tempo de compilação se um manipulador de rotas retorna apenas os resultados que ele declara retornar. Tentar retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.

Considere o ponto de extremidade a seguir, para o qual um código de status 400 BadRequest é retornado quando orderId é maior que 999. Caso contrário, ele produzirá um 200 OK com o conteúdo esperado.

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

Para documentar esse ponto de extremidade corretamente, o método de extensão Produces é chamado. No entanto, como o auxiliar de TypedResults inclui automaticamente os metadados para o endpoint, você pode retornar o tipo de união Results<T1, Tn> em vez disso, conforme mostrado no código a seguir.

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

Resultados internos

Auxiliares de resultados comuns existem nas classes estáticas Results e TypedResults. É preferível retornar TypedResults a retornar Results. Para obter mais informações, consulte TypedResults vs Results.

As seções a seguir demonstram o uso dos auxiliares de resultados comuns.

JSON

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

WriteAsJsonAsync é uma maneira alternativa de retornar JSON:

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

Código de status personalizado

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

Erro interno do servidor

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

O exemplo anterior retorna um código de status 500.

Problema e ValidationProblem

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Texto

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

Fluxo

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

as sobrecargas de Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem buffer. O exemplo a seguir usa ImageSharp para retornar um tamanho reduzido da imagem 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);
}

O exemplo a seguir transmite uma imagem a partir do Armazenamento de Blobs do Azure:

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

O exemplo a seguir transmite um vídeo de um Blob do 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);
});

Redirecionar

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

Arquivo

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

Interfaces de HttpResult

As interfaces abaixo no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detectar o tipo IResult em runtime, o que é um padrão comum em implementações de filtro:

Aqui está um exemplo de um filtro que usa uma destas 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 obter mais informações, consulte Filtros em aplicativos de APIs mínimas e tipos de implementação IResult.

Modificando cabeçalhos

Use o objeto HttpResponse para modificar cabeçalhos de resposta:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

Personalizando respostas

Aplicativos podem controlar as respostas implementando um tipo IResult personalizado. O código a seguir é um exemplo de um 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);
    }
}

Recomenda-se adicionar um método de extensão a Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detectáveis.

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

Além disso, um tipo personalizado IResult pode fornecer sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código a seguir adiciona uma anotação ao tipo precedente HtmlResult que descreve a resposta produzida pelo endpoint.

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

O ProducesHtmlMetadata é uma implementação de IProducesResponseTypeMetadata que define o tipo de conteúdo de resposta produzido text/html e o código de status 200 OK.

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

    public int StatusCode => 200;

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

Uma abordagem alternativa é usar o Microsoft.AspNetCore.Mvc.ProducesAttribute para descrever a resposta produzida. O código a seguir altera o método PopulateMetadata para usar ProducesAttribute.

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

Configurar opções de serialização JSON

Por padrão, aplicativos de APIs mínimas usam opções Web defaults durante a serialização e desserialização JSON.

Configurar opções de serialização JSON globalmente

As opções de um aplicativo podem ser configuradas globalmente ao invocar ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formata a saída 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
// }

Como os campos são incluídos, o código anterior lê NameField e o inclui na saída JSON.

Configurar as opções de desserialização JSON de um ponto de extremidade

Para configurar as opções de serialização de um ponto de extremidade, invoque Results.Json e passe para ele um objeto JsonSerializerOptions, conforme mostrado no exemplo a seguir:

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 uma sobrecarga de WriteAsJsonAsync que aceite um objeto JsonSerializerOptions. O exemplo a seguir usa essa sobrecarga para formatar a saída JSON:

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 adicionais

Os pontos de extremidade mínimos oferecem suporte aos seguintes tipos de valores de retorno:

  1. string - Isso inclui Task<string> e ValueTask<string>.
  2. T (Qualquer outro tipo) – Isso inclui Task<T> e ValueTask<T>.
  3. Baseado em IResult - Isso inclui Task<IResult> e ValueTask<IResult>.

Valores de retorno string

Comportamento Tipo de conteúdo
A estrutura grava a cadeia de caracteres diretamente na resposta. text/plain

Considere o manipulador de rotas a seguir, que retorna um texto Hello world.

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

O código status 200 é retornado com o cabeçalho Content-Type text/plain e o conteúdo a seguir.

Hello World

T (Qualquer outro tipo) retorna valores

Comportamento Tipo de conteúdo
A estrutura JSON serializa a resposta. application/json

Considere o manipulador de rotas a seguir, que retorna um tipo anônimo que contém uma propriedade de cadeia de caracteres Message.

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

O código status 200 é retornado com o cabeçalho Content-Type application/json e o conteúdo a seguir.

{"message":"Hello World"}

Valores de retorno IResult

Comportamento Tipo de conteúdo
A estrutura chama IResult.ExecuteAsync. Decidido pela implementação de IResult.

A interface IResult define um contrato que representa o resultado de um ponto de extremidade HTTP. A classe estática Resultados o TypedResults estático são usados para criar vários objetos IResult que representam diferentes tipos de respostas.

TypedResults x Results

As classes estáticas Results e TypedResults fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é o equivalente tipado da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar de TypedResults é um dos tipos de implementação de IResult. A diferença significa que, para auxiliares Results, uma conversão é necessária quando o tipo concreto é necessário, por exemplo, para teste de unidade. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.

Retornar TypedResults em vez de Results tem as seguintes vantagens:

Considere o seguinte endpoint, para o qual o código de status 200 OK com a resposta JSON esperada é produzido.

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

Para documentar este endpoint corretamente, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, conforme mostrado no código a seguir. TypedResults fornece os metadados do ponto de extremidade automaticamente.

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

Para obter mais informações sobre como descrever um tipo de resposta, consulte Suporte a OpenAPI em APIs mínimas.

Conforme mencionado anteriormente, ao usar TypedResults, uma conversão não é necessária. Considere a API mínima a seguir que retorna uma classe TypedResults

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

O teste a seguir verifica o tipo completo de concreto:

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

Como todos os métodos em Results retornaam IResult em sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado de solicitação ao retornar resultados diferentes de um único ponto de extremidade. TypedResults requer o uso de Results<T1, TN> desses delegados.

O método a seguir é compilado porque Results.Ok e Results.NotFound são declarados como retornando IResult, mesmo que os tipos concretos reais dos objetos retornados sejam diferentes:

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

O método a seguir não é compilado, pois TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo de correspondência:

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, o tipo de retorno deve ser totalmente declarado, o que quando assíncrono requer o encapsulador Task<>. O uso de TypedResults é mais detalhado, mas essa é a compensação por ter as informações de tipo estaticamente disponíveis e, portanto, capazes de auto-descrever para o 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());

Resulta em <TResult1, TResultN>

Use Results<TResult1, TResultN> como o tipo de retorno do manipulador de ponto de extremidade em vez de IResult quando:

  • Vários tipos de implementação IResult são retornados do manipulador de ponto de extremidade.
  • A classe estática TypedResult é usada para criar os objetos IResult.

Essa alternativa é melhor do que retornar IResult porque os tipos de união genéricos retêm automaticamente os metadados do ponto de extremidade. E como os tipos de união Results<TResult1, TResultN> implementam operadores de conversão implícita, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos em uma instância do tipo de união.

Essa funcionalidade tem o benefício adicional de verificar durante o tempo de compilação se um manipulador de rotas retorna apenas os resultados que ele declara retornar. Tentar retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.

Considere o ponto de extremidade a seguir, para o qual um código de status 400 BadRequest é retornado quando orderId é maior que 999. Caso contrário, ele produzirá um 200 OK com o conteúdo esperado.

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

Para documentar esse ponto de extremidade corretamente, o método de extensão Produces é chamado. No entanto, como o auxiliar de TypedResults inclui automaticamente os metadados para o endpoint, você pode retornar o tipo de união Results<T1, Tn> em vez disso, conforme mostrado no código a seguir.

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

Resultados internos

Auxiliares de resultados comuns existem nas classes estáticas Results e TypedResults. É preferível retornar TypedResults a retornar Results. Para obter mais informações, consulte TypedResults vs Results.

As seções a seguir demonstram o uso dos auxiliares de resultados comuns.

JSON

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

WriteAsJsonAsync é uma maneira alternativa de retornar JSON:

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

Código de status personalizado

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

Texto

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

Fluxo

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

as sobrecargas de Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem buffer. O exemplo a seguir usa ImageSharp para retornar um tamanho reduzido da imagem 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);
}

O exemplo a seguir transmite uma imagem a partir do Armazenamento de Blobs do Azure:

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

O exemplo a seguir transmite um vídeo de um Blob do 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);
});

Redirecionar

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

Arquivo

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

Interfaces de HttpResult

As interfaces abaixo no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detectar o tipo IResult em runtime, o que é um padrão comum em implementações de filtro:

Aqui está um exemplo de um filtro que usa uma destas 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 obter mais informações, consulte Filtros em aplicativos de APIs mínimas e tipos de implementação IResult.

Personalizando respostas

Aplicativos podem controlar as respostas implementando um tipo IResult personalizado. O código a seguir é um exemplo de um 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);
    }
}

Recomenda-se adicionar um método de extensão a Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detectáveis.

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

Além disso, um tipo personalizado IResult pode fornecer sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código a seguir adiciona uma anotação ao tipo precedente HtmlResult que descreve a resposta produzida pelo endpoint.

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

O ProducesHtmlMetadata é uma implementação de IProducesResponseTypeMetadata que define o tipo de conteúdo de resposta produzido text/html e o código de status 200 OK.

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

    public int StatusCode => 200;

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

Uma abordagem alternativa é usar o Microsoft.AspNetCore.Mvc.ProducesAttribute para descrever a resposta produzida. O código a seguir altera o método PopulateMetadata para usar ProducesAttribute.

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

Configurar opções de serialização JSON

Por padrão, aplicativos de APIs mínimas usam opções Web defaults durante a serialização e desserialização JSON.

Configurar opções de serialização JSON globalmente

As opções de um aplicativo podem ser configuradas globalmente ao invocar ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formata a saída 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
// }

Como os campos são incluídos, o código anterior lê NameField e o inclui na saída JSON.

Configurar as opções de desserialização JSON de um ponto de extremidade

Para configurar as opções de serialização de um ponto de extremidade, invoque Results.Json e passe para ele um objeto JsonSerializerOptions, conforme mostrado no exemplo a seguir:

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 uma sobrecarga de WriteAsJsonAsync que aceite um objeto JsonSerializerOptions. O exemplo a seguir usa essa sobrecarga para formatar a saída JSON:

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 adicionais