Delen via


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:

  1. string - dit omvat Task<string> en ValueTask<string>.
  2. T (elk ander type): dit omvat Task<T> en ValueTask<T>.
  3. IResult-based - dit omvat Task<IResult> en ValueTask<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:

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 TypedResultsgeen 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 ResultsIResult 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 IResultte 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 TypedResultste 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 de IResult-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:

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 ProducesAttributete 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:

  1. string - dit omvat Task<string> en ValueTask<string>.
  2. T (elk ander type): dit omvat Task<T> en ValueTask<T>.
  3. IResult gebaseerd - dit omvat Task<IResult> en ValueTask<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:

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 TypedResultsgeen 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 ResultsIResult 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 TypedResultste 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 de IResult-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:

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 ProducesAttributete 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
// }

Aanvullende informatiebronnen