Como criar respostas em aplicativos de APIs Mínimas
Observação
Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 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 informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.
Os ponto de extremidade mínimos dão suporte aos seguintes tipos de valores retornados:
string
- Isso incluiTask<string>
eValueTask<string>
.T
(Qualquer outro tipo) – Isso incluiTask<T>
eValueTask<T>
.- Baseado em
IResult
- Isso incluiTask<IResult>
eValueTask<IResult>
.
Valores retornados de 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 | Content-Type |
---|---|
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"}
IResult
retorna valores
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:
- Os auxiliares de
TypedResults
retornam objetos fortemente tipado, o que pode melhorar a legibilidade do código, o teste de unidade e reduzir a chance de erros de runtime. - O tipo de implementação fornece automaticamente os metadados de tipo de resposta para OpenAPI para descrever o ponto de extremidade.
Considere o ponto de extremidade a seguir, para o qual um código de status 200 OK
com a resposta JSON esperada é produzida.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar esse ponto de extremidade 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 automaticamente os metadados para o ponto de extremidade.
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 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);
});
}
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 objetosIResult
.
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ões Produces
é chamado. No entanto, como o auxiliar de TypedResults
inclui automaticamente os metadados para o ponto de extremidade, você pode retornar o tipo de união Results<T1, Tn>
ao invés 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 no Results e nas classes estáticas TypedResults. É preferível retornar TypedResults
a retornar Results
. Para obter mais informações, consulte TypedResults vs Resultados.
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.
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();
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 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 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:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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 anterior HtmlResult
que descreve a resposta produzida pelo ponto de extremidade.
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 aplicativo invocando 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 ponto de extremidade mínimos dão suporte aos seguintes tipos de valores retornados:
string
- Isso incluiTask<string>
eValueTask<string>
.T
(Qualquer outro tipo) – Isso incluiTask<T>
eValueTask<T>
.- Baseado em
IResult
- Isso incluiTask<IResult>
eValueTask<IResult>
.
Valores retornados de 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 | Content-Type |
---|---|
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"}
IResult
retorna valores
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:
- Os auxiliares de
TypedResults
retornam objetos fortemente tipado, o que pode melhorar a legibilidade do código, o teste de unidade e reduzir a chance de erros de runtime. - O tipo de implementação fornece automaticamente os metadados de tipo de resposta para OpenAPI para descrever o ponto de extremidade.
Considere o ponto de extremidade a seguir, para o qual um código de status 200 OK
com a resposta JSON esperada é produzida.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar esse ponto de extremidade 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 automaticamente os metadados para o ponto de extremidade.
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 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);
});
}
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 objetosIResult
.
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ões Produces
é chamado. No entanto, como o auxiliar de TypedResults
inclui automaticamente os metadados para o ponto de extremidade, você pode retornar o tipo de união Results<T1, Tn>
ao invés 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 no Results e nas classes estáticas TypedResults. É preferível retornar TypedResults
a retornar Results
. Para obter mais informações, consulte TypedResults vs Resultados.
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"));
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();
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 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 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:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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 anterior HtmlResult
que descreve a resposta produzida pelo ponto de extremidade.
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 aplicativo invocando 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
// }