Dela via


Så här skapar du svar i minimala API-appar

Notera

Det här är inte den senaste versionen av den här artikeln. För den aktuella utgåvan, se version .NET 9 av den här artikeln.

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i .NET och .NET Core Support Policy. För den aktuella utgåvan, se .NET 9-versionen av den här artikeln.

Viktig

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Minimala slutpunkter stöder följande typer av returvärden:

  1. string – Detta inkluderar Task<string> och ValueTask<string>.
  2. T (alla andra typer) – Detta inkluderar Task<T> och ValueTask<T>.
  3. IResult baserad – Detta inkluderar Task<IResult> och ValueTask<IResult>.

string Returnera värden

Uppförande Innehållstyp
Ramverket skriver strängen direkt till svaret. text/plain

Överväg följande routningshanterare, som returnerar en Hello world text.

app.MapGet("/hello", () => "Hello World");

Statuskoden 200 returneras med text/plain innehållstypsrubrik och följande innehåll.

Hello World

T (alla andra typer) returnerar värden

Uppförande Innehållstyp
Ramverket JSON-serialiserar svaret. application/json

Överväg följande routningshanterare, som returnerar en anonym typ som innehåller en Message strängegenskap.

app.MapGet("/hello", () => new { Message = "Hello World" });

Statuskoden 200 returneras med application/json innehållstypsrubrik och följande innehåll.

{"message":"Hello World"}

IResult returnera värden

Uppförande Innehållstyp
Ramverket anropar IResult.ExecuteAsync. Bestäms av IResult implementeringen.

Gränssnittet IResult definierar ett kontrakt som representerar resultatet av en HTTP-slutpunkt. Den statiska Results-klassen och den statiska TypedResults- används för att skapa olika IResult objekt som representerar olika typer av svar.

TypedResults vs Resultat

De Results- och TypedResults statiska klasserna ger liknande uppsättningar resultathjälpare. Klassen TypedResults är den av typen motsvarigheten till klassen Results. Results-hjälpens returtyp är dock IResult, medan varje TypedResults-hjälpens returtyp är en av de IResult implementeringstyperna. Skillnaden innebär att en konvertering krävs för Results hjälpverktyg när den konkreta typen behövs, till exempel för enhetstestning. Implementeringstyperna definieras i namnområdet Microsoft.AspNetCore.Http.HttpResults.

Att returnera TypedResults i stället för Results har följande fördelar:

Överväg följande slutpunkt, för vilken en 200 OK statuskod med det förväntade JSON-svaret skapas.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

För att kunna dokumentera den här slutpunkten korrekt anropas tilläggsmetoden Produces. Det är dock inte nödvändigt att anropa Produces om TypedResults används i stället för Results, enligt följande kod. TypedResults tillhandahåller automatiskt metadata för slutpunkten.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Mer information om hur du beskriver en svarstyp finns i OpenAPI-stöd i minimala API:er.

Som tidigare nämnts behövs ingen konvertering när du använder TypedResults. Överväg följande minimala API som returnerar en TypedResults-klass

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

Följande test kontrollerar den fullständiga betongtypen:

[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);
    });
}

Eftersom alla metoder på Results returnerar IResult i sina signaturer, drar kompilatorn automatiskt slutsatsen att detta ska vara returneringstypen för begärandedelegaten när olika resultat returneras från en enda slutpunkt. TypedResults kräver att Results<T1, TN> används av sådana ombud.

Följande metod kompileras eftersom både Results.Ok och Results.NotFound deklareras som returvärden av IResult, även om de konkreta typerna för de returnerade objekten är olika:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

Följande metod kompileras inte eftersom TypedResults.Ok och TypedResults.NotFound deklareras som returnerade olika typer och kompilatorn inte försöker härleda den bästa matchningstypen:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

För att använda TypedResultsmåste returtypen vara helt deklarerad, vilket när den används asynkront kräver wrappern Task<>. Att använda TypedResults är mer utförligt, men det är kompromissen för att typinformationen ska vara statiskt tillgänglig och därmed kunna självbeskriva till 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());

Resultat<TResult1, TResultN>

Använd Results<TResult1, TResultN> som slutpunktshanterarens returtyp i stället för IResult när:

  • Flera IResult implementeringstyper returneras från slutpunktshanteraren.
  • Den statiska TypedResult-klassen används för att skapa IResult objekt.

