Come creare risposte in app per le API minime

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 8 di questo articolo.

Gli endpoint minimi supportano i tipi di valori restituiti seguenti:

  1. string : include Task<string> e ValueTask<string>.
  2. T (Qualsiasi altro tipo): include Task<T> e ValueTask<T>.
  3. IResult based: include Task<IResult> e ValueTask<IResult>.

string valori restituiti

Comportamento Content-Type
Il framework scrive la stringa direttamente nella risposta. text/plain

Si consideri il gestore di route seguente, che restituisce un Hello world testo.

app.MapGet("/hello", () => "Hello World");

Il 200 codice di stato viene restituito con text/plain l'intestazione Content-Type e il contenuto seguente.

Hello World

T (Qualsiasi altro tipo) restituisce valori

Comportamento Content-Type
Il framework JSON serializza la risposta. application/json

Si consideri il gestore di route seguente, che restituisce un tipo anonimo contenente una Message proprietà stringa.

app.MapGet("/hello", () => new { Message = "Hello World" });

Il 200 codice di stato viene restituito con application/json l'intestazione Content-Type e il contenuto seguente.

{"message":"Hello World"}

IResult valori restituiti

Comportamento Content-Type
Il framework chiama IResult.ExecuteAsync. Deciso dall'implementazione IResult .

L'interfaccia IResult definisce un contratto che rappresenta il risultato di un endpoint HTTP. La classe static Results e i TypedResult statici vengono usati per creare vari IResult oggetti che rappresentano diversi tipi di risposte.

TypedResults e Risultati

Le Results classi statiche e TypedResults forniscono set simili di helper di risultati. La TypedResults classe è l'equivalente tipizzato della Results classe . Tuttavia, il Results tipo restituito degli helper è IResult, mentre il tipo restituito di ogni TypedResults helper è uno dei IResult tipi di implementazione. La differenza significa che per Results gli helper è necessaria una conversione quando è necessario il tipo concreto, ad esempio per unit test. I tipi di implementazione sono definiti nello spazio dei Microsoft.AspNetCore.Http.HttpResults nomi .

La restituzione TypedResults anziché Results presenta i vantaggi seguenti:

  • TypedResults gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, unit test e ridurre la probabilità di errori di runtime.
  • Il tipo di implementazione fornisce automaticamente i metadati del tipo di risposta per OpenAPI per descrivere l'endpoint.

Si consideri l'endpoint seguente, per il quale viene generato un 200 OK codice di stato con la risposta JSON prevista.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

Per documentare correttamente questo endpoint, viene chiamato il metodo Produces extensions. Tuttavia, non è necessario chiamare Produces se TypedResults viene usato invece di Results, come illustrato nel codice seguente. TypedResults fornisce automaticamente i metadati per l'endpoint.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Per altre informazioni sulla descrizione di un tipo di risposta, vedere Supporto OpenAPI nelle API minime.

Come accennato in precedenza, quando si usa TypedResults, non è necessaria una conversione. Si consideri l'API minima seguente che restituisce una TypedResults classe

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

Il test seguente verifica la presenza del 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);
    });
}

Poiché tutti i metodi sulla Results restituzione IResult nella firma, il compilatore deduce automaticamente che come tipo restituito dal delegato della richiesta quando restituisce risultati diversi da un singolo endpoint. TypedResults richiede l'uso di Results<T1, TN> da tali delegati.

Il metodo seguente viene compilato perché sia Results.Ok che Results.NotFound vengono dichiarati come restituiti IResult, anche se i tipi concreti effettivi degli oggetti restituiti sono diversi:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

Il metodo seguente non viene compilato perché TypedResults.Ok e TypedResults.NotFound vengono dichiarati come tipi diversi e il compilatore non tenterà di dedurre il tipo di corrispondenza migliore:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

Per usare TypedResults, il tipo restituito deve essere completamente dichiarato, che quando è necessario il Task<> wrapper asincrono. L'uso TypedResults è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo disponibili in modo statico e quindi in grado di autodescrittura in 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());

Risultati<TResult1, TResultN>

Usare Results<TResult1, TResultN> come tipo restituito del gestore endpoint IResult anziché quando:

  • Dal gestore endpoint vengono restituiti più IResult tipi di implementazione.
  • La classe statica viene utilizzata TypedResult per creare gli IResult oggetti .

Questa alternativa è migliore rispetto alla restituzione IResult perché i tipi di unione generica mantengono automaticamente i metadati dell'endpoint. Poiché i Results<TResult1, TResultN> tipi di unione implementano operatori cast impliciti, il compilatore può convertire automaticamente i tipi specificati negli argomenti generici in un'istanza del tipo di unione.

Questo ha il vantaggio aggiunto di fornire un controllo in fase di compilazione che un gestore di route restituisce effettivamente solo i risultati che dichiara. Se si tenta di restituire un tipo non dichiarato come uno degli argomenti generici, si Results<> verifica un errore di compilazione.

Si consideri l'endpoint seguente, per il quale viene restituito un 400 BadRequest codice di stato quando è orderId maggiore di 999. In caso contrario, produce un 200 OK oggetto con il contenuto previsto.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Per documentare correttamente questo endpoint, viene chiamato il metodo Produces di estensione. Tuttavia, poiché l'helper TypedResults include automaticamente i metadati per l'endpoint, è possibile restituire invece il Results<T1, Tn> tipo di unione, come illustrato nel codice seguente.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Risultati predefiniti

Gli helper di risultati comuni esistono nelle Results classi statiche e TypedResults . La restituzione di è preferibile TypedResults Resultsper restituire . Per altre informazioni, vedere TypedResults vs Results.

Le sezioni seguenti illustrano l'utilizzo degli helper dei risultati comuni.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync è un modo alternativo per restituire JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Codice di stato personalizzato

app.MapGet("/405", () => Results.StatusCode(405));

Internal Server Error

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

L'esempio precedente restituisce un codice di stato 500.

Testo

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 gli overload consentono l'accesso al flusso di risposta HTTP sottostante senza buffering. L'esempio seguente usa ImageSharp per restituire una dimensione ridotta dell'immagine specificata:

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'esempio seguente trasmette un'immagine dall'archivio BLOB di 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'esempio seguente trasmette un video da un BLOB di 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);
});

Reindirizza

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

file

app.MapGet("/download", () => Results.File("myfile.text"));

Interfacce HttpResult

Le interfacce seguenti nello Microsoft.AspNetCore.Http spazio dei nomi consentono di rilevare il IResult tipo in fase di esecuzione, un modello comune nelle implementazioni del filtro:

Ecco un esempio di filtro che usa una di queste interfacce:

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
    };
});

Per altre informazioni, vedere Filtri in App per le API minime e tipi di implementazione IResult.

Personalizzazione delle risposte

Le applicazioni possono controllare le risposte implementando un tipo personalizzato IResult . Il codice seguente è un esempio di tipo di risultato 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);
    }
}

È consigliabile aggiungere un metodo di estensione per Microsoft.AspNetCore.Http.IResultExtensions rendere questi risultati personalizzati più individuabili.

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();

Inoltre, un tipo personalizzato IResult può fornire una propria annotazione implementando l'interfaccia IEndpointMetadataProvider . Ad esempio, il codice seguente aggiunge un'annotazione al tipo precedente HtmlResult che descrive la risposta prodotta dall'endpoint.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata è un'implementazione di che definisce il tipo di text/html contenuto di IProducesResponseTypeMetadata risposta prodotto e il codice 200 OKdi stato .

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Un approccio alternativo consiste nell'usare per Microsoft.AspNetCore.Mvc.ProducesAttribute descrivere la risposta prodotta. Il codice seguente modifica il PopulateMetadata metodo per usare ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Configurare le opzioni di serializzazione JSON

Per impostazione predefinita, le app per le API minime usano Web defaults le opzioni durante la serializzazione e la deserializzazione JSON.

Configurare le opzioni di serializzazione JSON a livello globale

Le opzioni possono essere configurate a livello globale per un'app richiamando ConfigureHttpJsonOptions. L'esempio seguente include campi pubblici e formatta l'output 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
// }

Poiché i campi sono inclusi, il codice precedente legge NameField e lo include nel codice JSON di output.

Configurare le opzioni di serializzazione JSON per un endpoint

Per configurare le opzioni di serializzazione per un endpoint, richiamare Results.Json e passarvi un JsonSerializerOptions oggetto, come illustrato nell'esempio seguente:

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
// }

In alternativa, usare un overload di WriteAsJsonAsync che accetta un JsonSerializerOptions oggetto . L'esempio seguente usa questo overload per formattare il codice JSON di output:

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
// }

Risorse aggiuntive

Gli endpoint minimi supportano i tipi di valori restituiti seguenti:

  1. string : include Task<string> e ValueTask<string>.
  2. T (Qualsiasi altro tipo): include Task<T> e ValueTask<T>.
  3. IResult based: include Task<IResult> e ValueTask<IResult>.

string valori restituiti

Comportamento Content-Type
Il framework scrive la stringa direttamente nella risposta. text/plain

Si consideri il gestore di route seguente, che restituisce un Hello world testo.

app.MapGet("/hello", () => "Hello World");

Il 200 codice di stato viene restituito con text/plain l'intestazione Content-Type e il contenuto seguente.

Hello World

T (Qualsiasi altro tipo) restituisce valori

Comportamento Content-Type
Il framework JSON serializza la risposta. application/json

Si consideri il gestore di route seguente, che restituisce un tipo anonimo contenente una Message proprietà stringa.

app.MapGet("/hello", () => new { Message = "Hello World" });

Il 200 codice di stato viene restituito con application/json l'intestazione Content-Type e il contenuto seguente.

{"message":"Hello World"}

IResult valori restituiti

Comportamento Content-Type
Il framework chiama IResult.ExecuteAsync. Deciso dall'implementazione IResult .

L'interfaccia IResult definisce un contratto che rappresenta il risultato di un endpoint HTTP. La classe static Results e i TypedResult statici vengono usati per creare vari IResult oggetti che rappresentano diversi tipi di risposte.

TypedResults e Risultati

Le Results classi statiche e TypedResults forniscono set simili di helper di risultati. La TypedResults classe è l'equivalente tipizzato della Results classe . Tuttavia, il Results tipo restituito degli helper è IResult, mentre il tipo restituito di ogni TypedResults helper è uno dei IResult tipi di implementazione. La differenza significa che per Results gli helper è necessaria una conversione quando è necessario il tipo concreto, ad esempio per unit test. I tipi di implementazione sono definiti nello spazio dei Microsoft.AspNetCore.Http.HttpResults nomi .

La restituzione TypedResults anziché Results presenta i vantaggi seguenti:

  • TypedResults gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, unit test e ridurre la probabilità di errori di runtime.
  • Il tipo di implementazione fornisce automaticamente i metadati del tipo di risposta per OpenAPI per descrivere l'endpoint.

Si consideri l'endpoint seguente, per il quale viene generato un 200 OK codice di stato con la risposta JSON prevista.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

Per documentare correttamente questo endpoint, viene chiamato il metodo Produces extensions. Tuttavia, non è necessario chiamare Produces se TypedResults viene usato invece di Results, come illustrato nel codice seguente. TypedResults fornisce automaticamente i metadati per l'endpoint.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Per altre informazioni sulla descrizione di un tipo di risposta, vedere Supporto OpenAPI nelle API minime.

Come accennato in precedenza, quando si usa TypedResults, non è necessaria una conversione. Si consideri l'API minima seguente che restituisce una TypedResults classe

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

Il test seguente verifica la presenza del 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);
    });
}

Poiché tutti i metodi sulla Results restituzione IResult nella firma, il compilatore deduce automaticamente che come tipo restituito dal delegato della richiesta quando restituisce risultati diversi da un singolo endpoint. TypedResults richiede l'uso di Results<T1, TN> da tali delegati.

Il metodo seguente viene compilato perché sia Results.Ok che Results.NotFound vengono dichiarati come restituiti IResult, anche se i tipi concreti effettivi degli oggetti restituiti sono diversi:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

Il metodo seguente non viene compilato perché TypedResults.Ok e TypedResults.NotFound vengono dichiarati come tipi diversi e il compilatore non tenterà di dedurre il tipo di corrispondenza migliore:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

Per usare TypedResults, il tipo restituito deve essere completamente dichiarato, che quando è necessario il Task<> wrapper asincrono. L'uso TypedResults è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo disponibili in modo statico e quindi in grado di autodescrittura in 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());

Risultati<TResult1, TResultN>

Usare Results<TResult1, TResultN> come tipo restituito del gestore endpoint IResult anziché quando:

  • Dal gestore endpoint vengono restituiti più IResult tipi di implementazione.
  • La classe statica viene utilizzata TypedResult per creare gli IResult oggetti .

Questa alternativa è migliore rispetto alla restituzione IResult perché i tipi di unione generica mantengono automaticamente i metadati dell'endpoint. Poiché i Results<TResult1, TResultN> tipi di unione implementano operatori cast impliciti, il compilatore può convertire automaticamente i tipi specificati negli argomenti generici in un'istanza del tipo di unione.

Questo ha il vantaggio aggiunto di fornire un controllo in fase di compilazione che un gestore di route restituisce effettivamente solo i risultati che dichiara. Se si tenta di restituire un tipo non dichiarato come uno degli argomenti generici, si Results<> verifica un errore di compilazione.

Si consideri l'endpoint seguente, per il quale viene restituito un 400 BadRequest codice di stato quando è orderId maggiore di 999. In caso contrario, produce un 200 OK oggetto con il contenuto previsto.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

Per documentare correttamente questo endpoint, viene chiamato il metodo Produces di estensione. Tuttavia, poiché l'helper TypedResults include automaticamente i metadati per l'endpoint, è possibile restituire invece il Results<T1, Tn> tipo di unione, come illustrato nel codice seguente.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Risultati predefiniti

Gli helper di risultati comuni esistono nelle Results classi statiche e TypedResults . La restituzione di è preferibile TypedResults Resultsper restituire . Per altre informazioni, vedere TypedResults vs Results.

Le sezioni seguenti illustrano l'utilizzo degli helper dei risultati comuni.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync è un modo alternativo per restituire JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Codice di stato personalizzato

app.MapGet("/405", () => Results.StatusCode(405));

Testo

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 gli overload consentono l'accesso al flusso di risposta HTTP sottostante senza buffering. L'esempio seguente usa ImageSharp per restituire una dimensione ridotta dell'immagine specificata:

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'esempio seguente trasmette un'immagine dall'archivio BLOB di 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'esempio seguente trasmette un video da un BLOB di 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);
});

Reindirizza

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

file

app.MapGet("/download", () => Results.File("myfile.text"));

Interfacce HttpResult

Le interfacce seguenti nello Microsoft.AspNetCore.Http spazio dei nomi consentono di rilevare il IResult tipo in fase di esecuzione, un modello comune nelle implementazioni del filtro:

Ecco un esempio di filtro che usa una di queste interfacce:

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
    };
});

Per altre informazioni, vedere Filtri in App per le API minime e tipi di implementazione IResult.

Personalizzazione delle risposte

Le applicazioni possono controllare le risposte implementando un tipo personalizzato IResult . Il codice seguente è un esempio di tipo di risultato 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);
    }
}

È consigliabile aggiungere un metodo di estensione per Microsoft.AspNetCore.Http.IResultExtensions rendere questi risultati personalizzati più individuabili.

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();

Inoltre, un tipo personalizzato IResult può fornire una propria annotazione implementando l'interfaccia IEndpointMetadataProvider . Ad esempio, il codice seguente aggiunge un'annotazione al tipo precedente HtmlResult che descrive la risposta prodotta dall'endpoint.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata è un'implementazione di che definisce il tipo di text/html contenuto di IProducesResponseTypeMetadata risposta prodotto e il codice 200 OKdi stato .

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

Un approccio alternativo consiste nell'usare per Microsoft.AspNetCore.Mvc.ProducesAttribute descrivere la risposta prodotta. Il codice seguente modifica il PopulateMetadata metodo per usare ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Configurare le opzioni di serializzazione JSON

Per impostazione predefinita, le app per le API minime usano Web defaults le opzioni durante la serializzazione e la deserializzazione JSON.

Configurare le opzioni di serializzazione JSON a livello globale

Le opzioni possono essere configurate a livello globale per un'app richiamando ConfigureHttpJsonOptions. L'esempio seguente include campi pubblici e formatta l'output 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
// }

Poiché i campi sono inclusi, il codice precedente legge NameField e lo include nel codice JSON di output.

Configurare le opzioni di serializzazione JSON per un endpoint

Per configurare le opzioni di serializzazione per un endpoint, richiamare Results.Json e passarvi un JsonSerializerOptions oggetto, come illustrato nell'esempio seguente:

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
// }

In alternativa, usare un overload di WriteAsJsonAsync che accetta un JsonSerializerOptions oggetto . L'esempio seguente usa questo overload per formattare il codice JSON di output:

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
// }

Risorse aggiuntive