Leggere in inglese

Condividi tramite


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 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 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 9 di questo articolo.

Gli endpoint minimi supportano i tipi di valori restituiti seguenti:

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

string valori restituiti

Comportamento Tipo di Contenuto (Content-Type)
Il framework scrive la stringa direttamente nella risposta. text/plain

Considera il seguente gestore di percorso, che restituisce un Hello world testo.

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

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

Hello World

T (Qualsiasi altro tipo) restituisce valori

Comportamento Tipo di Contenuto (Content-Type)
Il framework JSON serializza la risposta. application/json

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

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

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

{"message":"Hello World"}

IResult valori restituiti

Comportamento Tipo di Contenuto (Content-Type)
Il framework chiama IResult.ExecuteAsync. Stabilito 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 vs 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 tipo restituito degli Results helper è IResult, mentre il tipo restituito di ogni TypedResults helper è uno tra i IResult tipi di implementazione. La differenza significa che per Results helper è necessaria una conversione quando è necessario il tipo concreto, ad esempio per i test unitari. I tipi di implementazione sono definiti nello spazio dei nomi Microsoft.AspNetCore.Http.HttpResults.

La restituzione TypedResults anziché Results presenta i vantaggi seguenti:

  • TypedResults gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, la capacità di eseguire test unitari 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 extensions Produces. 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 su Results restituiscono IResult nella loro firma, il compilatore inferisce automaticamente quello come tipo restituito del delegato della richiesta quando vengono restituiti 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; quando è asincrono, è necessario il wrapper Task<>. L'uso di TypedResults è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo staticamente disponibili e quindi in grado di autodescriversi con 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 invece di IResult 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 a Results<>, risulta in 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 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 classi statiche Results e TypedResults. Restituire TypedResults è preferibile a restituire Results. Per altre informazioni, vedere TypedResults vs Results.

Le sezioni seguenti illustrano l'utilizzo delle funzioni di 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));

Errore interno del server

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

L'esempio precedente restituisce un codice di stato 500.

Problema e problema di convalida

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

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

Eventi Server-Sent (SSE)

L'API TypedResults.ServerSentEvents supporta la restituzione di un risultato serverSentEvents .

Server-Sent Eventi è una tecnologia push server che consente a un server di inviare un flusso di messaggi di evento a un client tramite una singola connessione HTTP. In .NET i messaggi di evento vengono rappresentati come SseItem<T> oggetti, che possono contenere un tipo di evento, un ID e un payload di dati di tipo T.

La classe TypedResults ha un metodo statico denominato ServerSentEvents che può essere usato per restituire un ServerSentEvents risultato. Il primo parametro di questo metodo è un oggetto IAsyncEnumerable<SseItem<T>> che rappresenta il flusso di messaggi di evento da inviare al client.

L'esempio seguente illustra come usare l'API TypedResults.ServerSentEvents per restituire un flusso di eventi di frequenza cardiaca come oggetti JSON al client:

app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});

Per altre informazioni, vedere l'app di esempio api minima che usa l'API TypedResults.ServerSentEvents per restituire un flusso di eventi di frequenza cardiaca come stringa, ServerSentEventse oggetti JSON al client.

Reindirizza

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

file

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

Interfacce HttpResult

Le seguenti interfacce nel namespace Microsoft.AspNetCore.Http consentono di rilevare il tipo IResult in fase di esecuzione, un pattern comune nelle implementazioni di filtri.

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 delle API minime e tipi di implementazione IResult.

Modifica intestazioni

Usare l'oggetto HttpResponse per modificare le intestazioni di risposta:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

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 IProducesResponseTypeMetadata che definisce il tipo di contenuto prodotto della risposta text/html e il codice di stato 200 OK.

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

    public int StatusCode => 200;

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

Un approccio alternativo consiste nell'usare il Microsoft.AspNetCore.Mvc.ProducesAttribute per 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 le opzioni di Web defaults 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 oggetto JsonSerializerOptions. L'esempio seguente usa questo overload per formattare l'output 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
// }

Risorse aggiuntive

Gli endpoint minimi supportano i tipi di valori restituiti seguenti:

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

string valori restituiti

Comportamento Tipo di Contenuto (Content-Type)
Il framework scrive la stringa direttamente nella risposta. text/plain

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

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

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

Hello World

T (Qualsiasi altro tipo) restituisce valori

Comportamento Tipo di Contenuto (Content-Type)
Il framework JSON serializza la risposta. application/json

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

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

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

{"message":"Hello World"}

IResult valori restituiti

Comportamento Tipo di Contenuto (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 vs 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 tipo restituito degli Results helper è IResult, mentre il tipo restituito di ogni TypedResults helper è uno tra i IResult tipi di implementazione. La differenza significa che per gli helper Results è necessaria una conversione quando è necessario il tipo concreto, ad esempio per i test unitari. I tipi di implementazione sono definiti nello spazio dei nomi Microsoft.AspNetCore.Http.HttpResults.

La restituzione TypedResults anziché Results presenta i vantaggi seguenti:

  • TypedResults gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, la capacità di eseguire test unitari 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 extensions Produces. 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 su Results restituiscono IResult nella loro firma, il compilatore inferisce automaticamente quello come tipo restituito del delegato della richiesta quando vengono restituiti 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; quando è asincrono, è necessario il wrapper Task<>. L'uso di TypedResults è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo staticamente disponibili e quindi in grado di autodescriversi con 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 invece di IResult 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 a Results<>, risulta in 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 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 classi statiche Results e TypedResults. Restituire TypedResults è preferibile a restituire Results. Per altre informazioni, vedere TypedResults vs Results.

Le sezioni seguenti illustrano l'utilizzo delle funzioni di 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));

Errore interno del server

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

L'esempio precedente restituisce un codice di stato 500.

Problema e problema di convalida

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Testo

app.MapGet("/text", () => Results.Text("This is some text"));

Streaming

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 seguenti interfacce nel namespace Microsoft.AspNetCore.Http consentono di rilevare il tipo IResult in fase di esecuzione, un pattern comune nelle implementazioni di filtri.

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 delle API minime e tipi di implementazione IResult.

Modifica intestazioni

Usare l'oggetto HttpResponse per modificare le intestazioni di risposta:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

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 IProducesResponseTypeMetadata che definisce il tipo di contenuto prodotto della risposta text/html e il codice di stato 200 OK.

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

    public int StatusCode => 200;

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

Un approccio alternativo consiste nell'usare il Microsoft.AspNetCore.Mvc.ProducesAttribute per 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 le opzioni di Web defaults 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 oggetto JsonSerializerOptions. L'esempio seguente usa questo overload per formattare l'output 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
// }

Risorse aggiuntive

Gli endpoint minimi supportano i tipi di valori restituiti seguenti:

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

string valori restituiti

Comportamento Tipo di Contenuto (Content-Type)
Il framework scrive la stringa direttamente nella risposta. text/plain

Si consideri il gestore del percorso seguente, che restituisce un Hello world testo.

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

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

Hello World

T (Qualsiasi altro tipo) restituisce valori

Comportamento Tipo di Contenuto (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 l'intestazione application/json Content-Type e il contenuto seguente.

{"message":"Hello World"}

IResult valori restituiti

Comportamento Tipo di Contenuto (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 contro 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 tipo restituito degli Results helper è IResult, mentre il tipo restituito di ogni TypedResults helper è uno tra i IResult tipi di implementazione. La differenza significa che per gli helper Results è necessaria una conversione quando il tipo concreto è richiesto, ad esempio per i test unitari. I tipi di implementazione sono definiti nello spazio dei nomi Microsoft.AspNetCore.Http.HttpResults.

La restituzione TypedResults anziché Results presenta i vantaggi seguenti:

  • TypedResults gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, la capacità di eseguire test unitari 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 extensions Produces. 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 su Results restituiscono IResult nella loro firma, il compilatore inferisce automaticamente quello come tipo restituito del delegato della richiesta quando vengono restituiti 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; quando è asincrono, è necessario il wrapper Task<>. L'uso di TypedResults è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo staticamente disponibili e quindi in grado di autodescriversi con 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 invece di IResult 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 a Results<>, risulta in 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 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 classi statiche Results e TypedResults. Restituire TypedResults è preferibile a restituire Results. Per altre informazioni, vedere TypedResults vs Results.

Le sezioni seguenti illustrano l'utilizzo delle funzioni di 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"));

Flusso

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

documento

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

Interfacce HttpResult

Le seguenti interfacce nel namespace Microsoft.AspNetCore.Http consentono di rilevare il tipo IResult in fase di esecuzione, un pattern comune nelle implementazioni di filtri.

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 delle 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 IProducesResponseTypeMetadata che definisce il tipo di contenuto prodotto della risposta text/html e il codice di stato 200 OK.

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

    public int StatusCode => 200;

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

Un approccio alternativo consiste nell'usare il Microsoft.AspNetCore.Mvc.ProducesAttribute per 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 le opzioni Web defaults 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 oggetto JsonSerializerOptions. L'esempio seguente usa questo overload per formattare l'output 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
// }

Risorse aggiuntive


Risorse aggiuntive