Partager via


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 9 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 support .NET et .NET Core. Pour la version actuelle, consultez la version .NET 9 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 9 de cet article.

Les points de terminaison minimaux prennent en charge les types de valeurs de retour suivants :

  1. string : cela inclut Task<string> et ValueTask<string>.
  2. T (Tout autre type) : cela inclut Task<T> et ValueTask<T>.
  3. basé surIResult : cela inclut Task<IResult> et ValueTask<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 :

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 objets IResult.

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 :

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 :

  1. string : cela inclut Task<string> et ValueTask<string>.
  2. T (Tout autre type) : cela inclut Task<T> et ValueTask<T>.
  3. basé surIResult : cela inclut Task<IResult> et ValueTask<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 :

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 objets IResult.

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 :

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