Comment créer des réponses dans des applications API minimales
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 8 de cet article.
Les points de terminaison minimaux prennent en charge les types de valeurs de retour suivants :
string
: cela inclutTask<string>
etValueTask<string>
.T
(Tout autre type) : cela inclutTask<T>
etValueTask<T>
.- basé sur
IResult
: cela inclutTask<IResult>
etValueTask<IResult>
.
valeurs de retour string
Comportement | Content-Type |
---|---|
L’infrastructure écrit la chaîne directement dans la réponse. | text/plain |
Considérez le gestionnaire de routage suivant, qui retourne un texte Hello world
.
app.MapGet("/hello", () => "Hello World");
Le code d’état 200
est retourné avec une en-tête Content-Type text/plain
et le contenu suivant.
Hello World
Valeurs de retour T
(Tout autre type)
Comportement | Type de contenu |
---|---|
L’infrastructure JSON sérialise la réponse. | application/json |
Considérez le gestionnaire de routes suivant, qui retourne un type anonyme contenant une propriété de chaîne Message
.
app.MapGet("/hello", () => new { Message = "Hello World" });
Le code d’état 200
est retourné avec une en-tête Content-Type application/json
et le contenu suivant.
{"message":"Hello World"}
valeurs de retour IResult
Comportement | Content-Type |
---|---|
L’infrastructure appelle IResult.ExecuteAsync. | Décidé par l’implémentation IResult . |
L’interface IResult
définit un contrat qui représente le résultat d’un point de terminaison HTTP. La classe Results statique et les TypedResults statiques sont utilisés pour créer différents objets IResult
qui représentent différents types de réponses.
TypedResults vs Results
Les classes statiques Results et TypedResults fournissent des ensembles d’assistance similaires sur les résultats. La classe TypedResults
est l'équivalent typé de la classe Results
. Toutefois, le type de retour de les assistances Results
est IResult, tandis que le type de retour de chaque assistance TypedResults
est l’un des types d’implémentation IResult
. La différence signifie que pour les assistances Results
, une conversion est nécessaire lorsque le type concret est nécessaire, par exemple, pour les tests unitaires. Les types d’implémentation sont définis dans l’espace de noms Microsoft.AspNetCore.Http.HttpResults.
Retourner TypedResults
plutôt que Results
présente les avantages suivants :
TypedResults
les assistants renvoient des objets fortement typés, ce qui peut améliorer la lisibilité du code, les tests unitaires et réduire les risques d'erreurs d'exécution.- Le type d'implémentation fournit automatiquement les métadonnées du type de réponse pour OpenAPI afin de décrire le point de terminaison.
Considérez le point de terminaison suivant, pour lequel un code d'état 200 OK
avec la réponse JSON attendue est produit.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Pour documenter correctement ce point de terminaison, la méthode d’extensions Produces
est appelée. Toutefois, il n’est pas nécessaire d’appeler Produces
si TypedResults
est utilisée au lieu de Results
, comme indiqué dans le code suivant. TypedResults
fournit automatiquement les métadonnées du point de terminaison.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Pour plus d’informations sur la description d’un type de réponse, consultez Prise en charge d’OpenAPI dans les API minimales.
Comme mentionné précédemment, lors de l'utilisation de TypedResults
, une conversion n'est pas nécessaire. Considérez l'API minimale suivante qui renvoie une classe TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Le test suivant vérifie le type de béton complet :
[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);
});
}
Étant donné que toutes les méthodes sur Results
sont renvoyées à IResult
dans leur signature, le compilateur en déduit automatiquement que le type de retour du délégué de la requête lors du renvoi de différents résultats à partir d'un seul point de terminaison. TypedResults
nécessite l'utilisation de Results<T1, TN>
ces délégués.
La méthode suivante est compilée car les deux Results.Ok
et Results.NotFound
sont déclarés comme return IResult
, même si les types concrets réels des objets renvoyés sont différents :
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
La méthode suivante ne se compile pas, car TypedResults.Ok
et TypedResults.NotFound
sont déclarés comme renvoyant des types différents et le compilateur ne tentera pas de déduire le meilleur type correspondant :
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Pour utiliser TypedResults
, le type de retour doit être entièrement déclaré, ce qui, lorsqu'il est asynchrone, nécessite le wrapper Task<>
. L'utilisation de TypedResults
est plus détaillée, mais c'est le compromis pour que les informations de type soient disponibles statiquement et donc capables de s'auto-decrire à 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());
Résultats<TResult1, TResultN>
Utilisez Results<TResult1, TResultN>
comme type de retour du gestionnaire de points de terminaison au lieu de IResult
quand :
- Plusieurs types d’implémentation
IResult
sont retournés par le gestionnaire de points de terminaison. - La classe statique
TypedResult
est utilisée pour créer les objetsIResult
.
Cette alternative est préférable au retour de IResult
, car les types d’union génériques conservent automatiquement les métadonnées du point de terminaison. Et étant donné que les types d’union Results<TResult1, TResultN>
implémentent des opérateurs de conversion implicite, le compilateur peut convertir automatiquement les types spécifiés dans les arguments génériques en une instance du type d’union.
Cela offre l’avantage supplémentaire de fournir une vérification au moment de la compilation qu’un gestionnaire de routage retourne uniquement les résultats qu’il déclare. La tentative de retourner un type qui n’est pas déclaré comme l’un des arguments génériques vers Results<>
entraîne une erreur de compilation.
Considérez le point de terminaison suivant, pour lequel un code d’état 400 BadRequest
est retourné lorsque orderId
est supérieur à 999
. Sinon, il produit 200 OK
avec le contenu attendu.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Pour documenter correctement ce point de terminaison, la méthode d’extension Produces
est appelée. Toutefois, étant donné que l’assistance TypedResults
inclut automatiquement les métadonnées du point de terminaison, vous pouvez retourner le type d’union Results<T1, Tn>
à la place, comme indiqué dans le code suivant.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Résultats intégrés
Des assistants de résultats communs existent dans les classes Results et TypedResults statiques. Le retour de TypedResults
est préférable au retour Results
. Pour plus d'informations, consultez TypedResults vs Results.
Les sections suivantes illustrent l’utilisation des helpers de résultats courants.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync est une autre façon de retourner JSON :
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Code d’état personnalisé
app.MapGet("/405", () => Results.StatusCode(405));
Erreur interne du serveur
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
L’exemple précédent retourne un code d’état 500.
Détails
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();
Les surcharges Results.Stream
autorisent l’accès au flux de réponse HTTP sous-jacent sans mise en mémoire tampon. L’exemple suivant utilise ImageSharp pour retourner une taille réduite de l’image spécifiée :
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);
}
L’exemple suivant diffuse une image à partir du stockage Blob 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");
});
L’exemple suivant diffuse une vidéo à partir d’un objet blob 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);
});
Rediriger
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Fichier
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces HttpResult
Les interfaces suivantes dans l’espace de noms Microsoft.AspNetCore.Http permettent de détecter le type IResult
au moment de l’exécution, ce qui est un modèle courant dans les implémentations de filtre :
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Voici un exemple de filtre qui utilise l’une de ces 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
};
});
Pour plus d’informations, consultez Filtres dans les applications API minimales et types d’implémentation IResult.
Personnalisation des réponses
Les applications peuvent contrôler les réponses en implémentant un type IResult personnalisé. Le code suivant est un exemple de type de résultat 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);
}
}
Nous vous recommandons d’ajouter une méthode d’extension à Microsoft.AspNetCore.Http.IResultExtensions pour rendre ces résultats personnalisés plus détectables.
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();
En outre, un type personnalisé IResult
peut fournir sa propre annotation en implémentant l’interface IEndpointMetadataProvider. Par exemple, le code suivant ajoute une annotation au type HtmlResult
précédent qui décrit la réponse produite par le point de terminaison.
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
est une implémentation de IProducesResponseTypeMetadata qui définit le type de contenu de réponse produit text/html
et le code d’état 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Une autre approche consiste à utiliser Microsoft.AspNetCore.Mvc.ProducesAttribute pour décrire la réponse produite. Le code suivant modifie la méthode PopulateMetadata
pour utiliser ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configurer les options de sérialisation JSON
Par défaut, les applications API minimales utilisent des options Web defaults
pendant la sérialisation et la désérialisation JSON.
Configurer globalement les options de sérialisation JSON
Les options peuvent être configurées globalement pour une application en appelant ConfigureHttpJsonOptions. L’exemple suivant inclut des champs publics et des formats de sortie 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
// }
Étant donné que les champs sont inclus, le code précédent lit NameField
et l’inclut dans la sortie JSON.
Configurer les options de sérialisation JSON pour un point de terminaison
Pour configurer les options de sérialisation d’un point de terminaison, appelez Results.Json et transmettez-lui un objet JsonSerializerOptions , comme illustré dans l’exemple suivant :
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
// }
En guise d’alternative, utilisez une surcharge de WriteAsJsonAsync qui accepte un objet JsonSerializerOptions. L’exemple suivant utilise cette surcharge pour mettre en forme la sortie JSON :
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Ressources complémentaires
Les points de terminaison minimaux prennent en charge les types de valeurs de retour suivants :
string
: cela inclutTask<string>
etValueTask<string>
.T
(Tout autre type) : cela inclutTask<T>
etValueTask<T>
.- basé sur
IResult
: cela inclutTask<IResult>
etValueTask<IResult>
.
valeurs de retour string
Comportement | Content-Type |
---|---|
L’infrastructure écrit la chaîne directement dans la réponse. | text/plain |
Considérez le gestionnaire de routage suivant, qui retourne un texte Hello world
.
app.MapGet("/hello", () => "Hello World");
Le code d’état 200
est retourné avec une en-tête Content-Type text/plain
et le contenu suivant.
Hello World
Valeurs de retour T
(Tout autre type)
Comportement | Type de contenu |
---|---|
L’infrastructure JSON sérialise la réponse. | application/json |
Considérez le gestionnaire de routes suivant, qui retourne un type anonyme contenant une propriété de chaîne Message
.
app.MapGet("/hello", () => new { Message = "Hello World" });
Le code d’état 200
est retourné avec une en-tête Content-Type application/json
et le contenu suivant.
{"message":"Hello World"}
valeurs de retour IResult
Comportement | Content-Type |
---|---|
L’infrastructure appelle IResult.ExecuteAsync. | Décidé par l’implémentation IResult . |
L’interface IResult
définit un contrat qui représente le résultat d’un point de terminaison HTTP. La classe Results statique et les TypedResults statiques sont utilisés pour créer différents objets IResult
qui représentent différents types de réponses.
TypedResults vs Results
Les classes statiques Results et TypedResults fournissent des ensembles d’assistance similaires sur les résultats. La classe TypedResults
est l'équivalent typé de la classe Results
. Toutefois, le type de retour de les assistances Results
est IResult, tandis que le type de retour de chaque assistance TypedResults
est l’un des types d’implémentation IResult
. La différence signifie que pour les assistances Results
, une conversion est nécessaire lorsque le type concret est nécessaire, par exemple, pour les tests unitaires. Les types d’implémentation sont définis dans l’espace de noms Microsoft.AspNetCore.Http.HttpResults.
Retourner TypedResults
plutôt que Results
présente les avantages suivants :
TypedResults
les assistants renvoient des objets fortement typés, ce qui peut améliorer la lisibilité du code, les tests unitaires et réduire les risques d'erreurs d'exécution.- Le type d'implémentation fournit automatiquement les métadonnées du type de réponse pour OpenAPI afin de décrire le point de terminaison.
Considérez le point de terminaison suivant, pour lequel un code d'état 200 OK
avec la réponse JSON attendue est produit.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Pour documenter correctement ce point de terminaison, la méthode d’extensions Produces
est appelée. Toutefois, il n’est pas nécessaire d’appeler Produces
si TypedResults
est utilisée au lieu de Results
, comme indiqué dans le code suivant. TypedResults
fournit automatiquement les métadonnées du point de terminaison.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Pour plus d’informations sur la description d’un type de réponse, consultez Prise en charge d’OpenAPI dans les API minimales.
Comme mentionné précédemment, lors de l'utilisation de TypedResults
, une conversion n'est pas nécessaire. Considérez l'API minimale suivante qui renvoie une classe TypedResults
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Le test suivant vérifie le type de béton complet :
[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);
});
}
Étant donné que toutes les méthodes sur Results
sont renvoyées à IResult
dans leur signature, le compilateur en déduit automatiquement que le type de retour du délégué de la requête lors du renvoi de différents résultats à partir d'un seul point de terminaison. TypedResults
nécessite l'utilisation de Results<T1, TN>
ces délégués.
La méthode suivante est compilée car les deux Results.Ok
et Results.NotFound
sont déclarés comme return IResult
, même si les types concrets réels des objets renvoyés sont différents :
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
La méthode suivante ne se compile pas, car TypedResults.Ok
et TypedResults.NotFound
sont déclarés comme renvoyant des types différents et le compilateur ne tentera pas de déduire le meilleur type correspondant :
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Pour utiliser TypedResults
, le type de retour doit être entièrement déclaré, ce qui, lorsqu'il est asynchrone, nécessite le wrapper Task<>
. L'utilisation de TypedResults
est plus détaillée, mais c'est le compromis pour que les informations de type soient disponibles statiquement et donc capables de s'auto-decrire à 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());
Résultats<TResult1, TResultN>
Utilisez Results<TResult1, TResultN>
comme type de retour du gestionnaire de points de terminaison au lieu de IResult
quand :
- Plusieurs types d’implémentation
IResult
sont retournés par le gestionnaire de points de terminaison. - La classe statique
TypedResult
est utilisée pour créer les objetsIResult
.
Cette alternative est préférable au retour de IResult
, car les types d’union génériques conservent automatiquement les métadonnées du point de terminaison. Et étant donné que les types d’union Results<TResult1, TResultN>
implémentent des opérateurs de conversion implicite, le compilateur peut convertir automatiquement les types spécifiés dans les arguments génériques en une instance du type d’union.
Cela offre l’avantage supplémentaire de fournir une vérification au moment de la compilation qu’un gestionnaire de routage retourne uniquement les résultats qu’il déclare. La tentative de retourner un type qui n’est pas déclaré comme l’un des arguments génériques vers Results<>
entraîne une erreur de compilation.
Considérez le point de terminaison suivant, pour lequel un code d’état 400 BadRequest
est retourné lorsque orderId
est supérieur à 999
. Sinon, il produit 200 OK
avec le contenu attendu.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Pour documenter correctement ce point de terminaison, la méthode d’extension Produces
est appelée. Toutefois, étant donné que l’assistance TypedResults
inclut automatiquement les métadonnées du point de terminaison, vous pouvez retourner le type d’union Results<T1, Tn>
à la place, comme indiqué dans le code suivant.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Résultats intégrés
Des assistants de résultats communs existent dans les classes Results et TypedResults statiques. Le retour de TypedResults
est préférable au retour Results
. Pour plus d'informations, consultez TypedResults vs Results.
Les sections suivantes illustrent l’utilisation des helpers de résultats courants.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync est une autre façon de retourner JSON :
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Code d’état personnalisé
app.MapGet("/405", () => Results.StatusCode(405));
Texte
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();
Les surcharges Results.Stream
autorisent l’accès au flux de réponse HTTP sous-jacent sans mise en mémoire tampon. L’exemple suivant utilise ImageSharp pour retourner une taille réduite de l’image spécifiée :
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);
}
L’exemple suivant diffuse une image à partir du stockage Blob 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");
});
L’exemple suivant diffuse une vidéo à partir d’un objet blob 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);
});
Rediriger
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Fichier
app.MapGet("/download", () => Results.File("myfile.text"));
Interfaces HttpResult
Les interfaces suivantes dans l’espace de noms Microsoft.AspNetCore.Http permettent de détecter le type IResult
au moment de l’exécution, ce qui est un modèle courant dans les implémentations de filtre :
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Voici un exemple de filtre qui utilise l’une de ces 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
};
});
Pour plus d’informations, consultez Filtres dans les applications API minimales et types d’implémentation IResult.
Personnalisation des réponses
Les applications peuvent contrôler les réponses en implémentant un type IResult personnalisé. Le code suivant est un exemple de type de résultat 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);
}
}
Nous vous recommandons d’ajouter une méthode d’extension à Microsoft.AspNetCore.Http.IResultExtensions pour rendre ces résultats personnalisés plus détectables.
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();
En outre, un type personnalisé IResult
peut fournir sa propre annotation en implémentant l’interface IEndpointMetadataProvider. Par exemple, le code suivant ajoute une annotation au type HtmlResult
précédent qui décrit la réponse produite par le point de terminaison.
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
est une implémentation de IProducesResponseTypeMetadata qui définit le type de contenu de réponse produit text/html
et le code d’état 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Une autre approche consiste à utiliser Microsoft.AspNetCore.Mvc.ProducesAttribute pour décrire la réponse produite. Le code suivant modifie la méthode PopulateMetadata
pour utiliser ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configurer les options de sérialisation JSON
Par défaut, les applications API minimales utilisent des options Web defaults
pendant la sérialisation et la désérialisation JSON.
Configurer globalement les options de sérialisation JSON
Les options peuvent être configurées globalement pour une application en appelant ConfigureHttpJsonOptions. L’exemple suivant inclut des champs publics et des formats de sortie 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
// }
Étant donné que les champs sont inclus, le code précédent lit NameField
et l’inclut dans la sortie JSON.
Configurer les options de sérialisation JSON pour un point de terminaison
Pour configurer les options de sérialisation d’un point de terminaison, appelez Results.Json et transmettez-lui un objet JsonSerializerOptions , comme illustré dans l’exemple suivant :
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
// }
En guise d’alternative, utilisez une surcharge de WriteAsJsonAsync qui accepte un objet JsonSerializerOptions. L’exemple suivant utilise cette surcharge pour mettre en forme la sortie JSON :
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }