Sdílet prostřednictvím


Jak vytvářet odpovědi v minimálních aplikacích API

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Upozorňující

Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Minimální koncové body podporují následující typy návratových hodnot:

  1. string - To zahrnuje Task<string> a ValueTask<string>.
  2. T (Jakýkoli jiný typ) - To zahrnuje Task<T> a ValueTask<T>.
  3. IResult na základě - To zahrnuje Task<IResult> a ValueTask<IResult>.

string návratové hodnoty

Chování Typ obsahu
Architektura zapíše řetězec přímo do odpovědi. text/plain

Představte si následující obslužnou rutinu Hello world trasy, která vrací text.

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

Stavový 200 kód se vrátí s hlavičkou text/plain Content-Type a následujícím obsahem.

Hello World

T (Jakýkoli jiný typ) vrácené hodnoty

Chování Typ obsahu
Rozhraní JSON-serializuje odpověď. application/json

Zvažte následující obslužnou rutinu trasy, která vrací anonymní typ obsahující Message vlastnost řetězce.

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

Stavový 200 kód se vrátí s hlavičkou application/json Content-Type a následujícím obsahem.

{"message":"Hello World"}

IResult návratové hodnoty

Chování Typ obsahu
Architektura volá IResult.ExecuteAsync. Rozhodlo se implementací IResult .

Rozhraní IResult definuje kontrakt, který představuje výsledek koncového bodu HTTP. Static Results třída a static TypedResults slouží k vytvoření různých objektů, které představují různé IResult typy odpovědí.

TypedResults vs. výsledky

Třídy Results a TypedResults statické třídy poskytují podobné sady pomocných rutin výsledků. Třída TypedResults je typový ekvivalent Results třídy. Návratový Results typ pomocných rutin je IResultvšak , zatímco návratový typ jednotlivých TypedResults pomocných rutin je jedním z IResult typů implementace. Rozdíl znamená, že u Results pomocných rutin je potřeba převod, když je třeba konkrétní typ, například pro testování jednotek. Typy implementace jsou definovány v Microsoft.AspNetCore.Http.HttpResults oboru názvů.

Vrácení TypedResults místo Results následujících výhod:

Vezměte v úvahu následující koncový bod, pro který se vytvoří stavový kód s očekávanou 200 OKJSodpovědí ON.

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

Aby bylo možné zdokumentovat tento koncový bod správně, volá se metoda Produces rozšíření. Není však nutné volat Produces , pokud TypedResults se používá místo Results, jak je znázorněno v následujícím kódu. TypedResults automaticky poskytuje metadata pro koncový bod.

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

Další informace o popisu typu odpovědi najdete v tématu Podpora OpenAPI v minimálních rozhraních API.

Jak už bylo zmíněno dříve, při použití TypedResultsnení převod potřeba. Zvažte následující minimální rozhraní API, které vrací TypedResults třídu.

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

Následující zkoušky kontrolou úplného typu betonu:

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

Vzhledem k tomu, že všechny metody při Results vrácení IResult ve svém podpisu kompilátor automaticky odvodí, že jako typ vrácení delegáta požadavku při vrácení různých výsledků z jednoho koncového bodu. TypedResults vyžaduje použití Results<T1, TN> těchto delegátů.

Následující metoda se zkompiluje, protože obě Results.Ok a Results.NotFound jsou deklarovány jako vrácení IResult, i když skutečné konkrétní typy vrácených objektů jsou odlišné:

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

Následující metoda se nekompiluje, protože TypedResults.Ok a TypedResults.NotFound jsou deklarovány jako vrácení různých typů a kompilátor se nepokusí odvodit nejlepší odpovídající typ:

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

Chcete-li použít TypedResults, návratový typ musí být plně deklarován, což v případě asynchronního vyžaduje obálku Task<> . Použití TypedResults je více podrobné, ale to je kompromis pro to, aby informace o typu byly staticky dostupné a jsou tak schopny samostatně popsat 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());

Výsledky<TResult1, TResultN>

Jako návratový typ obslužné rutiny koncového IResult bodu použijte Results<TResult1, TResultN> místo v těchto případech:

  • Z obslužné rutiny koncového bodu se vrátí více IResult typů implementace.
  • Statická TypedResult třída se používá k vytvoření IResult objektů.

Tato alternativa je lepší než vrácení IResult , protože obecné typy sjednocení automaticky uchovávají metadata koncového bodu. A vzhledem k tomu, že Results<TResult1, TResultN> typy sjednocení implementují implicitní operátory přetypování, kompilátor může automaticky převést typy zadané v obecných argumentech na instanci typu sjednocení.

To má přidanou výhodu poskytování kontroly času kompilace, že obslužná rutina trasy skutečně vrací pouze výsledky, které deklaruje. Pokus o vrácení typu, který není deklarován jako jeden z obecných argumentů, aby výsledkem Results<> byla chyba kompilace.

Vezměte v úvahu následující koncový bod, pro který se vrátí stavový 400 BadRequest kód, když orderId je větší než 999. V opačném případě vytvoří s očekávaným obsahem 200 OK .

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

Aby bylo možné tento koncový bod správně zdokumentovat, volá se metoda Produces rozšíření. Vzhledem k tomu, že pomocná TypedResults rutina automaticky obsahuje metadata koncového bodu, můžete místo toho vrátit Results<T1, Tn> typ sjednocení, jak je znázorněno v následujícím kódu.

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

Předdefinované výsledky

Běžné pomocné rutiny výsledků existují ve statických ResultsTypedResults třídách. TypedResults Vrácení je upřednostňované pro vrácení Results. Další informace naleznete v tématu TypedResults vs Výsledky.

Následující části ukazují použití běžných pomocných rutin výsledků.

JSNA

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

WriteAsJsonAsync je alternativní způsob, jak vrátit hodnotu JSON:

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

Vlastní stavový kód

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

Vnitřní chyba serveru

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

Předchozí příklad vrátí stavový kód 500.

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 přetížení umožňují přístup k podkladovému streamu odpovědi HTTP bez ukládání do vyrovnávací paměti. Následující příklad používá ImageSharp k vrácení zmenšené velikosti zadané image:

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

Následující příklad streamuje obrázek ze služby 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");
});

Následující příklad streamuje video z objektu blob Azure:

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

Přesměrování

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

Soubor

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

Rozhraní HttpResult

Následující rozhraní v Microsoft.AspNetCore.Http oboru názvů poskytují způsob, jak zjistit IResult typ za běhu, což je běžný vzor v implementacích filtrů:

Tady je příklad filtru, který používá jedno z těchto rozhraní:

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

Další informace najdete v tématu Filtry v minimálních aplikacích API a typech implementace IResult.

Přizpůsobení odpovědí

Aplikace můžou řídit odpovědi implementací vlastního IResult typu. Následující kód je příkladem typu výsledku HTML:

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

Doporučujeme přidat metodu rozšíření, aby Microsoft.AspNetCore.Http.IResultExtensions byly tyto vlastní výsledky lépe zjistitelné.

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

IResult Vlastní typ může také poskytnout vlastní poznámku implementací IEndpointMetadataProvider rozhraní. Následující kód například přidá poznámku k předchozímu HtmlResult typu, který popisuje odpověď vytvořenou koncovým bodem.

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

Jedná se ProducesHtmlMetadata o implementaci IProducesResponseTypeMetadata , která definuje vytvořený typ text/html obsahu odpovědi a stavový kód 200 OK.

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

    public int StatusCode => 200;

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

Alternativním přístupem je popis Microsoft.AspNetCore.Mvc.ProducesAttribute vytvořené odpovědi. Následující kód změní metodu PopulateMetadata , která se má použít ProducesAttribute.

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

Konfigurace JSmožností serializace ON

Ve výchozím nastavení používají Web defaults minimální aplikace API možnosti při JSserializaci a deserializaci PŘI.

Globální konfigurace JSmožností serializace ZAPNUTO

Možnosti lze pro aplikaci nakonfigurovat globálně vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že pole jsou zahrnuta, předchozí kód přečte NameField a zahrne ho do výstupu JSON.

Konfigurace JSmožností serializace ON pro koncový bod

Pokud chcete nakonfigurovat možnosti serializace pro koncový bod, vyvoláte Results.Json ho a předejte mu JsonSerializerOptions objekt, jak je znázorněno v následujícím příkladu:

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

Jako alternativu použijte přetížení WriteAsJsonAsync , které přijímá JsonSerializerOptions objekt. Následující příklad používá toto přetížení k formátování výstupu 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
// }

