Cómo crear respuestas en aplicaciones de API mínimas
Nota:
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la directiva de compatibilidad de .NET y .NET Core. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión de .NET 9 de este artículo.
Los puntos de conexión mínimos admiten los siguientes tipos de valores devueltos:
string
: incluyeTask<string>
yValueTask<string>
.T
(cualquier otro tipo): incluyeTask<T>
yValueTask<T>
.- Basado en
IResult
: incluyeTask<IResult>
yValueTask<IResult>
.
Valores devueltos string
Comportamiento | Content-Type |
---|---|
El marco escribe la cadena directamente en la respuesta. | text/plain |
Tenga en cuenta el siguiente controlador de ruta, que devuelve un texto Hello world
.
app.MapGet("/hello", () => "Hello World");
El código de estado 200
se devuelve con el encabezado Content-Type text/plain
y el siguiente contenido.
Hello World
Valores devueltos T
(cualquier otro tipo)
Comportamiento | Content-Type |
---|---|
El marco JSON serializa la respuesta. | application/json |
Tenga en cuenta el siguiente controlador de ruta, que devuelve un tipo anónimo que contiene una propiedad de cadena Message
.
app.MapGet("/hello", () => new { Message = "Hello World" });
El código de estado 200
se devuelve con el encabezado Content-Type application/json
y el siguiente contenido.
{"message":"Hello World"}
Valores devueltos IResult
Comportamiento | Content-Type |
---|---|
El marco llama a IResult.ExecuteAsync. | Decidido por la implementación de IResult . |
La interfaz IResult
define un contrato que representa el resultado de un punto de conexión HTTP. Las clases estáticas Results y TypedResults se usan para crear distintos objetos IResult
que representan diferentes tipos de respuestas.
TypedResults frente a Results
Las clases estáticas Results y TypedResults proporcionan conjuntos de asistentes de resultados similares. La clase TypedResults
es el equivalente con tipo de la clase Results
. Sin embargo, el tipo de valor devuelto de los asistentes Results
es IResult, mientras que cada tipo de valor devuelto del asistente TypedResults
es uno de los tipos de implementación IResult
. La diferencia significa que para los asistentes Results
hace falta una conversión cuando el tipo concreto es necesario, por ejemplo, para las pruebas unitarias. Los tipos de implementación se definen en el espacio de nombres Microsoft.AspNetCore.Http.HttpResults.
Devolver TypedResults
en lugar de Results
tiene las siguientes ventajas:
- Los asistentes
TypedResults
devuelven objetos fuertemente tipados, que pueden mejorar la legibilidad del código, las pruebas unitarias y reducir la posibilidad de errores en tiempo de ejecución. - El tipo de implementación proporciona automáticamente los metadatos del tipo de respuesta para que OpenAPI describa el punto de conexión.
Ten en cuenta el punto de conexión siguiente, para el que se genera un código de estado 200 OK
con la respuesta JSON esperada.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar este punto de conexión correctamente, se llama al método de extensiones Produces
. Sin embargo, no es necesario llamar a Produces
si se usa TypedResults
en lugar de Results
, como se muestra en el código siguiente. TypedResults
proporciona automáticamente los metadatos para el punto de conexión.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Para obtener más información sobre cómo describir un tipo de respuesta, consulte Compatibilidad con OpenAPI en API mínimas.
Como se mencionó anteriormente, cuando se usa TypedResults
, no se necesita una conversión. Tenga en cuenta la siguiente API mínima que devuelve una clase TypedResults
.
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
La prueba siguiente comprueba el tipo concreto completo:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Dado que todos los métodos de Results
devuelven IResult
en su firma, el compilador deduce automáticamente que como el tipo de valor devuelto del delegado de solicitud al devolver resultados diferentes de un único punto de conexión. TypedResults
requiere el uso de Results<T1, TN>
de dichos delegados.
El método siguiente se compila porque Results.Ok
y Results.NotFound
se declaran como IResult
devueltos, aunque los tipos concretos reales de los objetos devueltos son diferentes:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
El método siguiente no se compila, ya que TypedResults.Ok
y TypedResults.NotFound
se declaran como devolver tipos diferentes y el compilador no intentará deducir el mejor tipo de coincidencia:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Para usar TypedResults
, el tipo de valor devuelto debe declararse completamente, que cuando es asincrónico requiere el contenedor Task<>
. El uso de TypedResults
es más detallado, pero es el inconveniente de tener la información de tipo disponible estáticamente y, por lo tanto, capaz de describirse automáticamente en OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
Use Results<TResult1, TResultN>
como tipo de valor devuelto del controlador de punto de conexión en lugar de IResult
cuando:
- Se devuelvan varios tipos de implementación
IResult
desde el controlador de punto de conexión. - La clase estática
TypedResult
se use para crear los objetosIResult
.
Esta alternativa es mejor que devolver IResult
porque los tipos de unión genéricos conservan automáticamente los metadatos del punto de conexión. Y dado que los tipos de unión Results<TResult1, TResultN>
implementan operadores de conversión implícitos, el compilador puede convertir automáticamente los tipos especificados en los argumentos genéricos en una instancia del tipo de unión.
Esto tiene la ventaja adicional de proporcionar la comprobación en tiempo de compilación de que un controlador de ruta solo devuelve realmente los resultados que declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<>
, se producirá un error de compilación.
Tenga en cuenta el siguiente punto de conexión, para el que se devuelve un código de estado 400 BadRequest
cuando orderId
es mayor que 999
. De lo contrario, genera una respuesta de que todo está correcto (200 OK
) con el contenido esperado.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Para documentar este punto de conexión correctamente, se llama al método de extensión Produces
. Sin embargo, dado que el asistente TypedResults
incluye automáticamente los metadatos para el punto de conexión, puede devolver el tipo de unión Results<T1, Tn>
en su lugar, como se muestra en el código siguiente.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Resultados integrados
Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults
es preferible a devolver Results
. Para más información, consulte TypedResults frente a Results.
En las secciones siguientes se muestra el uso de los asistentes de resultados comunes.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync es una manera alternativa de devolver JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Código de estado personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Internal Server Error
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
En el ejemplo anterior se devuelve un código de estado 500.
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
las sobrecargas permiten el acceso al flujo de respuesta HTTP subyacente sin almacenamiento en búfer. En el ejemplo siguiente se usa ImageSharp para devolver un tamaño reducido de la imagen especificada:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
En el ejemplo siguiente se transmite una imagen desde Azure Blob Storage:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
En el ejemplo siguiente se transmite un vídeo desde un blob de Azure:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Archivo
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces HttpResult
Las interfaces siguientes del espacio de nombres Microsoft.AspNetCore.Http proporcionan una manera de detectar el tipo IResult
en tiempo de ejecución, que es un patrón común en las implementaciones de filtros:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Este es un ejemplo de un filtro que usa una de estas interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
Para obtener más información, consulte Filtros en las aplicaciones de API mínimas y Tipos de implementación de IResult.
Personalización de respuestas
Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Además, un tipo IResult
personalizado puede proporcionar su propia anotación implementando la interfaz IEndpointMetadataProvider. Por ejemplo, el código siguiente agrega una anotación al tipo HtmlResult
anterior que describe la respuesta generada por el punto de conexión.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
es una implementación de IProducesResponseTypeMetadata que define el tipo de contenido de respuesta generada text/html
y el código de estado 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Un enfoque alternativo consiste en usar Microsoft.AspNetCore.Mvc.ProducesAttribute para describir la respuesta generada. El código siguiente cambia el método PopulateMetadata
para usar ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configuración de las opciones de serialización de JSON
De manera predeterminada, las aplicaciones de API mínimas usan las opciones Web defaults
durante la serialización y deserialización JSON.
Configuración global de las opciones de serialización de JSON
Las opciones se pueden configurar globalmente para una aplicación mediante la invocación de ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Dado que se incluyen campos, el código anterior lee NameField
y lo incluye en el JSON de salida.
Configuración de las opciones de serialización de JSON para un punto de conexión
Para configurar las opciones de serialización para un punto de conexión, invoque Results.Json y páselo a un objeto JsonSerializerOptions, como se muestra en el ejemplo siguiente:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Como alternativa, use una sobrecarga de WriteAsJsonAsync que acepte un objeto JsonSerializerOptions. En el ejemplo siguiente se usa esta sobrecarga para dar formato al JSON de salida:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Recursos adicionales
Los puntos de conexión mínimos admiten los siguientes tipos de valores devueltos:
string
: incluyeTask<string>
yValueTask<string>
.T
(cualquier otro tipo): incluyeTask<T>
yValueTask<T>
.- Basado en
IResult
: incluyeTask<IResult>
yValueTask<IResult>
.
Valores devueltos string
Comportamiento | Content-Type |
---|---|
El marco escribe la cadena directamente en la respuesta. | text/plain |
Tenga en cuenta el siguiente controlador de ruta, que devuelve un texto Hello world
.
app.MapGet("/hello", () => "Hello World");
El código de estado 200
se devuelve con el encabezado Content-Type text/plain
y el siguiente contenido.
Hello World
Valores devueltos T
(cualquier otro tipo)
Comportamiento | Content-Type |
---|---|
El marco JSON serializa la respuesta. | application/json |
Tenga en cuenta el siguiente controlador de ruta, que devuelve un tipo anónimo que contiene una propiedad de cadena Message
.
app.MapGet("/hello", () => new { Message = "Hello World" });
El código de estado 200
se devuelve con el encabezado Content-Type application/json
y el siguiente contenido.
{"message":"Hello World"}
Valores devueltos IResult
Comportamiento | Content-Type |
---|---|
El marco llama a IResult.ExecuteAsync. | Decidido por la implementación de IResult . |
La interfaz IResult
define un contrato que representa el resultado de un punto de conexión HTTP. Las clases estáticas Results y TypedResults se usan para crear distintos objetos IResult
que representan diferentes tipos de respuestas.
TypedResults frente a Results
Las clases estáticas Results y TypedResults proporcionan conjuntos de asistentes de resultados similares. La clase TypedResults
es el equivalente con tipo de la clase Results
. Sin embargo, el tipo de valor devuelto de los asistentes Results
es IResult, mientras que cada tipo de valor devuelto del asistente TypedResults
es uno de los tipos de implementación IResult
. La diferencia significa que para los asistentes Results
hace falta una conversión cuando el tipo concreto es necesario, por ejemplo, para las pruebas unitarias. Los tipos de implementación se definen en el espacio de nombres Microsoft.AspNetCore.Http.HttpResults.
Devolver TypedResults
en lugar de Results
tiene las siguientes ventajas:
- Los asistentes
TypedResults
devuelven objetos fuertemente tipados, que pueden mejorar la legibilidad del código, las pruebas unitarias y reducir la posibilidad de errores en tiempo de ejecución. - El tipo de implementación proporciona automáticamente los metadatos del tipo de respuesta para que OpenAPI describa el punto de conexión.
Ten en cuenta el punto de conexión siguiente, para el que se genera un código de estado 200 OK
con la respuesta JSON esperada.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Para documentar este punto de conexión correctamente, se llama al método de extensiones Produces
. Sin embargo, no es necesario llamar a Produces
si se usa TypedResults
en lugar de Results
, como se muestra en el código siguiente. TypedResults
proporciona automáticamente los metadatos para el punto de conexión.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Para obtener más información sobre cómo describir un tipo de respuesta, consulte Compatibilidad con OpenAPI en API mínimas.
Como se mencionó anteriormente, cuando se usa TypedResults
, no se necesita una conversión. Tenga en cuenta la siguiente API mínima que devuelve una clase TypedResults
.
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
La prueba siguiente comprueba el tipo concreto completo:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Dado que todos los métodos de Results
devuelven IResult
en su firma, el compilador deduce automáticamente que como el tipo de valor devuelto del delegado de solicitud al devolver resultados diferentes de un único punto de conexión. TypedResults
requiere el uso de Results<T1, TN>
de dichos delegados.
El método siguiente se compila porque Results.Ok
y Results.NotFound
se declaran como IResult
devueltos, aunque los tipos concretos reales de los objetos devueltos son diferentes:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
El método siguiente no se compila, ya que TypedResults.Ok
y TypedResults.NotFound
se declaran como devolver tipos diferentes y el compilador no intentará deducir el mejor tipo de coincidencia:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Para usar TypedResults
, el tipo de valor devuelto debe declararse completamente, que cuando es asincrónico requiere el contenedor Task<>
. El uso de TypedResults
es más detallado, pero es el inconveniente de tener la información de tipo disponible estáticamente y, por lo tanto, capaz de describirse automáticamente en OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Results<TResult1, TResultN>
Use Results<TResult1, TResultN>
como tipo de valor devuelto del controlador de punto de conexión en lugar de IResult
cuando:
- Se devuelvan varios tipos de implementación
IResult
desde el controlador de punto de conexión. - La clase estática
TypedResult
se use para crear los objetosIResult
.
Esta alternativa es mejor que devolver IResult
porque los tipos de unión genéricos conservan automáticamente los metadatos del punto de conexión. Y dado que los tipos de unión Results<TResult1, TResultN>
implementan operadores de conversión implícitos, el compilador puede convertir automáticamente los tipos especificados en los argumentos genéricos en una instancia del tipo de unión.
Esto tiene la ventaja adicional de proporcionar la comprobación en tiempo de compilación de que un controlador de ruta solo devuelve realmente los resultados que declara. Si se intenta devolver un tipo que no se declara como uno de los argumentos genéricos de Results<>
, se producirá un error de compilación.
Tenga en cuenta el siguiente punto de conexión, para el que se devuelve un código de estado 400 BadRequest
cuando orderId
es mayor que 999
. De lo contrario, genera una respuesta de que todo está correcto (200 OK
) con el contenido esperado.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Para documentar este punto de conexión correctamente, se llama al método de extensión Produces
. Sin embargo, dado que el asistente TypedResults
incluye automáticamente los metadatos para el punto de conexión, puede devolver el tipo de unión Results<T1, Tn>
en su lugar, como se muestra en el código siguiente.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Resultados integrados
Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults
es preferible a devolver Results
. Para más información, consulte TypedResults frente a Results.
En las secciones siguientes se muestra el uso de los asistentes de resultados comunes.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync es una manera alternativa de devolver JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Código de estado personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
las sobrecargas permiten el acceso al flujo de respuesta HTTP subyacente sin almacenamiento en búfer. En el ejemplo siguiente se usa ImageSharp para devolver un tamaño reducido de la imagen especificada:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
En el ejemplo siguiente se transmite una imagen desde Azure Blob Storage:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
En el ejemplo siguiente se transmite un vídeo desde un blob de Azure:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Archivo
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces HttpResult
Las interfaces siguientes del espacio de nombres Microsoft.AspNetCore.Http proporcionan una manera de detectar el tipo IResult
en tiempo de ejecución, que es un patrón común en las implementaciones de filtros:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Este es un ejemplo de un filtro que usa una de estas interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
Para obtener más información, consulte Filtros en las aplicaciones de API mínimas y Tipos de implementación de IResult.
Personalización de respuestas
Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Además, un tipo IResult
personalizado puede proporcionar su propia anotación implementando la interfaz IEndpointMetadataProvider. Por ejemplo, el código siguiente agrega una anotación al tipo HtmlResult
anterior que describe la respuesta generada por el punto de conexión.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
es una implementación de IProducesResponseTypeMetadata que define el tipo de contenido de respuesta generada text/html
y el código de estado 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Un enfoque alternativo consiste en usar Microsoft.AspNetCore.Mvc.ProducesAttribute para describir la respuesta generada. El código siguiente cambia el método PopulateMetadata
para usar ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configuración de las opciones de serialización de JSON
De manera predeterminada, las aplicaciones de API mínimas usan las opciones Web defaults
durante la serialización y deserialización JSON.
Configuración global de las opciones de serialización de JSON
Las opciones se pueden configurar globalmente para una aplicación mediante la invocación de ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Dado que se incluyen campos, el código anterior lee NameField
y lo incluye en el JSON de salida.
Configuración de las opciones de serialización de JSON para un punto de conexión
Para configurar las opciones de serialización para un punto de conexión, invoque Results.Json y páselo a un objeto JsonSerializerOptions, como se muestra en el ejemplo siguiente:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Como alternativa, use una sobrecarga de WriteAsJsonAsync que acepte un objeto JsonSerializerOptions. En el ejemplo siguiente se usa esta sobrecarga para dar formato al JSON de salida:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }