Antwoorden maken in minimale API-apps
Notitie
Dit is niet de nieuwste versie van dit artikel. Zie de .NET 9-versie van dit artikelvoor de huidige release.
Waarschuwing
Deze versie van ASP.NET Core wordt niet meer ondersteund. Zie de .NET- en .NET Core-ondersteuningsbeleidvoor meer informatie. Zie de .NET 9-versie van dit artikelvoor de huidige release.
Belangrijk
Deze informatie heeft betrekking op een pre-releaseproduct dat aanzienlijk kan worden gewijzigd voordat het commercieel wordt uitgebracht. Microsoft geeft geen garanties, uitdrukkelijk of impliciet, met betrekking tot de informatie die hier wordt verstrekt.
Zie de .NET 9-versie van dit artikelvoor de huidige release.
Minimale eindpunten ondersteunen de volgende typen retourwaarden:
-
string
- dit omvatTask<string>
enValueTask<string>
. -
T
(elk ander type): dit omvatTask<T>
enValueTask<T>
. -
IResult
-based - dit omvatTask<IResult>
enValueTask<IResult>
.
string
retourwaarden
Gedrag | Inhoudstype |
---|---|
Het framework schrijft de tekenreeks rechtstreeks naar het antwoord. | text/plain |
Houd rekening met de volgende route-handler, die een Hello world
tekst retourneert.
app.MapGet("/hello", () => "Hello World");
De 200
statuscode wordt geretourneerd met de text/plain
Content-Type-header en de volgende inhoud.
Hello World
T
(elk ander type) retourwaarden
Gedrag | Inhoudstype |
---|---|
Het framework JSON-serialiseert het antwoord. | application/json |
Houd rekening met de volgende route-handler, die een anoniem type retourneert dat een Message
tekenreekseigenschap bevat.
app.MapGet("/hello", () => new { Message = "Hello World" });
De 200
statuscode wordt geretourneerd met de application/json
Content-Type-header en de volgende inhoud.
{"message":"Hello World"}
IResult
retourwaarden
Gedrag | Inhoudstype |
---|---|
Het framework roept IResult.ExecuteAsyncaan. | Besloten door de implementatie van IResult . |
De IResult
-interface definieert een contract dat het resultaat van een HTTP-eindpunt vertegenwoordigt. De statische resultaten klasse en de statische TypedResults worden gebruikt om verschillende IResult
objecten te maken die verschillende typen antwoorden vertegenwoordigen.
TypedResults versus Results
De statische klassen Results en TypedResults bieden vergelijkbare sets resultatenhulpen. De TypedResults
klasse is de equivalent van de Results
klasse. Het retourtype van de Results
helpers is echter IResult, terwijl het retourtype van elke TypedResults
helper een van de IResult
implementatietypen is. Het verschil betekent dat voor Results
helpers een conversie nodig is wanneer het betontype nodig is, bijvoorbeeld voor het testen van eenheden. De implementatietypen worden gedefinieerd in de Microsoft.AspNetCore.Http.HttpResults naamruimte.
Het retourneren van TypedResults
in plaats van Results
heeft de volgende voordelen:
-
TypedResults
helpers retourneren sterk getypte objecten, wat kan bijdragen aan de verbetering van de leesbaarheid van code, het unit testen kan verbeteren en de kans op runtime-fouten kan verminderen. - Het implementatietype geeft automatisch de metagegevens van het antwoordtype voor OpenAPI- om het eindpunt te beschrijven.
Houd rekening met het volgende eindpunt, waarvoor een 200 OK
statuscode met het verwachte JSON-antwoord wordt geproduceerd.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Als u dit eindpunt correct wilt documenteren, wordt de uitbreidingsmethode Produces
aangeroepen. Het is echter niet nodig om Produces
aan te roepen als TypedResults
wordt gebruikt in plaats van Results
, zoals wordt weergegeven in de volgende code.
TypedResults
levert automatisch de metagegevens voor het eindpunt.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Zie OpenAPI-ondersteuning in minimale API'svoor meer informatie over het beschrijven van een antwoordtype.
Zoals eerder vermeld, is bij het gebruik van TypedResults
geen conversie nodig. Houd rekening met de volgende minimale API die een TypedResults
-klasse retourneert
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
De volgende test controleert op het volledige betontype:
[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);
});
}
Omdat alle methoden op Results
IResult
in hun signatuur retourneren, leidt de compiler dat automatisch af als het retourtype van de aanvraagafgevaardigde wanneer verschillende resultaten worden geretourneerd van één eindpunt.
TypedResults
vereist het gebruik van Results<T1, TN>
van dergelijke gemachtigden.
De volgende methode compileert correct omdat zowel Results.Ok
als Results.NotFound
zijn gedeclareerd om IResult
te retourneren, ook al zijn de daadwerkelijke concrete typen van de geretourneerde objecten verschillend.
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
De volgende methode wordt niet gecompileerd, omdat TypedResults.Ok
en TypedResults.NotFound
worden gedeclareerd als het retourneren van verschillende typen en de compiler niet probeert het beste overeenkomende type af te stellen:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Om TypedResults
te gebruiken, moet het retourtype volledig worden gedeclareerd, wat vereist dat bij asynchroon gebruik de Task<>
-wrapper nodig is. Het gebruik van TypedResults
is uitgebreider, maar dat is de afweging voor het statisch beschikbaar maken van de typegegevens en dus in staat om zichzelf te beschrijven voor 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());
Resultaten<TResult1, TResultN>
Gebruik Results<TResult1, TResultN>
als het retourtype eindpunthandler in plaats van IResult
wanneer:
- Er worden meerdere
IResult
implementatietypen geretourneerd door de eindpunthandler. - De klasse statische
TypedResult
wordt gebruikt om deIResult
-objecten te maken.
Dit alternatief is beter dan het retourneren van IResult
omdat de algemene samenvoegtypen automatisch de metagegevens van het eindpunt behouden. En omdat de Results<TResult1, TResultN>
samenvoegtypen impliciete cast-operators implementeren, kan de compiler automatisch de typen die in de algemene argumenten zijn opgegeven, converteren naar een exemplaar van het samenvoegtype.
Dit heeft het extra voordeel van het bieden van compileertijdcontrole dat een route-handler daadwerkelijk alleen de resultaten retourneert die het declareert. Als u probeert een type te retourneren dat niet is gedeclareerd als een van de algemene argumenten voor Results<>
resulteert in een compilatiefout.
Houd rekening met het volgende eindpunt, waarvoor een 400 BadRequest
statuscode wordt geretourneerd wanneer de orderId
groter is dan 999
. Anders produceert het een 200 OK
met de verwachte inhoud.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Als u dit eindpunt correct wilt documenteren, wordt de extensiemethode Produces
aangeroepen. Aangezien de TypedResults
helper echter automatisch de metagegevens voor het eindpunt bevat, kunt u in plaats daarvan het Results<T1, Tn>
samenvoegingstype retourneren, zoals wordt weergegeven in de volgende code.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Ingebouwde resultaten
Algemene helpers voor resultaten bestaan in de Results en TypedResults statische klassen. Het retourneren van TypedResults
heeft de voorkeur boven het retourneren van Results
. Voor meer informatie, zie TypedResults versus Results.
In de volgende secties ziet u het gebruik van de algemene helpers voor resultaten.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync is een alternatieve manier om JSON te retourneren:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Aangepaste statuscode
app.MapGet("/405", () => Results.StatusCode(405));
Interne serverfout
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
In het voorgaande voorbeeld wordt een statuscode van 500 geretourneerd.
Probleem en Validatieprobleem
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
Tekst
app.MapGet("/text", () => Results.Text("This is some text"));
Stroom
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
overloads maken toegang tot de onderliggende HTTP-antwoordstroom mogelijk zonder buffering. In het volgende voorbeeld wordt ImageSharp- gebruikt om een kleinere grootte van de opgegeven afbeelding te retourneren:
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);
}
In het volgende voorbeeld wordt een afbeelding uit Azure Blob Storage-gestreamd:
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");
});
In het volgende voorbeeld wordt een video van een Azure Blob gestreamd:
// 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);
});
Doorsturen
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Bestand
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult-interfaces
De volgende interfaces in de Microsoft.AspNetCore.Http naamruimte bieden een manier om het IResult
type tijdens runtime te detecteren. Dit is een algemeen patroon in filter-implementaties:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Hier volgt een voorbeeld van een filter dat gebruikmaakt van een van deze interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
Zie Filters in minimale API-apps en IResult-implementatietypenvoor meer informatie.
Kopteksten wijzigen
Gebruik het HttpResponse
-object om antwoordheaders te wijzigen:
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";
});
Antwoorden aanpassen
Toepassingen kunnen antwoorden beheren door een aangepast IResult type te implementeren. De volgende code is een voorbeeld van een HTML-resultaattype:
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);
}
}
U wordt aangeraden een extensiemethode toe te voegen aan Microsoft.AspNetCore.Http.IResultExtensions om deze aangepaste resultaten beter te detecteren.
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();
Bovendien kan een aangepast IResult
type een eigen aantekening bieden door de IEndpointMetadataProvider interface te implementeren. Met de volgende code wordt bijvoorbeeld een aantekening toegevoegd aan het voorgaande HtmlResult
type dat het antwoord beschrijft dat door het eindpunt wordt geproduceerd.
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());
}
}
De ProducesHtmlMetadata
is een implementatie van IProducesResponseTypeMetadata die het geproduceerde inhoudstype voor antwoorden definieert text/html
en de statuscode 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Een alternatieve benadering is het gebruik van de Microsoft.AspNetCore.Mvc.ProducesAttribute om het geproduceerde antwoord te beschrijven. Met de volgende code wordt de methode PopulateMetadata
gewijzigd om ProducesAttribute
te gebruiken.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Opties voor JSON-serialisatie configureren
Standaard gebruiken minimale API-apps Web defaults
opties tijdens JSON-serialisatie en deserialisatie.
JSON-serialisatieopties globaal configureren
Opties kunnen globaal worden geconfigureerd voor een app door ConfigureHttpJsonOptionsaan te roepen. Het volgende voorbeeld bevat openbare velden en indelingen voor JSON-uitvoer.
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
// }
Omdat velden zijn opgenomen, leest de voorgaande code NameField
en bevat deze in de uitvoer-JSON.
JSON-serialisatieopties configureren voor een eindpunt
Als u serialisatieopties voor een eindpunt wilt configureren, roept u Results.Json aan en geeft u dit door aan een JsonSerializerOptions-object, zoals wordt weergegeven in het volgende voorbeeld:
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
// }
Als alternatief kunt u een overload-functie van WriteAsJsonAsync gebruiken die een JsonSerializerOptions-object accepteert. In het volgende voorbeeld wordt deze overbelasting gebruikt om de uitvoer-JSON op te maken:
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
// }
Aanvullende informatiebronnen
Minimale eindpunten ondersteunen de volgende typen retourwaarden:
-
string
- dit omvatTask<string>
enValueTask<string>
. -
T
(elk ander type): dit omvatTask<T>
enValueTask<T>
. -
IResult
gebaseerd - dit omvatTask<IResult>
enValueTask<IResult>
.
string
retourwaarden
Gedrag | Inhoudstype |
---|---|
Het framework schrijft de tekenreeks rechtstreeks naar de respons. | text/plain |
Houd rekening met de volgende route-handler, die een Hello world
tekst retourneert.
app.MapGet("/hello", () => "Hello World");
De 200
-statuscode wordt geretourneerd met de text/plain
Content-Type-kop en de volgende inhoud.
Hello World
T
(elk ander type) retourwaarden
Gedrag | Inhoudstype |
---|---|
Het framework JSON-serialiseert het antwoord. | application/json |
Houd rekening met de volgende route-handler, die een anoniem type retourneert dat een Message
tekenreekseigenschap bevat.
app.MapGet("/hello", () => new { Message = "Hello World" });
De 200
-statuscode wordt samen met de application/json
Content-Type-header en de volgende inhoud geretourneerd.
{"message":"Hello World"}
IResult
terugkeerwaarden
Gedrag | Inhoudstype |
---|---|
Het framework roept IResult.ExecuteAsyncaan. | Besloten door de IResult implementatie. |
De IResult
-interface definieert een contract dat het resultaat van een HTTP-eindpunt vertegenwoordigt. De statische resultaten klasse en de statische TypedResults worden gebruikt om verschillende IResult
objecten te maken die verschillende typen antwoorden vertegenwoordigen.
TypedResults vs Results
De statische klassen Results en TypedResults bieden vergelijkbare sets resultatenhulpen. De TypedResults
klas is de getypte tegenhanger van de Results
klas. Het retourtype van de Results
helpers is echter IResult, terwijl het retourtype van elke TypedResults
helper een van de IResult
implementatietypen is. Het verschil betekent dat voor Results
helpers een conversie nodig is wanneer het concrete type nodig is, bijvoorbeeld voor unittesting. De implementatietypen worden gedefinieerd in de Microsoft.AspNetCore.Http.HttpResults naamruimte.
Het retourneren van TypedResults
in plaats van Results
heeft de volgende voordelen:
-
TypedResults
-helpers retourneren sterk getypte objecten, waardoor de leesbaarheid van de code, het testen van eenheden, en de kans op runtime-fouten kan worden verbeterd. - Het implementatietype geeft automatisch de metagegevens van het antwoordtype voor OpenAPI- om het eindpunt te beschrijven.
Houd rekening met het volgende eindpunt, waarvoor een 200 OK
statuscode met het verwachte JSON-antwoord wordt geproduceerd.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Als u dit eindpunt correct wilt documenteren, wordt de uitbreidingsmethode Produces
aangeroepen. Het is echter niet nodig om Produces
aan te roepen als TypedResults
wordt gebruikt in plaats van Results
, zoals wordt weergegeven in de volgende code.
TypedResults
levert automatisch de metagegevens voor het eindpunt.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Zie OpenAPI-ondersteuning in minimale API'svoor meer informatie over het beschrijven van een antwoordtype.
Zoals eerder vermeld, is bij het gebruik van TypedResults
geen conversie nodig. Houd rekening met de volgende minimale API die een TypedResults
-klasse retourneert
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
De volgende test controleert op het volledige betontype:
[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);
});
}
Omdat alle methoden op Results
IResult
retourneren in hun signatuur, leidt de compiler dat automatisch af als het aanvraagdelegaat retourtype voor verschillende resultaten van een enkel eindpunt.
TypedResults
vereist het gebruik van Results<T1, TN>
van dergelijke gemachtigden.
De volgende methode compileert omdat zowel Results.Ok
als Results.NotFound
worden gedeclareerd als retournerende IResult
, ook al zijn de werkelijke concrete typen van de geretourneerde objecten anders.
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
De volgende methode wordt niet gecompileerd, omdat TypedResults.Ok
en TypedResults.NotFound
worden gedeclareerd als het retourneren van verschillende typen en de compiler niet probeert het beste overeenkomende type af te stellen:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Om TypedResults
te gebruiken, moet het retourtype volledig worden gedeclareerd; als het proces asynchroon is, is de Task<>
-wrapper vereist. Het gebruik van TypedResults
is uitgesprokener, maar dat is de afweging voor het statisch beschikbaar maken van de type-informatie, en dus in staat om zelfbeschrijvend te zijn binnen 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());
Resultaten<TResult1, TResultN>
Gebruik Results<TResult1, TResultN>
als het retourtype eindpunthandler in plaats van IResult
wanneer:
- Er worden meerdere
IResult
implementatietypen geretourneerd door de eindpunthandler. - De klasse statische
TypedResult
wordt gebruikt om deIResult
-objecten te maken.
Dit alternatief is beter dan het retourneren van IResult
omdat de algemene samenvoegtypen automatisch de metagegevens van het eindpunt behouden. En omdat de Results<TResult1, TResultN>
samenvoegtypen impliciete cast-operators implementeren, kan de compiler automatisch de typen die in de algemene argumenten zijn opgegeven, converteren naar een exemplaar van het samenvoegtype.
Dit heeft het extra voordeel van het bieden van compileertijdcontrole dat een route-handler daadwerkelijk alleen de resultaten retourneert die het declareert. Als u probeert een type te retourneren dat niet is gedeclareerd als een van de algemene argumenten voor Results<>
resulteert in een compilatiefout.
Houd rekening met het volgende eindpunt, waarvoor een 400 BadRequest
statuscode wordt geretourneerd wanneer de orderId
groter is dan 999
. Anders produceert het een 200 OK
met de verwachte inhoud.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Als u dit eindpunt correct wilt documenteren, wordt de extensiemethode Produces
aangeroepen. Aangezien de TypedResults
helper echter automatisch de metagegevens voor het eindpunt bevat, kunt u in plaats daarvan het Results<T1, Tn>
samenvoegingstype retourneren, zoals wordt weergegeven in de volgende code.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Ingebouwde resultaten
Algemene helpers voor resultaten bestaan in de Results en TypedResults statische klassen. Het retourneren van TypedResults
heeft de voorkeur boven het retourneren van Results
. Voor meer informatie, zie TypedResults versus Results.
In de volgende secties ziet u het gebruik van de algemene helpers voor resultaten.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync is een alternatieve manier om JSON te retourneren:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Aangepaste statuscode
app.MapGet("/405", () => Results.StatusCode(405));
Tekst
app.MapGet("/text", () => Results.Text("This is some text"));
Stroom
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
overloads bieden toegang tot de onderliggende HTTP-antwoordstroom zonder buffering. In het volgende voorbeeld wordt ImageSharp- gebruikt om een kleinere grootte van de opgegeven afbeelding te retourneren:
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);
}
In het volgende voorbeeld wordt een afbeelding uit Azure Blob Storage-gestreamd:
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");
});
In het volgende voorbeeld wordt een video van een Azure Blob gestreamd:
// 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);
});
Omleiden
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Bestand
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResultinterfaces
De volgende interfaces in de Microsoft.AspNetCore.Http naamruimte bieden een manier om het IResult
type tijdens runtime te detecteren. Dit is een algemeen patroon in filter-implementaties:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Hier volgt een voorbeeld van een filter dat gebruikmaakt van een van deze interfaces:
app.MapGet("/weatherforecast", (int days) =>
{
if (days <= 0)
{
return Results.BadRequest();
}
var forecast = Enumerable.Range(1, days).Select(index =>
new WeatherForecast(DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), "Cool"))
.ToArray();
return Results.Ok(forecast);
}).
AddEndpointFilter(async (context, next) =>
{
var result = await next(context);
return result switch
{
IValueHttpResult<WeatherForecast[]> weatherForecastResult => new WeatherHttpResult(weatherForecastResult.Value),
_ => result
};
});
Zie Filters in minimale API-apps en IResult-implementatietypenvoor meer informatie.
Antwoorden aanpassen
Toepassingen kunnen antwoorden beheren door een aangepast IResult type te implementeren. De volgende code is een voorbeeld van een HTML-resultaattype:
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);
}
}
U wordt aangeraden een extensiemethode toe te voegen aan Microsoft.AspNetCore.Http.IResultExtensions om deze aangepaste resultaten beter te detecteren.
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();
Bovendien kan een aangepast IResult
type een eigen aantekening bieden door de IEndpointMetadataProvider interface te implementeren. Met de volgende code wordt bijvoorbeeld een aantekening toegevoegd aan het voorgaande HtmlResult
type dat het antwoord beschrijft dat door het eindpunt wordt geproduceerd.
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());
}
}
De ProducesHtmlMetadata
is een implementatie van IProducesResponseTypeMetadata die het geproduceerde inhoudstype voor antwoorden definieert text/html
en de statuscode 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Een alternatieve benadering is het gebruik van de Microsoft.AspNetCore.Mvc.ProducesAttribute om het geproduceerde antwoord te beschrijven. Met de volgende code wordt de methode PopulateMetadata
gewijzigd om ProducesAttribute
te gebruiken.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Opties voor JSON-serialisatie configureren
Standaard gebruiken minimale API-apps Web defaults
opties tijdens JSON-serialisatie en deserialisatie.
JSON-serialisatieopties globaal configureren
Opties kunnen globaal worden geconfigureerd voor een app door ConfigureHttpJsonOptionsaan te roepen. Het volgende voorbeeld bevat openbare velden en indelingen voor JSON-uitvoer.
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
// }
Omdat velden zijn opgenomen, leest de voorgaande code NameField
en bevat deze in de uitvoer-JSON.
JSON-serialisatieopties configureren voor een eindpunt
Als u serialisatieopties voor een eindpunt wilt configureren, roept u Results.Json aan en geeft u dit door aan een JsonSerializerOptions-object, zoals wordt weergegeven in het volgende voorbeeld:
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
// }
Als alternatief kunt u een overload van WriteAsJsonAsync gebruiken die een JsonSerializerOptions-object accepteert. In het volgende voorbeeld wordt deze overbelasting gebruikt om de uitvoer-JSON op te maken:
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
// }