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:

  1. string: Dies schließt Task<string> und ValueTask<string> ein.
  2. T (ein beliebiger weiterer Typ): Dies schließt Task<T> und ValueTask<T> ein.
  3. IResult-basiert: Dies schließt Task<IResult> und ValueTask<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:

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 statische TypedResult-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:

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:

  1. string: Dies schließt Task<string> und ValueTask<string> ein.
  2. T (ein beliebiger weiterer Typ): Dies schließt Task<T> und ValueTask<T> ein.
  3. IResult-basiert: Dies schließt Task<IResult> und ValueTask<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:

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 statische TypedResult-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:

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