Další materiály

Minimální koncové body podporují následující typy návratových hodnot:

  1. string - To zahrnuje Task<string> a ValueTask<string>.
  2. T (Jakýkoli jiný typ) - To zahrnuje Task<T> a ValueTask<T>.
  3. IResult na základě - To zahrnuje Task<IResult> a ValueTask<IResult>.

string návratové hodnoty

Chování Typ obsahu
Architektura zapíše řetězec přímo do odpovědi. text/plain

Představte si následující obslužnou rutinu Hello world trasy, která vrací text.

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

Stavový 200 kód se vrátí s hlavičkou text/plain Content-Type a následujícím obsahem.

Hello World

T (Jakýkoli jiný typ) vrácené hodnoty

Chování Typ obsahu
Rozhraní JSON-serializuje odpověď. application/json

Zvažte následující obslužnou rutinu trasy, která vrací anonymní typ obsahující Message vlastnost řetězce.

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

Stavový 200 kód se vrátí s hlavičkou application/json Content-Type a následujícím obsahem.

{"message":"Hello World"}

IResult návratové hodnoty

Chování Typ obsahu
Architektura volá IResult.ExecuteAsync. Rozhodlo se implementací IResult .

Rozhraní IResult definuje kontrakt, který představuje výsledek koncového bodu HTTP. Static Results třída a static TypedResults slouží k vytvoření různých objektů, které představují různé IResult typy odpovědí.

TypedResults vs. výsledky

Třídy Results a TypedResults statické třídy poskytují podobné sady pomocných rutin výsledků. Třída TypedResults je typový ekvivalent Results třídy. Návratový Results typ pomocných rutin je IResultvšak , zatímco návratový typ jednotlivých TypedResults pomocných rutin je jedním z IResult typů implementace. Rozdíl znamená, že u Results pomocných rutin je potřeba převod, když je třeba konkrétní typ, například pro testování jednotek. Typy implementace jsou definovány v Microsoft.AspNetCore.Http.HttpResults oboru názvů.

Vrácení TypedResults místo Results následujících výhod:

Vezměte v úvahu následující koncový bod, pro který se vytvoří stavový kód s očekávanou 200 OKJSodpovědí ON.

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

Aby bylo možné zdokumentovat tento koncový bod správně, volá se metoda Produces rozšíření. Není však nutné volat Produces , pokud TypedResults se používá místo Results, jak je znázorněno v následujícím kódu. TypedResults automaticky poskytuje metadata pro koncový bod.

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

Další informace o popisu typu odpovědi najdete v tématu Podpora OpenAPI v minimálních rozhraních API.

Jak už bylo zmíněno dříve, při použití TypedResultsnení převod potřeba. Zvažte následující minimální rozhraní API, které vrací TypedResults třídu.

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

Následující zkoušky kontrolou úplného typu betonu:

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

Vzhledem k tomu, že všechny metody při Results vrácení IResult ve svém podpisu kompilátor automaticky odvodí, že jako typ vrácení delegáta požadavku při vrácení různých výsledků z jednoho koncového bodu. TypedResults vyžaduje použití Results<T1, TN> těchto delegátů.

Následující metoda se zkompiluje, protože obě Results.Ok a Results.NotFound jsou deklarovány jako vrácení IResult, i když skutečné konkrétní typy vrácených objektů jsou odlišné:

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

Následující metoda se nekompiluje, protože TypedResults.Ok a TypedResults.NotFound jsou deklarovány jako vrácení různých typů a kompilátor se nepokusí odvodit nejlepší odpovídající typ:

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

Chcete-li použít TypedResults, návratový typ musí být plně deklarován, což v případě asynchronního vyžaduje obálku Task<> . Použití TypedResults je více podrobné, ale to je kompromis pro to, aby informace o typu byly staticky dostupné a jsou tak schopny samostatně popsat 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());

Výsledky<TResult1, TResultN>

Jako návratový typ obslužné rutiny koncového IResult bodu použijte Results<TResult1, TResultN> místo v těchto případech:

  • Z obslužné rutiny koncového bodu se vrátí více IResult typů implementace.
  • Statická TypedResult třída se používá k vytvoření IResult objektů.

Tato alternativa je lepší než vrácení IResult , protože obecné typy sjednocení automaticky uchovávají metadata koncového bodu. A vzhledem k tomu, že Results<TResult1, TResultN> typy sjednocení implementují implicitní operátory přetypování, kompilátor může automaticky převést typy zadané v obecných argumentech na instanci typu sjednocení.

To má přidanou výhodu poskytování kontroly času kompilace, že obslužná rutina trasy skutečně vrací pouze výsledky, které deklaruje. Pokus o vrácení typu, který není deklarován jako jeden z obecných argumentů, aby výsledkem Results<> byla chyba kompilace.

Vezměte v úvahu následující koncový bod, pro který se vrátí stavový 400 BadRequest kód, když orderId je větší než 999. V opačném případě vytvoří s očekávaným obsahem 200 OK .

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

Aby bylo možné tento koncový bod správně zdokumentovat, volá se metoda Produces rozšíření. Vzhledem k tomu, že pomocná TypedResults rutina automaticky obsahuje metadata koncového bodu, můžete místo toho vrátit Results<T1, Tn> typ sjednocení, jak je znázorněno v následujícím kódu.

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

Předdefinované výsledky

Běžné pomocné rutiny výsledků existují ve statických ResultsTypedResults třídách. TypedResults Vrácení je upřednostňované pro vrácení Results. Další informace naleznete v tématu TypedResults vs Výsledky.

Následující části ukazují použití běžných pomocných rutin výsledků.

JSNA

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

WriteAsJsonAsync je alternativní způsob, jak vrátit hodnotu JSON:

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

Vlastní stavový kód

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 přetížení umožňují přístup k podkladovému streamu odpovědi HTTP bez ukládání do vyrovnávací paměti. Následující příklad používá ImageSharp k vrácení zmenšené velikosti zadané image:

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

Následující příklad streamuje obrázek ze služby 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");
});

Následující příklad streamuje video z objektu blob Azure:

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

Přesměrování

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

Soubor

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

Rozhraní HttpResult

Následující rozhraní v Microsoft.AspNetCore.Http oboru názvů poskytují způsob, jak zjistit IResult typ za běhu, což je běžný vzor v implementacích filtrů:

Tady je příklad filtru, který používá jedno z těchto rozhraní:

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

Další informace najdete v tématu Filtry v minimálních aplikacích API a typech implementace IResult.

Přizpůsobení odpovědí

Aplikace můžou řídit odpovědi implementací vlastního IResult typu. Následující kód je příkladem typu výsledku HTML:

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

Doporučujeme přidat metodu rozšíření, aby Microsoft.AspNetCore.Http.IResultExtensions byly tyto vlastní výsledky lépe zjistitelné.

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

IResult Vlastní typ může také poskytnout vlastní poznámku implementací IEndpointMetadataProvider rozhraní. Následující kód například přidá poznámku k předchozímu HtmlResult typu, který popisuje odpověď vytvořenou koncovým bodem.

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

Jedná se ProducesHtmlMetadata o implementaci IProducesResponseTypeMetadata , která definuje vytvořený typ text/html obsahu odpovědi a stavový kód 200 OK.

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

    public int StatusCode => 200;

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

Alternativním přístupem je popis Microsoft.AspNetCore.Mvc.ProducesAttribute vytvořené odpovědi. Následující kód změní metodu PopulateMetadata , která se má použít ProducesAttribute.

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

Konfigurace JSmožností serializace ON

Ve výchozím nastavení používají Web defaults minimální aplikace API možnosti při JSserializaci a deserializaci PŘI.

Globální konfigurace JSmožností serializace ZAPNUTO

Možnosti lze pro aplikaci nakonfigurovat globálně vyvoláním ConfigureHttpJsonOptions. Následující příklad obsahuje veřejná pole a formáty JSON výstupu.

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

Vzhledem k tomu, že pole jsou zahrnuta, předchozí kód přečte NameField a zahrne ho do výstupu JSON.

Konfigurace JSmožností serializace ON pro koncový bod

Pokud chcete nakonfigurovat možnosti serializace pro koncový bod, vyvoláte Results.Json ho a předejte mu JsonSerializerOptions objekt, jak je znázorněno v následujícím příkladu:

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

Jako alternativu použijte přetížení WriteAsJsonAsync , které přijímá JsonSerializerOptions objekt. Následující příklad používá toto přetížení k formátování výstupu 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
// }

Další materiály