Come creare risposte in app per le API minime
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Avviso
Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere Criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Importante
Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.
Per la versione corrente, vedere la versione .NET 8 di questo articolo.
Gli endpoint minimi supportano i tipi di valori restituiti seguenti:
string
: includeTask<string>
eValueTask<string>
.T
(Qualsiasi altro tipo): includeTask<T>
eValueTask<T>
.IResult
based: includeTask<IResult>
eValueTask<IResult>
.
string
valori restituiti
Comportamento | Content-Type |
---|---|
Il framework scrive la stringa direttamente nella risposta. | text/plain |
Si consideri il gestore di route seguente, che restituisce un Hello world
testo.
app.MapGet("/hello", () => "Hello World");
Il 200
codice di stato viene restituito con text/plain
l'intestazione Content-Type e il contenuto seguente.
Hello World
T
(Qualsiasi altro tipo) restituisce valori
Comportamento | Content-Type |
---|---|
Il framework JSON serializza la risposta. | application/json |
Si consideri il gestore di route seguente, che restituisce un tipo anonimo contenente una Message
proprietà stringa.
app.MapGet("/hello", () => new { Message = "Hello World" });
Il 200
codice di stato viene restituito con application/json
l'intestazione Content-Type e il contenuto seguente.
{"message":"Hello World"}
IResult
valori restituiti
Comportamento | Content-Type |
---|---|
Il framework chiama IResult.ExecuteAsync. | Deciso dall'implementazione IResult . |
L'interfaccia IResult
definisce un contratto che rappresenta il risultato di un endpoint HTTP. La classe static Results e i TypedResult statici vengono usati per creare vari IResult
oggetti che rappresentano diversi tipi di risposte.
TypedResults e Risultati
Le Results classi statiche e TypedResults forniscono set simili di helper di risultati. La TypedResults
classe è l'equivalente tipizzato della Results
classe . Tuttavia, il Results
tipo restituito degli helper è IResult, mentre il tipo restituito di ogni TypedResults
helper è uno dei IResult
tipi di implementazione. La differenza significa che per Results
gli helper è necessaria una conversione quando è necessario il tipo concreto, ad esempio per unit test. I tipi di implementazione sono definiti nello spazio dei Microsoft.AspNetCore.Http.HttpResults nomi .
La restituzione TypedResults
anziché Results
presenta i vantaggi seguenti:
TypedResults
gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, unit test e ridurre la probabilità di errori di runtime.- Il tipo di implementazione fornisce automaticamente i metadati del tipo di risposta per OpenAPI per descrivere l'endpoint.
Si consideri l'endpoint seguente, per il quale viene generato un 200 OK
codice di stato con la risposta JSON prevista.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Per documentare correttamente questo endpoint, viene chiamato il metodo Produces
extensions. Tuttavia, non è necessario chiamare Produces
se TypedResults
viene usato invece di Results
, come illustrato nel codice seguente. TypedResults
fornisce automaticamente i metadati per l'endpoint.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Per altre informazioni sulla descrizione di un tipo di risposta, vedere Supporto OpenAPI nelle API minime.
Come accennato in precedenza, quando si usa TypedResults
, non è necessaria una conversione. Si consideri l'API minima seguente che restituisce una TypedResults
classe
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Il test seguente verifica la presenza del tipo concreto completo:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Poiché tutti i metodi sulla Results
restituzione IResult
nella firma, il compilatore deduce automaticamente che come tipo restituito dal delegato della richiesta quando restituisce risultati diversi da un singolo endpoint. TypedResults
richiede l'uso di Results<T1, TN>
da tali delegati.
Il metodo seguente viene compilato perché sia Results.Ok
che Results.NotFound
vengono dichiarati come restituiti IResult
, anche se i tipi concreti effettivi degli oggetti restituiti sono diversi:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Il metodo seguente non viene compilato perché TypedResults.Ok
e TypedResults.NotFound
vengono dichiarati come tipi diversi e il compilatore non tenterà di dedurre il tipo di corrispondenza migliore:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Per usare TypedResults
, il tipo restituito deve essere completamente dichiarato, che quando è necessario il Task<>
wrapper asincrono. L'uso TypedResults
è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo disponibili in modo statico e quindi in grado di autodescrittura in OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Risultati<TResult1, TResultN>
Usare Results<TResult1, TResultN>
come tipo restituito del gestore endpoint IResult
anziché quando:
- Dal gestore endpoint vengono restituiti più
IResult
tipi di implementazione. - La classe statica viene utilizzata
TypedResult
per creare gliIResult
oggetti .
Questa alternativa è migliore rispetto alla restituzione IResult
perché i tipi di unione generica mantengono automaticamente i metadati dell'endpoint. Poiché i Results<TResult1, TResultN>
tipi di unione implementano operatori cast impliciti, il compilatore può convertire automaticamente i tipi specificati negli argomenti generici in un'istanza del tipo di unione.
Questo ha il vantaggio aggiunto di fornire un controllo in fase di compilazione che un gestore di route restituisce effettivamente solo i risultati che dichiara. Se si tenta di restituire un tipo non dichiarato come uno degli argomenti generici, si Results<>
verifica un errore di compilazione.
Si consideri l'endpoint seguente, per il quale viene restituito un 400 BadRequest
codice di stato quando è orderId
maggiore di 999
. In caso contrario, produce un 200 OK
oggetto con il contenuto previsto.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Per documentare correttamente questo endpoint, viene chiamato il metodo Produces
di estensione. Tuttavia, poiché l'helper TypedResults
include automaticamente i metadati per l'endpoint, è possibile restituire invece il Results<T1, Tn>
tipo di unione, come illustrato nel codice seguente.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Risultati predefiniti
Gli helper di risultati comuni esistono nelle Results classi statiche e TypedResults . La restituzione di è preferibile TypedResults
Results
per restituire . Per altre informazioni, vedere TypedResults vs Results.
Le sezioni seguenti illustrano l'utilizzo degli helper dei risultati comuni.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync è un modo alternativo per restituire JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Codice di stato personalizzato
app.MapGet("/405", () => Results.StatusCode(405));
Internal Server Error
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
L'esempio precedente restituisce un codice di stato 500.
Testo
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
gli overload consentono l'accesso al flusso di risposta HTTP sottostante senza buffering. L'esempio seguente usa ImageSharp per restituire una dimensione ridotta dell'immagine specificata:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
L'esempio seguente trasmette un'immagine dall'archivio BLOB di Azure:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
L'esempio seguente trasmette un video da un BLOB di Azure:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Reindirizza
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
file
app.MapGet("/download", () => Results.File("myfile.text"));
Interfacce HttpResult
Le interfacce seguenti nello Microsoft.AspNetCore.Http spazio dei nomi consentono di rilevare il IResult
tipo in fase di esecuzione, un modello comune nelle implementazioni del filtro:
- 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 per le API minime e tipi di implementazione IResult.
Personalizzazione delle risposte
Le applicazioni possono controllare le risposte implementando un tipo personalizzato IResult . Il codice seguente è un esempio di tipo di risultato HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
È consigliabile aggiungere un metodo di estensione per Microsoft.AspNetCore.Http.IResultExtensions rendere questi risultati personalizzati più individuabili.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Inoltre, un tipo personalizzato IResult
può fornire una propria annotazione implementando l'interfaccia IEndpointMetadataProvider . Ad esempio, il codice seguente aggiunge un'annotazione al tipo precedente HtmlResult
che descrive la risposta prodotta dall'endpoint.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
è un'implementazione di che definisce il tipo di text/html
contenuto di IProducesResponseTypeMetadata risposta prodotto e il codice 200 OK
di stato .
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Un approccio alternativo consiste nell'usare per Microsoft.AspNetCore.Mvc.ProducesAttribute descrivere la risposta prodotta. Il codice seguente modifica il PopulateMetadata
metodo per usare ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configurare le opzioni di serializzazione JSON
Per impostazione predefinita, le app per le API minime usano Web defaults
le opzioni durante la serializzazione e la deserializzazione JSON.
Configurare le opzioni di serializzazione JSON a livello globale
Le opzioni possono essere configurate a livello globale per un'app richiamando ConfigureHttpJsonOptions. L'esempio seguente include campi pubblici e formatta l'output JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Poiché i campi sono inclusi, il codice precedente legge NameField
e lo include nel codice JSON di output.
Configurare le opzioni di serializzazione JSON per un endpoint
Per configurare le opzioni di serializzazione per un endpoint, richiamare Results.Json e passarvi un JsonSerializerOptions oggetto, come illustrato nell'esempio seguente:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
In alternativa, usare un overload di WriteAsJsonAsync che accetta un JsonSerializerOptions oggetto . L'esempio seguente usa questo overload per formattare il codice JSON di output:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Risorse aggiuntive
Gli endpoint minimi supportano i tipi di valori restituiti seguenti:
string
: includeTask<string>
eValueTask<string>
.T
(Qualsiasi altro tipo): includeTask<T>
eValueTask<T>
.IResult
based: includeTask<IResult>
eValueTask<IResult>
.
string
valori restituiti
Comportamento | Content-Type |
---|---|
Il framework scrive la stringa direttamente nella risposta. | text/plain |
Si consideri il gestore di route seguente, che restituisce un Hello world
testo.
app.MapGet("/hello", () => "Hello World");
Il 200
codice di stato viene restituito con text/plain
l'intestazione Content-Type e il contenuto seguente.
Hello World
T
(Qualsiasi altro tipo) restituisce valori
Comportamento | Content-Type |
---|---|
Il framework JSON serializza la risposta. | application/json |
Si consideri il gestore di route seguente, che restituisce un tipo anonimo contenente una Message
proprietà stringa.
app.MapGet("/hello", () => new { Message = "Hello World" });
Il 200
codice di stato viene restituito con application/json
l'intestazione Content-Type e il contenuto seguente.
{"message":"Hello World"}
IResult
valori restituiti
Comportamento | Content-Type |
---|---|
Il framework chiama IResult.ExecuteAsync. | Deciso dall'implementazione IResult . |
L'interfaccia IResult
definisce un contratto che rappresenta il risultato di un endpoint HTTP. La classe static Results e i TypedResult statici vengono usati per creare vari IResult
oggetti che rappresentano diversi tipi di risposte.
TypedResults e Risultati
Le Results classi statiche e TypedResults forniscono set simili di helper di risultati. La TypedResults
classe è l'equivalente tipizzato della Results
classe . Tuttavia, il Results
tipo restituito degli helper è IResult, mentre il tipo restituito di ogni TypedResults
helper è uno dei IResult
tipi di implementazione. La differenza significa che per Results
gli helper è necessaria una conversione quando è necessario il tipo concreto, ad esempio per unit test. I tipi di implementazione sono definiti nello spazio dei Microsoft.AspNetCore.Http.HttpResults nomi .
La restituzione TypedResults
anziché Results
presenta i vantaggi seguenti:
TypedResults
gli helper restituiscono oggetti fortemente tipizzati, che possono migliorare la leggibilità del codice, unit test e ridurre la probabilità di errori di runtime.- Il tipo di implementazione fornisce automaticamente i metadati del tipo di risposta per OpenAPI per descrivere l'endpoint.
Si consideri l'endpoint seguente, per il quale viene generato un 200 OK
codice di stato con la risposta JSON prevista.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Per documentare correttamente questo endpoint, viene chiamato il metodo Produces
extensions. Tuttavia, non è necessario chiamare Produces
se TypedResults
viene usato invece di Results
, come illustrato nel codice seguente. TypedResults
fornisce automaticamente i metadati per l'endpoint.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Per altre informazioni sulla descrizione di un tipo di risposta, vedere Supporto OpenAPI nelle API minime.
Come accennato in precedenza, quando si usa TypedResults
, non è necessaria una conversione. Si consideri l'API minima seguente che restituisce una TypedResults
classe
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Il test seguente verifica la presenza del tipo concreto completo:
[Fact]
public async Task GetAllReturnsTodosFromDatabase()
{
// Arrange
await using var context = new MockDb().CreateDbContext();
context.Todos.Add(new Todo
{
Id = 1,
Title = "Test title 1",
Description = "Test description 1",
IsDone = false
});
context.Todos.Add(new Todo
{
Id = 2,
Title = "Test title 2",
Description = "Test description 2",
IsDone = true
});
await context.SaveChangesAsync();
// Act
var result = await TodoEndpointsV1.GetAllTodos(context);
//Assert
Assert.IsType<Ok<Todo[]>>(result);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
Assert.Collection(result.Value, todo1 =>
{
Assert.Equal(1, todo1.Id);
Assert.Equal("Test title 1", todo1.Title);
Assert.False(todo1.IsDone);
}, todo2 =>
{
Assert.Equal(2, todo2.Id);
Assert.Equal("Test title 2", todo2.Title);
Assert.True(todo2.IsDone);
});
}
Poiché tutti i metodi sulla Results
restituzione IResult
nella firma, il compilatore deduce automaticamente che come tipo restituito dal delegato della richiesta quando restituisce risultati diversi da un singolo endpoint. TypedResults
richiede l'uso di Results<T1, TN>
da tali delegati.
Il metodo seguente viene compilato perché sia Results.Ok
che Results.NotFound
vengono dichiarati come restituiti IResult
, anche se i tipi concreti effettivi degli oggetti restituiti sono diversi:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Il metodo seguente non viene compilato perché TypedResults.Ok
e TypedResults.NotFound
vengono dichiarati come tipi diversi e il compilatore non tenterà di dedurre il tipo di corrispondenza migliore:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Per usare TypedResults
, il tipo restituito deve essere completamente dichiarato, che quando è necessario il Task<>
wrapper asincrono. L'uso TypedResults
è più dettagliato, ma questo è il compromesso per avere le informazioni sul tipo disponibili in modo statico e quindi in grado di autodescrittura in OpenAPI:
app.MapGet("/todoitems/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Risultati<TResult1, TResultN>
Usare Results<TResult1, TResultN>
come tipo restituito del gestore endpoint IResult
anziché quando:
- Dal gestore endpoint vengono restituiti più
IResult
tipi di implementazione. - La classe statica viene utilizzata
TypedResult
per creare gliIResult
oggetti .
Questa alternativa è migliore rispetto alla restituzione IResult
perché i tipi di unione generica mantengono automaticamente i metadati dell'endpoint. Poiché i Results<TResult1, TResultN>
tipi di unione implementano operatori cast impliciti, il compilatore può convertire automaticamente i tipi specificati negli argomenti generici in un'istanza del tipo di unione.
Questo ha il vantaggio aggiunto di fornire un controllo in fase di compilazione che un gestore di route restituisce effettivamente solo i risultati che dichiara. Se si tenta di restituire un tipo non dichiarato come uno degli argomenti generici, si Results<>
verifica un errore di compilazione.
Si consideri l'endpoint seguente, per il quale viene restituito un 400 BadRequest
codice di stato quando è orderId
maggiore di 999
. In caso contrario, produce un 200 OK
oggetto con il contenuto previsto.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Per documentare correttamente questo endpoint, viene chiamato il metodo Produces
di estensione. Tuttavia, poiché l'helper TypedResults
include automaticamente i metadati per l'endpoint, è possibile restituire invece il Results<T1, Tn>
tipo di unione, come illustrato nel codice seguente.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Risultati predefiniti
Gli helper di risultati comuni esistono nelle Results classi statiche e TypedResults . La restituzione di è preferibile TypedResults
Results
per restituire . Per altre informazioni, vedere TypedResults vs Results.
Le sezioni seguenti illustrano l'utilizzo degli helper dei risultati comuni.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync è un modo alternativo per restituire JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Codice di stato personalizzato
app.MapGet("/405", () => Results.StatusCode(405));
Testo
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream
gli overload consentono l'accesso al flusso di risposta HTTP sottostante senza buffering. L'esempio seguente usa ImageSharp per restituire una dimensione ridotta dell'immagine specificata:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
L'esempio seguente trasmette un'immagine dall'archivio BLOB di Azure:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
L'esempio seguente trasmette un video da un BLOB di Azure:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Reindirizza
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
file
app.MapGet("/download", () => Results.File("myfile.text"));
Interfacce HttpResult
Le interfacce seguenti nello Microsoft.AspNetCore.Http spazio dei nomi consentono di rilevare il IResult
tipo in fase di esecuzione, un modello comune nelle implementazioni del filtro:
- 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 per le API minime e tipi di implementazione IResult.
Personalizzazione delle risposte
Le applicazioni possono controllare le risposte implementando un tipo personalizzato IResult . Il codice seguente è un esempio di tipo di risultato HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
È consigliabile aggiungere un metodo di estensione per Microsoft.AspNetCore.Http.IResultExtensions rendere questi risultati personalizzati più individuabili.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Inoltre, un tipo personalizzato IResult
può fornire una propria annotazione implementando l'interfaccia IEndpointMetadataProvider . Ad esempio, il codice seguente aggiunge un'annotazione al tipo precedente HtmlResult
che descrive la risposta prodotta dall'endpoint.
class HtmlResult : IResult, IEndpointMetadataProvider
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesHtmlMetadata());
}
}
ProducesHtmlMetadata
è un'implementazione di che definisce il tipo di text/html
contenuto di IProducesResponseTypeMetadata risposta prodotto e il codice 200 OK
di stato .
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Un approccio alternativo consiste nell'usare per Microsoft.AspNetCore.Mvc.ProducesAttribute descrivere la risposta prodotta. Il codice seguente modifica il PopulateMetadata
metodo per usare ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Configurare le opzioni di serializzazione JSON
Per impostazione predefinita, le app per le API minime usano Web defaults
le opzioni durante la serializzazione e la deserializzazione JSON.
Configurare le opzioni di serializzazione JSON a livello globale
Le opzioni possono essere configurate a livello globale per un'app richiamando ConfigureHttpJsonOptions. L'esempio seguente include campi pubblici e formatta l'output JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Poiché i campi sono inclusi, il codice precedente legge NameField
e lo include nel codice JSON di output.
Configurare le opzioni di serializzazione JSON per un endpoint
Per configurare le opzioni di serializzazione per un endpoint, richiamare Results.Json e passarvi un JsonSerializerOptions oggetto, come illustrato nell'esempio seguente:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };
app.MapGet("/", () =>
Results.Json(new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
In alternativa, usare un overload di WriteAsJsonAsync che accetta un JsonSerializerOptions oggetto . L'esempio seguente usa questo overload per formattare il codice JSON di output:
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
WriteIndented = true };
app.MapGet("/", (HttpContext context) =>
context.Response.WriteAsJsonAsync<Todo>(
new Todo { Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }