So erstellen Sie Antworten in Minimal-API-Apps
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Minimale Endpunkte unterstützen die folgenden Typen von Rückgabewerten:
string
: Dies schließtTask<string>
undValueTask<string>
ein.T
(ein beliebiger weiterer Typ): Dies schließtTask<T>
undValueTask<T>
ein.IResult
-basiert: Dies schließtTask<IResult>
undValueTask<IResult>
ein.
string
-Rückgabewerte
Verhalten | Content-Type |
---|---|
Das Framework schreibt die Zeichenfolge direkt in die Antwort. | text/plain |
Betrachten Sie den folgenden Routenhandler, der den Text Hello world
zurückgibt.
app.MapGet("/hello", () => "Hello World");
Der Statuscode 200
wird mit dem Content-Type-Header text/plain
und dem folgenden Inhalt zurückgegeben.
Hello World
T
(beliebiger anderer Typ): Rückgabewerte
Verhalten | Inhaltsart |
---|---|
Das Framework JSON-serialisiert die Antwort. | application/json |
Betrachten Sie den folgenden Routenhandler, der einen anonymen Typ zurückgibt, der eine Message
-Zeichenfolgeneigenschaft enthält.
app.MapGet("/hello", () => new { Message = "Hello World" });
Der Statuscode 200
wird mit dem Content-Type-Header application/json
und dem folgenden Inhalt zurückgegeben.
{"message":"Hello World"}
IResult
-Rückgabewerte
Verhalten | Content-Type |
---|---|
Das Framework ruft IResult.ExecuteAsync auf. | Die Entscheidung richtet sich nach der IResult -Implementierung. |
Die IResult
-Schnittstelle definiert einen Vertrag, der das Ergebnis eines HTTP-Endpunkts darstellt. Die statische Results-Klasse und die statischen TypedResults werden verwendet, um verschiedene IResult
-Objekte zu erstellen, die unterschiedliche Antworttypen darstellen.
Vergleich von TypedResults und Results
Die statischen Klassen Results und TypedResults bieten ähnliche Ergebnishilfsprogramme. Die Klasse TypedResults
ist die typisierte Entsprechung der Klasse Results
. Der Rückgabetyp des Results
-Hilfsprogramms lautet IResult, während der Rückgabetyp jedes TypedResults
-Hilfsprogramms einer der IResult
-Implementierungstypen ist. Der Unterschied bedeutet, dass für Results
-Hilfsprogramme eine Konvertierung erforderlich ist, wenn der konkrete Typ benötigt wird, z. B. für Komponententests. Die Implementierungstypen werden im Microsoft.AspNetCore.Http.HttpResults-Namespace definiert.
Die Rückgabe von TypedResults
anstelle von Results
hat die folgenden Vorteile:
TypedResults
-Hilfsprogramme geben stark typisierte Objekte zurück, die die Lesbarkeit von Code sowie Komponententests verbessern und die Wahrscheinlichkeit von Laufzeitfehlern verringern können.- Der Implementierungstyp stellt automatisch die Metadaten des Antworttyps für OpenAPI bereit, mit denen der Endpunkt beschrieben wird.
Betrachten Sie den folgenden Endpunkt, für den der Statuscode 200 OK
mit der erwarteten JSON-Antwort erstellt wird.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Um diesen Endpunkt korrekt zu dokumentieren, wird die Erweiterungsmethode Produces
aufgerufen. Es ist jedoch nicht erforderlich Produces
aufzurufen, wenn TypedResults
anstelle von Results
verwendet wird, wie im folgenden Code gezeigt. TypedResults
stellt automatisch die Metadaten für den Endpunkt bereit.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Weitere Informationen zum Beschreiben eines Antworttyps finden Sie unter OpenAPI-Unterstützung in Minimal-APIs.
Wie bereits erwähnt, ist bei Verwendung von TypedResults
keine Konvertierung erforderlich. Sehen Sie sich die folgende Minimal-API an, die eine TypedResults
-Klasse zurückgibt:
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Der folgende Test überprüft den vollständigen konkreten Typ:
[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);
});
}
Da alle Methoden für Results
in der Signatur IResult
zurückgeben, leitet der Compiler automatisch dies als Anforderungsdelegat ab, wenn unterschiedliche Ergebnisse von einem einzelnen Endpunkt zurückgegeben werden. TypedResults
erfordert die Verwendung von Results<T1, TN>
von solchen Delegaten.
Die folgende Methode wird kompiliert, da sowohl Results.Ok
als auch Results.NotFound
so deklariert sind, dass sie IResult
zurückgeben, obwohl die tatsächlichen konkreten Typen der zurückgegebenen Objekte unterschiedlich sind:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Die folgende Methode wird nicht kompiliert, da TypedResults.Ok
und TypedResults.NotFound
so deklariert sind, dass sie unterschiedliche Typen zurückgeben und der Compiler nicht versucht, den besten übereinstimmenden Typ abzuleiten:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Um TypedResults
zu verwenden, muss der Rückgabetyp vollständig deklariert werden, was bei asynchronen Abfragen den Wrapper Task<>
erfordert. Die Verwendung von TypedResults
ist ausführlicher, das ist jedoch der Kompromiss dafür, dass die Typinformationen statisch verfügbar sind und somit in der Lage sind, sich selbst gegenüber OpenAPI zu beschreiben:
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());
Results<TResult1, TResultN>
Verwenden Sie Results<TResult1, TResultN>
anstelle von IResult
als Rückgabetyp des Endpunkthandlers, wenn Folgendes gilt:
- Vom Endpunkthandler werden mehrere
IResult
-Implementierungstypen zurückgegeben. - Zum Erstellen der
IResult
-Objekte wird die statischeTypedResult
-Klasse verwendet.
Diese Alternative eignet sich besser als die Rückgabe von IResult
, da die generischen Union-Typen automatisch die Metadaten des Endpunkts beibehalten. Da die Union-Typen von Results<TResult1, TResultN>
implizite Umwandlungsoperatoren implementieren, kann der Compiler die in den generischen Argumenten angegebenen Typen automatisch in eine Instanz des Union-Typs konvertieren.
Dies hat den zusätzlichen Vorteil, dass zur Kompilierungszeit überprüft wird, ob ein Routenhandler tatsächlich nur die Ergebnisse zurückgibt, die er deklariert. Der Versuch, einen Typ zurückzugeben, der nicht als eines der generischen Argumente für Results<>
deklariert wurde, führt zu einem Kompilierungsfehler.
Betrachten Sie den folgenden Endpunkt, für den der Statuscode 400 BadRequest
zurückgegeben wird, wenn die orderId
größer als 999
ist. Andernfalls generiert er den Statuscode 200 OK
mit dem erwarteten Inhalt.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Um diesen Endpunkt korrekt zu dokumentieren, wird die Erweiterungsmethode Produces
aufgerufen. Da das TypedResults
-Hilfsprogramm jedoch automatisch die Metadaten für den Endpunkt enthält, können Sie stattdessen den Union-Typ Results<T1, Tn>
zurückgeben, wie im folgenden Code gezeigt.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Integrierte Ergebnisse
Die statischen Klassen Results und TypedResults enthalten allgemeine Ergebnishilfen. Die Rückgabe von TypedResults
wird der Rückgabe von Results
vorgezogen. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
In den folgenden Abschnitten wird die Verwendung der allgemeinen Ergebnishilfsprogramme veranschaulicht.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync ist eine alternative Möglichkeit, JSON zurückzugeben:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Benutzerdefinierter Statuscode
app.MapGet("/405", () => Results.StatusCode(405));
Interner Serverfehler
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
Im vorherigen Beispiel wird ein Statuscode von 500 zurückgegeben.
Text
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
-Überladungen ermöglichen den Zugriff auf den zugrunde liegenden HTTP-Antwortdatenstrom ohne Pufferung. Im folgenden Beispiel wird ImageSharp verwendet, um eine reduzierte Größe des angegebenen Bilds zurückzugeben:
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);
}
Im folgenden Beispiel wird ein Bild aus Azure Blob Storage gestreamt:
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");
});
Im folgenden Beispiel wird ein Video aus einem Azure-Blob gestreamt:
// 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);
});
Umleiten
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Datei
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult-Schnittstellen
Die folgenden Schnittstellen im Microsoft.AspNetCore.Http-Namespace bieten eine Möglichkeit, den IResult
-Typ zur Laufzeit zu erkennen. Dies ist ein häufiges Muster in Filterimplementierungen:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Hier sehen Sie ein Beispiel für einen Filter, der eine dieser Schnittstellen verwendet:
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
};
});
Weitere Informationen finden Sie unter Filter in Minimal-API-Apps und IResult-Implementierungstypen.
Anpassen der Antworten
Anwendungen können Antworten steuern, indem sie einen benutzerdefinierten IResult-Typ implementieren. Der folgende Code ist ein Beispiel für einen HTML-Ergebnistyp:
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);
}
}
Es wird empfohlen, Microsoft.AspNetCore.Http.IResultExtensions eine Erweiterungsmethode hinzuzufügen, um diese benutzerdefinierten Ergebnisse leichter auffindbar zu machen.
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();
Außerdem kann ein benutzerdefinierter IResult
-Typ eine eigene Anmerkung bereitstellen, indem die IEndpointMetadataProvider-Schnittstelle implementiert wird. Der folgende Code fügt dem vorherigen HtmlResult
-Typ beispielsweise eine Anmerkung hinzu, mit der die vom Endpunkt erstellte Antwort beschrieben wird.
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
ist hierbei eine Implementierung von IProducesResponseTypeMetadata, die den erstellten Inhaltstyps text/html
der Antwort und den Statuscode 200 OK
definiert.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternativer können Sie das Microsoft.AspNetCore.Mvc.ProducesAttribute verwenden, um die generierte Antwort zu beschreiben. Der folgende Code ändert die PopulateMetadata
-Methode so, dass ProducesAttribute
verwendet wird.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurieren von JSON-Serialisierungsoptionen
Standardmäßig verwenden minimale API-Apps Web defaults
-Optionen während der JSON-Serialisierung und -Deserialisierung.
Globales Konfigurieren von JSON-Serialisierungsoptionen
Optionen können global für eine App konfiguriert werden, indem Sie ConfigureHttpJsonOptions aufrufen. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da Felder enthalten sind, liest der vorherige Code das NameField
-Objekt und schließt es in der JSON-Ausgabe ein.
Konfigurieren von JSON-Serialisierungsoptionen für einen Endpunkt
Rufen Sie zum Konfigurieren von Serialisierungsoptionen für einen Endpunkt eine Results.Json-Methode auf, und übergeben Sie sie wie im folgenden Beispiel gezeigt an das JsonSerializerOptions-Objekt:
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
// }
Verwenden Sie alternativ eine Überladung von WriteAsJsonAsync, die ein JsonSerializerOptions-Objekt akzeptiert. Im folgenden Beispiel wird diese Überladung verwendet, um die JSON-Ausgabe zu formatieren:
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
// }
Weitere Ressourcen
Minimale Endpunkte unterstützen die folgenden Typen von Rückgabewerten:
string
: Dies schließtTask<string>
undValueTask<string>
ein.T
(ein beliebiger weiterer Typ): Dies schließtTask<T>
undValueTask<T>
ein.IResult
-basiert: Dies schließtTask<IResult>
undValueTask<IResult>
ein.
string
-Rückgabewerte
Verhalten | Content-Type |
---|---|
Das Framework schreibt die Zeichenfolge direkt in die Antwort. | text/plain |
Betrachten Sie den folgenden Routenhandler, der den Text Hello world
zurückgibt.
app.MapGet("/hello", () => "Hello World");
Der Statuscode 200
wird mit dem Content-Type-Header text/plain
und dem folgenden Inhalt zurückgegeben.
Hello World
T
(beliebiger anderer Typ): Rückgabewerte
Verhalten | Inhaltsart |
---|---|
Das Framework JSON-serialisiert die Antwort. | application/json |
Betrachten Sie den folgenden Routenhandler, der einen anonymen Typ zurückgibt, der eine Message
-Zeichenfolgeneigenschaft enthält.
app.MapGet("/hello", () => new { Message = "Hello World" });
Der Statuscode 200
wird mit dem Content-Type-Header application/json
und dem folgenden Inhalt zurückgegeben.
{"message":"Hello World"}
IResult
-Rückgabewerte
Verhalten | Content-Type |
---|---|
Das Framework ruft IResult.ExecuteAsync auf. | Die Entscheidung richtet sich nach der IResult -Implementierung. |
Die IResult
-Schnittstelle definiert einen Vertrag, der das Ergebnis eines HTTP-Endpunkts darstellt. Die statische Results-Klasse und die statischen TypedResults werden verwendet, um verschiedene IResult
-Objekte zu erstellen, die unterschiedliche Antworttypen darstellen.
Vergleich von TypedResults und Results
Die statischen Klassen Results und TypedResults bieten ähnliche Ergebnishilfsprogramme. Die Klasse TypedResults
ist die typisierte Entsprechung der Klasse Results
. Der Rückgabetyp des Results
-Hilfsprogramms lautet IResult, während der Rückgabetyp jedes TypedResults
-Hilfsprogramms einer der IResult
-Implementierungstypen ist. Der Unterschied bedeutet, dass für Results
-Hilfsprogramme eine Konvertierung erforderlich ist, wenn der konkrete Typ benötigt wird, z. B. für Komponententests. Die Implementierungstypen werden im Microsoft.AspNetCore.Http.HttpResults-Namespace definiert.
Die Rückgabe von TypedResults
anstelle von Results
hat die folgenden Vorteile:
TypedResults
-Hilfsprogramme geben stark typisierte Objekte zurück, die die Lesbarkeit von Code sowie Komponententests verbessern und die Wahrscheinlichkeit von Laufzeitfehlern verringern können.- Der Implementierungstyp stellt automatisch die Metadaten des Antworttyps für OpenAPI bereit, mit denen der Endpunkt beschrieben wird.
Betrachten Sie den folgenden Endpunkt, für den der Statuscode 200 OK
mit der erwarteten JSON-Antwort erstellt wird.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Um diesen Endpunkt korrekt zu dokumentieren, wird die Erweiterungsmethode Produces
aufgerufen. Es ist jedoch nicht erforderlich Produces
aufzurufen, wenn TypedResults
anstelle von Results
verwendet wird, wie im folgenden Code gezeigt. TypedResults
stellt automatisch die Metadaten für den Endpunkt bereit.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Weitere Informationen zum Beschreiben eines Antworttyps finden Sie unter OpenAPI-Unterstützung in Minimal-APIs.
Wie bereits erwähnt, ist bei Verwendung von TypedResults
keine Konvertierung erforderlich. Sehen Sie sich die folgende Minimal-API an, die eine TypedResults
-Klasse zurückgibt:
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Der folgende Test überprüft den vollständigen konkreten Typ:
[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);
});
}
Da alle Methoden für Results
in der Signatur IResult
zurückgeben, leitet der Compiler automatisch dies als Anforderungsdelegat ab, wenn unterschiedliche Ergebnisse von einem einzelnen Endpunkt zurückgegeben werden. TypedResults
erfordert die Verwendung von Results<T1, TN>
von solchen Delegaten.
Die folgende Methode wird kompiliert, da sowohl Results.Ok
als auch Results.NotFound
so deklariert sind, dass sie IResult
zurückgeben, obwohl die tatsächlichen konkreten Typen der zurückgegebenen Objekte unterschiedlich sind:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Die folgende Methode wird nicht kompiliert, da TypedResults.Ok
und TypedResults.NotFound
so deklariert sind, dass sie unterschiedliche Typen zurückgeben und der Compiler nicht versucht, den besten übereinstimmenden Typ abzuleiten:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Um TypedResults
zu verwenden, muss der Rückgabetyp vollständig deklariert werden, was bei asynchronen Abfragen den Wrapper Task<>
erfordert. Die Verwendung von TypedResults
ist ausführlicher, das ist jedoch der Kompromiss dafür, dass die Typinformationen statisch verfügbar sind und somit in der Lage sind, sich selbst gegenüber OpenAPI zu beschreiben:
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());
Results<TResult1, TResultN>
Verwenden Sie Results<TResult1, TResultN>
anstelle von IResult
als Rückgabetyp des Endpunkthandlers, wenn Folgendes gilt:
- Vom Endpunkthandler werden mehrere
IResult
-Implementierungstypen zurückgegeben. - Zum Erstellen der
IResult
-Objekte wird die statischeTypedResult
-Klasse verwendet.
Diese Alternative eignet sich besser als die Rückgabe von IResult
, da die generischen Union-Typen automatisch die Metadaten des Endpunkts beibehalten. Da die Union-Typen von Results<TResult1, TResultN>
implizite Umwandlungsoperatoren implementieren, kann der Compiler die in den generischen Argumenten angegebenen Typen automatisch in eine Instanz des Union-Typs konvertieren.
Dies hat den zusätzlichen Vorteil, dass zur Kompilierungszeit überprüft wird, ob ein Routenhandler tatsächlich nur die Ergebnisse zurückgibt, die er deklariert. Der Versuch, einen Typ zurückzugeben, der nicht als eines der generischen Argumente für Results<>
deklariert wurde, führt zu einem Kompilierungsfehler.
Betrachten Sie den folgenden Endpunkt, für den der Statuscode 400 BadRequest
zurückgegeben wird, wenn die orderId
größer als 999
ist. Andernfalls generiert er den Statuscode 200 OK
mit dem erwarteten Inhalt.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Um diesen Endpunkt korrekt zu dokumentieren, wird die Erweiterungsmethode Produces
aufgerufen. Da das TypedResults
-Hilfsprogramm jedoch automatisch die Metadaten für den Endpunkt enthält, können Sie stattdessen den Union-Typ Results<T1, Tn>
zurückgeben, wie im folgenden Code gezeigt.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Integrierte Ergebnisse
Die statischen Klassen Results und TypedResults enthalten allgemeine Ergebnishilfen. Die Rückgabe von TypedResults
wird der Rückgabe von Results
vorgezogen. Weitere Informationen finden Sie unter Vergleich von TypedResults und Results.
In den folgenden Abschnitten wird die Verwendung der allgemeinen Ergebnishilfsprogramme veranschaulicht.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync ist eine alternative Möglichkeit, JSON zurückzugeben:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Benutzerdefinierter Statuscode
app.MapGet("/405", () => Results.StatusCode(405));
Text
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
-Überladungen ermöglichen den Zugriff auf den zugrunde liegenden HTTP-Antwortdatenstrom ohne Pufferung. Im folgenden Beispiel wird ImageSharp verwendet, um eine reduzierte Größe des angegebenen Bilds zurückzugeben:
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);
}
Im folgenden Beispiel wird ein Bild aus Azure Blob Storage gestreamt:
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");
});
Im folgenden Beispiel wird ein Video aus einem Azure-Blob gestreamt:
// 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);
});
Umleiten
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Datei
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult-Schnittstellen
Die folgenden Schnittstellen im Microsoft.AspNetCore.Http-Namespace bieten eine Möglichkeit, den IResult
-Typ zur Laufzeit zu erkennen. Dies ist ein häufiges Muster in Filterimplementierungen:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Hier sehen Sie ein Beispiel für einen Filter, der eine dieser Schnittstellen verwendet:
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
};
});
Weitere Informationen finden Sie unter Filter in Minimal-API-Apps und IResult-Implementierungstypen.
Anpassen der Antworten
Anwendungen können Antworten steuern, indem sie einen benutzerdefinierten IResult-Typ implementieren. Der folgende Code ist ein Beispiel für einen HTML-Ergebnistyp:
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);
}
}
Es wird empfohlen, Microsoft.AspNetCore.Http.IResultExtensions eine Erweiterungsmethode hinzuzufügen, um diese benutzerdefinierten Ergebnisse leichter auffindbar zu machen.
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();
Außerdem kann ein benutzerdefinierter IResult
-Typ eine eigene Anmerkung bereitstellen, indem die IEndpointMetadataProvider-Schnittstelle implementiert wird. Der folgende Code fügt dem vorherigen HtmlResult
-Typ beispielsweise eine Anmerkung hinzu, mit der die vom Endpunkt erstellte Antwort beschrieben wird.
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
ist hierbei eine Implementierung von IProducesResponseTypeMetadata, die den erstellten Inhaltstyps text/html
der Antwort und den Statuscode 200 OK
definiert.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Alternativer können Sie das Microsoft.AspNetCore.Mvc.ProducesAttribute verwenden, um die generierte Antwort zu beschreiben. Der folgende Code ändert die PopulateMetadata
-Methode so, dass ProducesAttribute
verwendet wird.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Konfigurieren von JSON-Serialisierungsoptionen
Standardmäßig verwenden minimale API-Apps Web defaults
-Optionen während der JSON-Serialisierung und -Deserialisierung.
Globales Konfigurieren von JSON-Serialisierungsoptionen
Optionen können global für eine App konfiguriert werden, indem Sie ConfigureHttpJsonOptions aufrufen. Das folgende Beispiel enthält öffentliche Felder und Formate der JSON-Ausgabe.
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
// }
Da Felder enthalten sind, liest der vorherige Code das NameField
-Objekt und schließt es in der JSON-Ausgabe ein.
Konfigurieren von JSON-Serialisierungsoptionen für einen Endpunkt
Rufen Sie zum Konfigurieren von Serialisierungsoptionen für einen Endpunkt eine Results.Json-Methode auf, und übergeben Sie sie wie im folgenden Beispiel gezeigt an das JsonSerializerOptions-Objekt:
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
// }
Verwenden Sie alternativ eine Überladung von WriteAsJsonAsync, die ein JsonSerializerOptions-Objekt akzeptiert. Im folgenden Beispiel wird diese Überladung verwendet, um die JSON-Ausgabe zu formatieren:
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
// }