Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Observação
Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.
Advertência
Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.
Este artigo explica como criar respostas para pontos de extremidade de API mínimos no ASP.NET Core. As APIs mínimas fornecem várias maneiras de retornar dados e códigos de status HTTP.
Os pontos de extremidade mínimos suportam os seguintes tipos de valores de retorno:
-
string- IncluiTask<string>eValueTask<string>. -
T(Qualquer outro tipo) - IncluiTask<T>eValueTask<T>. - Baseado em
IResult- Isto incluiTask<IResult>eValueTask<IResult>.
Importante
A partir do ASP.NET Core 10, os pontos de extremidade de API conhecidos não redirecionam mais para páginas de login ao usar cookie a autenticação. Em vez disso, eles retornam códigos de status 401/403. Para obter detalhes, consulte Comportamento de autenticação de ponto de extremidade da API no ASP.NET Core.
string valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura grava a cadeia de caracteres diretamente na resposta. | text/plain |
Considere o seguinte manipulador de rota, que retorna um texto Hello world.
app.MapGet("/hello", () => "Hello World");
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type text/plain e do conteúdo a seguir.
Hello World
T Valores de retorno (Qualquer outro tipo)
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura JSON serializa a resposta. | application/json |
Considere o seguinte manipulador de rota, que retorna um tipo anônimo contendo uma propriedade de string Message.
app.MapGet("/hello", () => new { Message = "Hello World" });
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type application/json e do conteúdo a seguir.
{"message":"Hello World"}
IResult valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura chama IResult.ExecuteAsync. | Decidido pela implementação do IResult. |
A interface IResult define um contrato que representa o resultado de um endpoint HTTP. A classe estática Results e a classe estática TypedResults são usadas para criar vários objetos IResult que representam diferentes tipos de respostas.
TypedResults vs Resultados
As classes Results e TypedResults estáticas fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é a equivalente de tipo da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar TypedResults é um dos tipos de implementação IResult. A diferença significa que, para Results auxiliares, é necessária uma conversão quando o tipo concreto é necessário, por exemplo, para testes unitários. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.
Devolver TypedResults em vez de Results tem as seguintes vantagens:
-
TypedResultsauxiliares retornam objetos fortemente tipados, o que pode ajudar a melhorar a legibilidade do código, os testes de unidade e reduzir a chance de erros em tempo de execução. - Os metadados do tipo de resposta para OpenAPI são automaticamente fornecidos pelo tipo de implementação para descrever o ponto de extremidade.
Considere o seguinte endpoint, para o qual um código de estado 200 OK com a resposta JSON esperada é gerado.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar corretamente este endpoint, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, como mostrado no código a seguir.
TypedResults fornece automaticamente os metadados para o endpoint.
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.
Para obter exemplos sobre tipos de resultados de testes, consulte a documentação de teste.
Como todos os métodos em Results retornam IResult na sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado da solicitação ao retornar resultados diferentes de um único endpoint.
TypedResults requere a utilização de Results<T1, TN> por parte 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 compila, porque TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo correspondente:
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 envolver o tipo de retorno em um Task<>. Usar TypedResults é mais detalhado, mas essa é a contrapartida para que as informações de tipo estejam disponíveis estaticamente e, portanto, capazes de se autodescrever para 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());
Resultados obtidos<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 de
IResultsão devolvidos do endpoint handler. - A classe
TypedResultestática é 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 cast implícitos, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos numa instância do tipo de união.
Isso tem o benefício adicional de fornecer verificação em tempo de compilação de que um manipulador de rotas realmente retorna apenas os resultados que ele declara que faz. A tentativa de retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.
Considere o seguinte endpoint, para o qual se retorna um código de status 400 BadRequest quando o orderId é maior que 999. Caso contrário, produz 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, pode-se retornar o tipo de união Results<T1, Tn>, 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 integrados
Existem auxiliares de resultados comuns nas classes Results e TypedResults estáticas. Devolver TypedResults é preferível a devolver 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 Validação do Problema
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
Personalizar respostas de erro de validação usando IProblemDetailsService
Personalize as respostas de erro a partir da lógica mínima de validação da API com uma IProblemDetailsService implementação. Registre esse serviço na coleção de serviços do seu aplicativo para permitir respostas de erro mais consistentes e específicas do usuário. O suporte para validação mínima de API foi introduzido no ASP.NET Core no .NET 10.
Para implementar respostas de erro de validação personalizadas:
- Implementar IProblemDetailsService ou usar a implementação padrão
- Registrar o serviço no contêiner DI
- O sistema de validação utiliza automaticamente o serviço registado para formatar respostas de erro de validação
O exemplo a seguir mostra como registrar e configurar o para personalizar respostas de erro de IProblemDetailsService validação:
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
if (context.ProblemDetails.Status == 400)
{
context.ProblemDetails.Title = "Validation error occurred";
context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
}
};
});
Quando ocorre um erro de validação, o IProblemDetailsService será usado para gerar a resposta de erro, incluindo quaisquer personalizações adicionadas no retorno de CustomizeProblemDetails chamada.
Para obter um exemplo completo de aplicativo, consulte o aplicativo de exemplo de API mínima demonstrando como personalizar respostas de erro de validação usando as IProblemDetailsService APIs mínimas principais do ASP.NET.
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 funções de sobrecarga Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem armazenamento temporário. 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 de de armazenamento de Blob 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);
});
Server-Sent Eventos (SSE)
A API TypedResults.ServerSentEvents oferece suporte ao retorno de um resultado ServerSentEvents .
Server-Sent Events é uma tecnologia de push de servidor que permite que um servidor envie um fluxo de mensagens de eventos para um cliente através 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 uma carga útil 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 para o cliente.
O exemplo a seguir ilustra como usar a TypedResults.ServerSentEvents API para retornar um fluxo de eventos de frequê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 frequência cardíaca como string, ServerSentEventse objetos JSON para o cliente.
Redirecionamento
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Ficheiro
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces de HttpResult
As interfaces a seguir no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detetar o tipo de IResult em tempo de execuçã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 aplicações de API mínima e tipos de implementações 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";
});
Personalização de respostas
Os aplicativos podem controlar as respostas implementando um tipo de 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);
}
}
Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetá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 de IResult personalizado pode fornecer a sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código seguinte adiciona uma anotação ao tipo HtmlResult anterior 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, as aplicações mínimas de API utilizam as opções Web defaults durante a serialização e desserialização de JSON.
Configurar opções de serialização JSON globalmente
As opções podem ser configuradas globalmente para um aplicativo invocando ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formatos de 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 estão incluídos, o código anterior lê NameField e o inclui no JSON de saída.
Configurar opções de serialização JSON para um endpoint
Para configurar opções de serialização para 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 o JSON de saída:
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 suportam os seguintes tipos de valores de retorno:
-
string- IncluiTask<string>eValueTask<string>. -
T(Qualquer outro tipo) - IncluiTask<T>eValueTask<T>. - Baseado em
IResult- Isto incluiTask<IResult>eValueTask<IResult>.
string valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura grava a cadeia de caracteres diretamente na resposta. | text/plain |
Considere o seguinte manipulador de rota, que retorna um texto Hello world.
app.MapGet("/hello", () => "Hello World");
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type text/plain e do conteúdo a seguir.
Hello World
T Valores de retorno (Qualquer outro tipo)
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura JSON serializa a resposta. | application/json |
Considere o seguinte manipulador de rota, que retorna um tipo anônimo contendo uma propriedade de string Message.
app.MapGet("/hello", () => new { Message = "Hello World" });
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type application/json e do conteúdo a seguir.
{"message":"Hello World"}
IResult valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura chama IResult.ExecuteAsync. | Decidido pela implementação do IResult. |
A interface IResult define um contrato que representa o resultado de um endpoint HTTP. A classe estática Results e a classe estática TypedResults são usadas para criar vários objetos IResult que representam diferentes tipos de respostas.
TypedResults vs Resultados
As classes Results e TypedResults estáticas fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é a equivalente de tipo da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar TypedResults é um dos tipos de implementação IResult. A diferença significa que, para Results auxiliares, é necessária uma conversão quando o tipo concreto é necessário, por exemplo, para testes unitários. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.
Devolver TypedResults em vez de Results tem as seguintes vantagens:
-
TypedResultsauxiliares retornam objetos fortemente tipados, o que pode ajudar a melhorar a legibilidade do código, os testes de unidade e reduzir a chance de erros em tempo de execução. - Os metadados do tipo de resposta para OpenAPI são automaticamente fornecidos pelo tipo de implementação para descrever o ponto de extremidade.
Considere o seguinte endpoint, para o qual um código de estado 200 OK com a resposta JSON esperada é gerado.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar corretamente este endpoint, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, como mostrado no código a seguir.
TypedResults fornece automaticamente os metadados para o endpoint.
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.
Como mencionado anteriormente, ao usar TypedResults, uma conversão não é necessária. Considere a seguinte API mínima que retorna uma classe TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Os seguintes ensaios verificam o tipo de betão 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 retornam IResult na sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado da solicitação ao retornar resultados diferentes de um único endpoint.
TypedResults requere a utilização de Results<T1, TN> por parte 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 compila, porque TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo correspondente:
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, e quando é assíncrono, requer o envoltório Task<>. Usar TypedResults é mais detalhado, mas essa é a contrapartida para que as informações de tipo estejam disponíveis estaticamente e, portanto, capazes de se autodescrever para 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());
Resultados obtidos<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 de
IResultsão devolvidos do endpoint handler. - A classe
TypedResultestática é 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 cast implícitos, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos numa instância do tipo de união.
Isso tem o benefício adicional de fornecer verificação em tempo de compilação de que um manipulador de rotas realmente retorna apenas os resultados que ele declara que faz. A tentativa de retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.
Considere o seguinte endpoint, para o qual se retorna um código de status 400 BadRequest quando o orderId é maior que 999. Caso contrário, produz 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, pode-se retornar o tipo de união Results<T1, Tn>, 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 integrados
Existem auxiliares de resultados comuns nas classes Results e TypedResults estáticas. Devolver TypedResults é preferível a devolver 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 Validação do Problema
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"));
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 funções de sobrecarga Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem armazenamento temporário. 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 de de armazenamento de Blob 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);
});
Redirecionamento
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Ficheiro
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces de HttpResult
As interfaces a seguir no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detetar o tipo de IResult em tempo de execuçã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 aplicações de API mínima e tipos de implementações 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";
});
Personalização de respostas
Os aplicativos podem controlar as respostas implementando um tipo de 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);
}
}
Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetá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 de IResult personalizado pode fornecer a sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código seguinte adiciona uma anotação ao tipo HtmlResult anterior 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, as aplicações mínimas de API utilizam as opções Web defaults durante a serialização e desserialização de JSON.
Configurar opções de serialização JSON globalmente
As opções podem ser configuradas globalmente para um aplicativo invocando ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formatos de 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 estão incluídos, o código anterior lê NameField e o inclui no JSON de saída.
Configurar opções de serialização JSON para um endpoint
Para configurar opções de serialização para 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 o JSON de saída:
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 suportam os seguintes tipos de valores de retorno:
-
string- IncluiTask<string>eValueTask<string>. -
T(Qualquer outro tipo) - IncluiTask<T>eValueTask<T>. - Baseado em
IResult- Isto incluiTask<IResult>eValueTask<IResult>.
string valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura grava a cadeia de caracteres diretamente na resposta. | text/plain |
Considere o seguinte manipulador de rota, que retorna um texto Hello world.
app.MapGet("/hello", () => "Hello World");
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type text/plain e do conteúdo a seguir.
Hello World
T Valores de retorno (Qualquer outro tipo)
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura JSON serializa a resposta. | application/json |
Considere o seguinte manipulador de rota, que retorna um tipo anônimo contendo uma propriedade de string Message.
app.MapGet("/hello", () => new { Message = "Hello World" });
O código de status 200 é retornado, acompanhado do cabeçalho Content-Type application/json e do conteúdo a seguir.
{"message":"Hello World"}
IResult valores de retorno
| Comportamento | Tipo de conteúdo |
|---|---|
| A estrutura chama IResult.ExecuteAsync. | Decidido pela implementação do IResult. |
A interface IResult define um contrato que representa o resultado de um endpoint HTTP. A classe estática Results e a classe estática TypedResults são usadas para criar vários objetos IResult que representam diferentes tipos de respostas.
TypedResults vs Resultados
As classes Results e TypedResults estáticas fornecem conjuntos semelhantes de auxiliares de resultados. A classe TypedResults é a equivalente de tipo da classe Results. No entanto, o tipo de retorno dos auxiliares Results é IResult, enquanto o tipo de retorno de cada auxiliar TypedResults é um dos tipos de implementação IResult. A diferença significa que, para Results auxiliares, é necessária uma conversão quando o tipo concreto é necessário, por exemplo, para testes unitários. Os tipos de implementação são definidos no namespace Microsoft.AspNetCore.Http.HttpResults.
Devolver TypedResults em vez de Results tem as seguintes vantagens:
-
TypedResultsauxiliares retornam objetos fortemente tipados, o que pode ajudar a melhorar a legibilidade do código, os testes de unidade e reduzir a chance de erros em tempo de execução. - Os metadados do tipo de resposta para OpenAPI são automaticamente fornecidos pelo tipo de implementação para descrever o ponto de extremidade.
Considere o seguinte endpoint, para o qual um código de estado 200 OK com a resposta JSON esperada é gerado.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar corretamente este endpoint, o método de extensões Produces é chamado. No entanto, não é necessário chamar Produces se TypedResults for usado em vez de Results, como mostrado no código a seguir.
TypedResults fornece automaticamente os metadados para o endpoint.
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.
Como mencionado anteriormente, ao usar TypedResults, uma conversão não é necessária. Considere a seguinte API mínima que retorna uma classe TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Os seguintes ensaios verificam o tipo de betão 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 retornam IResult na sua assinatura, o compilador infere automaticamente isso como o tipo de retorno do delegado da solicitação ao retornar resultados diferentes de um único endpoint.
TypedResults requere a utilização de Results<T1, TN> por parte 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 compila, porque TypedResults.Ok e TypedResults.NotFound são declarados como retornando tipos diferentes e o compilador não tentará inferir o melhor tipo correspondente:
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, e quando é assíncrono, requer o envoltório Task<>. Usar TypedResults é mais detalhado, mas essa é a contrapartida para que as informações de tipo estejam disponíveis estaticamente e, portanto, capazes de se autodescrever para 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());
Resultados obtidos<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 de
IResultsão devolvidos do endpoint handler. - A classe
TypedResultestática é 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 cast implícitos, o compilador pode converter automaticamente os tipos especificados nos argumentos genéricos numa instância do tipo de união.
Isso tem o benefício adicional de fornecer verificação em tempo de compilação de que um manipulador de rotas realmente retorna apenas os resultados que ele declara que faz. A tentativa de retornar um tipo que não é declarado como um dos argumentos genéricos para Results<> resulta em um erro de compilação.
Considere o seguinte endpoint, para o qual se retorna um código de status 400 BadRequest quando o orderId é maior que 999. Caso contrário, produz 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, pode-se retornar o tipo de união Results<T1, Tn>, 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 integrados
Existem auxiliares de resultados comuns nas classes Results e TypedResults estáticas. Devolver TypedResults é preferível a devolver 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"));
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 funções de sobrecarga Results.Stream permitem o acesso ao fluxo de resposta HTTP subjacente sem armazenamento temporário. 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 de de armazenamento de Blob 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);
});
Redirecionamento
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Ficheiro
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces de HttpResult
As interfaces a seguir no namespace Microsoft.AspNetCore.Http fornecem uma maneira de detetar o tipo de IResult em tempo de execuçã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 aplicações de API mínima e tipos de implementações IResult.
Personalização de respostas
Os aplicativos podem controlar as respostas implementando um tipo de 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);
}
}
Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetá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 de IResult personalizado pode fornecer a sua própria anotação implementando a interface IEndpointMetadataProvider. Por exemplo, o código seguinte adiciona uma anotação ao tipo HtmlResult anterior 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, as aplicações mínimas de API utilizam as opções Web defaults durante a serialização e desserialização de JSON.
Configurar opções de serialização JSON globalmente
As opções podem ser configuradas globalmente para um aplicativo invocando ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formatos de 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 estão incluídos, o código anterior lê NameField e o inclui no JSON de saída.
Configurar opções de serialização JSON para um endpoint
Para configurar opções de serialização para 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 o JSON de saída:
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
// }