Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 10 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.
Questo articolo illustra come creare risposte per endpoint API minimi in ASP.NET Core. Le API minime offrono diversi modi per restituire dati e codici di stato HTTP.
Gli endpoint minimi supportano i tipi di valori restituiti seguenti:
-
string- Questo includeTask<string>eValueTask<string>. -
T(Qualsiasi altro tipo): includeTask<T>eValueTask<T>. -
IResultbased: Questo includeTask<IResult>eValueTask<IResult>.
Importante
A partire da ASP.NET Core 10, gli endpoint API noti non vengono più reindirizzi alle pagine di accesso quando si usa cookie l'autenticazione. Restituiscono invece codici di stato 401/403. Per informazioni dettagliate, vedere Comportamento di autenticazione dell'endpoint API in ASP.NET Core.
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:
-
TypedResultsgli 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.
Per esempi sui tipi di risultati di test, vedere la documentazione test.
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 il metodo è asincrono, la dichiarazione richiede il wrapping del tipo restituito in un oggetto 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ù
IResulttipi di implementazione. - La classe statica viene utilizzata
TypedResultper creare gliIResultoggetti .
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 (JavaScript Object Notation)
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);
});
Personalizzare le risposte agli errori di convalida usando IProblemDetailsService
Personalizzare le risposte di errore dalla logica di convalida minima dell'API con un'implementazione IProblemDetailsService . Registrare questo servizio nella raccolta di servizi dell'applicazione per abilitare risposte di errore più coerenti e specifiche dell'utente. Il supporto per la convalida minima dell'API è stato introdotto in ASP.NET Core in .NET 10.
Per implementare risposte di errore di convalida personalizzate:
- Implementare o usare l'implementazione IProblemDetailsService predefinita
- Registrare il servizio nel contenitore di inserimento delle dipendenze
- Il sistema di convalida usa automaticamente il servizio registrato per formattare le risposte agli errori di convalida
L'esempio seguente illustra come registrare e configurare per personalizzare le IProblemDetailsService risposte agli errori di convalida:
using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
if (context.ProblemDetails.Status == 400)
{
context.ProblemDetails.Title = "Validation error occurred";
context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
}
};
});
Quando si verifica un errore di convalida, IProblemDetailsService verrà usato per generare la risposta di errore, incluse eventuali personalizzazioni aggiunte nel CustomizeProblemDetails callback.
Per un esempio completo di app, vedere l'app di esempio api minima che illustra come personalizzare le IProblemDetailsService risposte agli errori di convalida usando in ASP.NET API minime core.
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);
});
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.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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:
-
string- Questo includeTask<string>eValueTask<string>. -
T(Qualsiasi altro tipo): includeTask<T>eValueTask<T>. -
IResultbased: Questo includeTask<IResult>eValueTask<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:
-
TypedResultsgli 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ù
IResulttipi di implementazione. - La classe statica viene utilizzata
TypedResultper creare gliIResultoggetti .
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 (JavaScript Object Notation)
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"));
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"));
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.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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:
-
string- Questo includeTask<string>eValueTask<string>. -
T(Qualsiasi altro tipo): includeTask<T>eValueTask<T>. -
IResultbased: Questo includeTask<IResult>eValueTask<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:
-
TypedResultsgli 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ù
IResulttipi di implementazione. - La classe statica viene utilizzata
TypedResultper creare gliIResultoggetti .
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 (JavaScript Object Notation)
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"));
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.
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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 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
// }