Det här alternativet är bättre än att returnera IResult eftersom de allmänna unionstyperna automatiskt behåller slutpunktsmetadata. Och eftersom Results<TResult1, TResultN>-unionstyperna implementerar implicita överföringsoperatorer kan kompilatorn automatiskt konvertera de typer som anges i de generiska argumenten till en instans av uniontypen.

Detta har den extra fördelen med att tillhandahålla kompileringstidskontroll att en routningshanterare faktiskt bara returnerar de resultat som den deklarerar att den gör. Försök att returnera en typ som inte deklareras som ett av de allmänna argumenten för att Results<> resulterar i ett kompileringsfel.

Tänk på följande slutpunkt, för vilken en 400 BadRequest statuskod returneras när orderId är större än 999. Annars producerar den en 200 OK med det förväntade innehållet.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

För att kunna dokumentera den här slutpunkten korrekt anropas tilläggsmetoden Produces. Men, eftersom TypedResults-hjälparskriptet automatiskt innehåller metadata för slutpunkten, kan du i stället returnera Results<T1, Tn>-unionstypen, som visas i följande kod.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Inbyggda resultat

Vanliga resultathjälpare finns i Results- och TypedResults statiska klasser. Att returnera TypedResults föredras framför att returnera Results. Mer information finns i TypedResults vs Results.

Följande avsnitt visar användningen av vanliga resultathjälpare.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync är ett alternativt sätt att returnera JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Anpassad statuskod

app.MapGet("/405", () => Results.StatusCode(405));

Internt serverfel

app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));

Föregående exempel returnerar en statuskod på 500.

Problem och valideringsproblem

app.MapGet("/problem", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions", 
                                                extensions: extensions);
});

Text

app.MapGet("/text", () => Results.Text("This is some text"));

Ström

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 överlagringar ger åtkomst till den underliggande HTTP-svarsströmmen utan buffring. I följande exempel används ImageSharp- för att returnera en reducerad storlek på den angivna bilden:

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);
}

I följande exempel strömmas en avbildning från Azure Blob Storage-:

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");
});

I följande exempel strömmas en video från en Azure Blob:

// 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);
});

Omdirigera

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fil

app.MapGet("/download", () => Results.File("myfile.text"));

HttpResult-gränssnitt

Följande gränssnitt i namnområdet Microsoft.AspNetCore.Http ger ett sätt att upptäcka typen IResult vid körning, vilket är ett vanligt mönster i filterimplementeringar.

Här är ett exempel på ett filter som använder något av följande gränssnitt:

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
    };
});

Mer information finns i filter i minimala API-appar och IResult-implementeringstyper.

Ändra rubriker

Använd HttpResponse-objektet för att ändra svarshuvuden:

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";
});

Anpassa svar

Program kan styra svar genom att implementera en anpassad IResult typ. Följande kod är ett exempel på en HTML-resultattyp:

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);
    }
}

Vi rekommenderar att du lägger till en tilläggsmetod till Microsoft.AspNetCore.Http.IResultExtensions för att göra dessa anpassade resultat mer upptäckbara.

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();

Dessutom kan en anpassad IResult typ ge en egen anteckning genom att implementera IEndpointMetadataProvider-gränssnittet. Följande kod lägger till en annotation till den föregående HtmlResult-typen som beskriver svaret som genereras av slutpunkten.

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 är en implementering av IProducesResponseTypeMetadata som definierar den producerade svarsinnehållstypen text/html och statuskoden 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

En alternativ metod är att använda Microsoft.AspNetCore.Mvc.ProducesAttribute för att beskriva det producerade svaret. Följande kod ändrar metoden PopulateMetadata för att använda ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Konfigurera alternativ för JSON-serialisering

Som standard använder minimala API-appar Web defaults alternativ under JSON-serialisering och deserialisering.

Konfigurera JSON-serialiseringsalternativ globalt

Alternativ kan konfigureras globalt för en app genom att anropa ConfigureHttpJsonOptions. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom fält ingår läser koden ovan NameField och innehåller den i JSON-utdata.

Konfigurera JSON-serialiseringsalternativ för en slutpunkt

Om du vill konfigurera serialiseringsalternativ för en slutpunkt anropar du Results.Json och skickar ett JsonSerializerOptions objekt till den, som du ser i följande exempel:

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

Du kan istället använda en överbelastning av WriteAsJsonAsync som tar emot en JsonSerializerOptions-objekt. I följande exempel används den här överlagringen för att formatera utdata-JSON:

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

Ytterligare resurser

Minimala slutpunkter stöder följande typer av returvärden:

  1. string – Detta inkluderar Task<string> och ValueTask<string>.
  2. T (alla andra typer) – Detta inkluderar Task<T> och ValueTask<T>.
  3. IResult-baserat – Detta inkluderar Task<IResult> och ValueTask<IResult>.

string returnera värden

Uppförande Innehållstyp
Ramverket skriver strängen direkt till svaret. text/plain

Överväg följande routningshanterare, som returnerar en Hello world text.

app.MapGet("/hello", () => "Hello World");

Statuskoden 200 returneras med text/plain innehållstypsrubrik och följande innehåll.

Hello World

T (alla andra typer) returnerar värden

Uppförande Innehållstyp
Ramverket JSON-serialiserar svaret. application/json

Överväg följande routningshanterare, som returnerar en anonym typ som innehåller en Message strängegenskap.

app.MapGet("/hello", () => new { Message = "Hello World" });

Statuskoden 200 returneras med application/json innehållstypsrubrik och följande innehåll.

{"message":"Hello World"}

IResult returnera värden

Uppförande Innehållstyp
Ramverket anropar IResult.ExecuteAsync. Bestäms genom implementeringen av IResult.

Gränssnittet IResult definierar ett kontrakt som representerar resultatet av en HTTP-slutpunkt. Den statiska Results-klassen och den statiska TypedResults- används för att skapa olika IResult objekt som representerar olika typer av svar.

TypedResults vs Resultat

De Results- och TypedResults statiska klasserna ger liknande uppsättningar resultathjälpare. Klassen TypedResults är den motsvarigheten av typen till klassen Results. Results-hjälpens returtyp är dock IResult, medan varje TypedResults-hjälpens returtyp är en av de IResult implementeringstyperna. Skillnaden innebär att en konvertering krävs för Results assistenter när den konkreta typen behövs, till exempel för enhetstestning. Implementeringstyperna definieras i namnområdet Microsoft.AspNetCore.Http.HttpResults.

Att returnera TypedResults i stället för Results har följande fördelar:

Överväg följande slutpunkt, för vilken en 200 OK statuskod med det förväntade JSON-svaret skapas.

app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
    .Produces<Message>();

För att kunna dokumentera den här slutpunkten korrekt anropas tilläggsmetoden Produces. Det är dock inte nödvändigt att anropa Produces om TypedResults används i stället för Results, enligt följande kod. TypedResults tillhandahåller automatiskt metadata för slutpunkten.

app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));

Mer information om hur du beskriver en svarstyp finns i OpenAPI-stöd i minimala API:er.

Som tidigare nämnts behövs ingen konvertering när du använder TypedResults. Överväg följande minimala API som returnerar en TypedResults-klass

public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
    var todos = await database.Todos.ToArrayAsync();
    return TypedResults.Ok(todos);
}

Följande test kontrollerar den fullständiga betongtypen:

[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);
    });
}

Eftersom alla metoder på Results returnerar IResult i sina signaturer, härleder kompilatorn automatiskt att detta är begärandedelegatens returtyp när olika resultat returneras från en enda slutpunkt. TypedResults kräver användning av Results<T1, TN> av sådana ombud.

Följande metod kompilerar eftersom både Results.Ok och Results.NotFound deklareras som att de returnerar IResult, även om de faktiska konkreta typerna för de objekt som returneras skiljer sig åt.

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
    await db.Todos.FindAsync(id)
        is Todo todo
            ? Results.Ok(todo)
            : Results.NotFound());

Följande metod kompileras inte eftersom TypedResults.Ok och TypedResults.NotFound deklareras som returnerade olika typer och kompilatorn inte försöker härleda den bästa matchningstypen:

app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
     await db.Todos.FindAsync(id)
     is Todo todo
        ? TypedResults.Ok(todo)
        : TypedResults.NotFound());

Om du vill använda TypedResultsmåste returtypen vara helt deklarerad, och när det är asynkront krävs Task<>-wrapper. Att använda TypedResults är mer utförligt, men det är kompromissen för att typinformationen ska vara statiskt tillgänglig och därmed kunna självbeskriva till 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());

Resultat<TResult1, TResultN>

Använd Results<TResult1, TResultN> som slutpunktshanterarens returtyp i stället för IResult när:

  • Flera IResult implementeringstyper returneras från slutpunktshanteraren.
  • Den statiska TypedResult-klassen används för att skapa IResult objekt.

Det här alternativet är bättre än att returnera IResult eftersom de allmänna unionstyperna automatiskt behåller slutpunktsmetadata. Och eftersom dessa Results<TResult1, TResultN> unionstyper implementerar implicita cast-operatorer kan kompilatorn automatiskt konvertera de typer som anges i de generiska argumenten till en instans av unionstypen.

Detta har den extra fördelen med att tillhandahålla kompileringstidskontroll att en routningshanterare faktiskt bara returnerar de resultat som den deklarerar att den gör. Försök att returnera en typ som inte deklareras som ett av de allmänna argumenten för att Results<> resulterar i ett kompileringsfel.

Tänk på följande slutpunkt, för vilken en 400 BadRequest statuskod returneras när orderId är större än 999. Om inte, skapar den en 200 OK med det förväntade innehållet.

app.MapGet("/orders/{orderId}", IResult (int orderId)
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
    .Produces(400)
    .Produces<Order>();

För att kunna dokumentera den här slutpunkten korrekt anropas tilläggsmetoden Produces. Men eftersom TypedResults-hjälpen automatiskt innehåller metadata för slutpunkten kan du returnera Results<T1, Tn> unionstyp i stället, som du ser i följande kod.

app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId) 
    => orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));

Inbyggda resultat

Vanliga resultathjälpare finns i Results- och TypedResults statiska klasser. Att returnera TypedResults föredras framför att returnera Results. Mer information finns i TypedResults vs Results.

Följande avsnitt visar användningen av vanliga resultathjälpare.

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

WriteAsJsonAsync är ett alternativt sätt att returnera JSON:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
    (new { Message = "Hello World" }));

Anpassad statuskod

app.MapGet("/405", () => Results.StatusCode(405));

Text

app.MapGet("/text", () => Results.Text("This is some text"));

Ström

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 överlagringar ger åtkomst till den underliggande HTTP-svarsströmmen utan buffring. I följande exempel används ImageSharp- för att returnera en reducerad storlek på den angivna bilden:

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);
}

I följande exempel strömmas en avbildning från Azure Blob Storage-:

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");
});

I följande exempel strömmas en video från en Azure Blob:

// 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);
});

Omdirigera

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Fil

app.MapGet("/download", () => Results.File("myfile.text"));

HttpResult-gränssnitt

Följande gränssnitt i namnområdet Microsoft.AspNetCore.Http ger ett sätt att identifiera IResult-typen vid körning, vilken är en vanlig struktur vid filterimplementeringar.

Här är ett exempel på ett filter som använder något av följande gränssnitt:

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
    };
});

Mer information finns i filter i minimala API-appar och IResult-implementeringstyper.

Anpassa svar

Program kan styra svar genom att implementera en anpassad IResult typ. Följande kod är ett exempel på en HTML-resultattyp:

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);
    }
}

Vi rekommenderar att du lägger till en tilläggsmetod till Microsoft.AspNetCore.Http.IResultExtensions för att göra dessa anpassade resultat lättare att upptäcka.

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();

Dessutom kan en anpassad IResult typ ge en egen anteckning genom att implementera IEndpointMetadataProvider-gränssnittet. Följande kod lägger till en anteckning till den föregående HtmlResult-typen som beskriver det svar som genereras av slutpunkten.

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 är en implementering av IProducesResponseTypeMetadata som definierar den producerade svarsinnehållstypen text/html och statuskoden 200 OK.

internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
    public Type? Type => null;

    public int StatusCode => 200;

    public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}

En alternativ metod är att använda Microsoft.AspNetCore.Mvc.ProducesAttribute för att beskriva det producerade svaret. Följande kod ändrar metoden PopulateMetadata för att använda ProducesAttribute.

public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
    builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}

Konfigurera alternativ för JSON-serialisering

Som standard använder minimala API-appar Web defaults alternativ under JSON-serialisering och deserialisering.

Konfigurera JSON-serialiseringsalternativ globalt

Alternativ kan konfigureras globalt för en app genom att anropa ConfigureHttpJsonOptions. Följande exempel innehåller offentliga fält och formaterar JSON-utdata.

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

Eftersom fält ingår läser koden ovan NameField och innehåller den i JSON-utdata.

Konfigurera JSON-serialiseringsalternativ för en slutpunkt

Om du vill konfigurera serialiseringsalternativ för en slutpunkt anropar du Results.Json och skickar ett JsonSerializerOptions objekt till den, som du ser i följande exempel:

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

Du kan också använda en överbelastning av WriteAsJsonAsync som accepterar ett JsonSerializerOptions-objekt. I följande exempel används den här överlagringen för att formatera utdata-JSON:

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

Ytterligare resurser