Udostępnij za pośrednictwem


Jak tworzyć odpowiedzi w minimalnych aplikacjach interfejsu API

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.

Minimalne punkty końcowe obsługują następujące typy zwracanych wartości:

  1. string — Dotyczy to również Task<string> i ValueTask<string>.
  2. T (Dowolny inny typ) — dotyczy to również Task<T> elementów i ValueTask<T>.
  3. IResult based — obejmuje to Task<IResult> i ValueTask<IResult>.

string zwracane wartości

Zachowanie Typ zawartości
Struktura zapisuje ciąg bezpośrednio w odpowiedzi. text/plain

Rozważmy następującą procedurę Hello world obsługi tras, która zwraca tekst.

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

Kod 200 stanu jest zwracany z nagłówkiem text/plain Content-Type i następującą zawartością.

Hello World

T (Dowolny inny typ) zwracane wartości

Zachowanie Typ zawartości
Struktura JSON serializuje odpowiedź. application/json

Rozważmy następującą procedurę obsługi tras, która zwraca typ anonimowy zawierający Message właściwość ciągu.

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

Kod 200 stanu jest zwracany z nagłówkiem application/json Content-Type i następującą zawartością.

{"message":"Hello World"}

IResult zwracane wartości

Zachowanie Typ zawartości
Struktura wywołuje metodę IResult.ExecuteAsync. Decyzja o wdrożeniu IResult .

Interfejs IResult definiuje kontrakt reprezentujący wynik punktu końcowego HTTP. Statyczna klasa Results i statyczny typdResults służą do tworzenia różnych obiektów reprezentujących różne IResult typy odpowiedzi.

TypedResults a wyniki

Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults jest typem równoważnej Results klasie. Jednak Results zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults pomocnika jest jednym z IResult typów implementacji. Różnica oznacza, że w przypadku Results pomocników wymagana jest konwersja, gdy wymagany jest konkretny typ, na przykład do testowania jednostkowego. Typy implementacji są definiowane Microsoft.AspNetCore.Http.HttpResults w przestrzeni nazw.

Zwracanie TypedResults zamiast Results ma następujące zalety:

Rozważ następujący punkt końcowy, dla którego 200 OK jest generowany kod stanu z oczekiwaną odpowiedzią JSON.

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

Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces rozszerzeń. Nie jest jednak konieczne wywołanie Produces metody , jeśli TypedResults jest używana zamiast Results, jak pokazano w poniższym kodzie. TypedResults automatycznie udostępnia metadane punktu końcowego.

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

Aby uzyskać więcej informacji na temat opisywania typu odpowiedzi, zobacz Obsługa interfejsu OpenAPI w minimalnych interfejsach API.

Jak wspomniano wcześniej, w przypadku korzystania z programu TypedResultskonwersja nie jest wymagana. Rozważmy następujący minimalny interfejs API, który zwraca klasę TypedResults

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

Następujące testy sprawdzają pełny typ konkretnego:

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

Ponieważ wszystkie metody zwracane Results IResult w podpisie, kompilator automatycznie wywnioskuje, że jako delegat żądania zwraca typ zwracany podczas zwracania różnych wyników z jednego punktu końcowego. TypedResults wymaga użycia z Results<T1, TN> takich delegatów.

Następująca metoda kompiluje się, ponieważ obie Results.Ok metody i Results.NotFound są deklarowane jako zwracane IResult, mimo że rzeczywiste konkretne typy zwracanych obiektów są różne:

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

Poniższa metoda nie jest kompilowana, ponieważ i TypedResults.NotFound są deklarowane jako zwracane różne typy, a TypedResults.Ok kompilator nie podejmie próby wywnioskowania najlepszego zgodnego typu:

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

Aby użyć TypedResultsmetody , typ zwracany musi być w pełni zadeklarowany, co w przypadku, gdy asynchroniczny wymaga Task<> otoki. Użycie TypedResults jest bardziej pełne, ale to kompromis w przypadku statycznego udostępniania informacji o typie i w związku z tym możliwość samodzielnego opisywania interfejsu 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());

Wyniki<TResult1, TResultN>

Użyj Results<TResult1, TResultN> jako typu zwracanego programu obsługi punktów końcowych IResult zamiast w następujących przypadkach:

  • Z programu obsługi punktu końcowego jest zwracanych wiele IResult typów implementacji.
  • Klasa statyczna TypedResult służy do tworzenia IResult obiektów.

Ta alternatywa jest lepsza niż zwracanie IResult , ponieważ typy związków ogólnych automatycznie zachowują metadane punktu końcowego. Results<TResult1, TResultN> Ponieważ typy unii implementują niejawne operatory rzutowania, kompilator może automatycznie konwertować typy określone w argumentach ogólnych na wystąpienie typu unii.

Ma to dodatkową zaletę zapewnienia sprawdzania czasu kompilacji, że program obsługi tras rzeczywiście zwraca tylko wyniki, które deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych, aby spowodować Results<> błąd kompilacji.

Rozważ następujący punkt końcowy, dla którego 400 BadRequest jest zwracany kod stanu, gdy orderId wartość jest większa niż 999. W przeciwnym razie tworzy element 200 OK z oczekiwaną zawartością.

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

Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces rozszerzenia. Jednak ponieważ TypedResults pomocnik automatycznie zawiera metadane punktu końcowego, można zamiast tego zwrócić Results<T1, Tn> typ unii, jak pokazano w poniższym kodzie.

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

Wbudowane wyniki

Typowe pomocniki wyników istnieją w Results klasach statycznych i .TypedResults Zwracanie jest preferowane TypedResults do zwracania Resultswartości . Aby uzyskać więcej informacji, zobacz TypedResults vs Results (TypdResults a wyniki).

W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.

JSON

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

WriteAsJsonAsync jest alternatywnym sposobem zwracania kodu JSON:

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

Niestandardowy kod stanu

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

Wewnętrzny błąd serwera

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

Powyższy przykład zwraca kod stanu 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 przeciążenia umożliwiają dostęp do bazowego strumienia odpowiedzi HTTP bez buforowania. W poniższym przykładzie użyto elementu ImageSharp w celu zwrócenia mniejszego rozmiaru określonego obrazu:

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

Poniższy przykład przesyła strumieniowo obraz z usługi 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");
});

Poniższy przykład przesyła strumieniowo wideo z obiektu blob platformy 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);
});

Przekierowanie

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

Plik

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

Interfejsy HttpResult

Następujące interfejsy w Microsoft.AspNetCore.Http przestrzeni nazw umożliwiają wykrywanie IResult typu w czasie wykonywania, który jest typowym wzorcem implementacji filtrów:

Oto przykład filtru, który używa jednego z następujących interfejsów:

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

Aby uzyskać więcej informacji, zobacz Filtry w minimalnych aplikacjach interfejsu API i typach implementacji IResult.

Dostosowywanie odpowiedzi

Aplikacje mogą kontrolować odpowiedzi, implementując typ niestandardowy IResult . Poniższy kod jest przykładem typu wyniku 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);
    }
}

Zalecamy dodanie metody rozszerzenia w celu Microsoft.AspNetCore.Http.IResultExtensions zwiększenia możliwości odnajdywania tych niestandardowych wyników.

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

Ponadto typ niestandardowy IResult może zapewnić własną adnotację przez zaimplementowanie interfejsu IEndpointMetadataProvider . Na przykład poniższy kod dodaje adnotację do poprzedniego HtmlResult typu, który opisuje odpowiedź wygenerowaną przez punkt końcowy.

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

Jest ProducesHtmlMetadata to implementacja IProducesResponseTypeMetadata , która definiuje wygenerowany typ text/html zawartości odpowiedzi i kod 200 OKstanu .

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

    public int StatusCode => 200;

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

Alternatywnym podejściem jest użycie metody do Microsoft.AspNetCore.Mvc.ProducesAttribute opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata , aby użyć ProducesAttributemetody .

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

Konfigurowanie opcji serializacji JSON

Domyślnie minimalne aplikacje interfejsu API używają Web defaults opcji podczas serializacji i deserializacji JSON.

Globalne konfigurowanie opcji serializacji JSON

Opcje można skonfigurować globalnie dla aplikacji, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

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

Ponieważ pola są uwzględniane, powyższy kod odczytuje NameField i zawiera je w wyjściowym formacie JSON.

Konfigurowanie opcji serializacji JSON dla punktu końcowego

Aby skonfigurować opcje serializacji dla punktu końcowego, wywołaj Results.Json i przekaż do niego JsonSerializerOptions obiekt, jak pokazano w poniższym przykładzie:

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

Alternatywnie należy użyć przeciążenia WriteAsJsonAsync , które akceptuje JsonSerializerOptions obiekt. W poniższym przykładzie użyto tego przeciążenia do sformatowania wyjściowego kodu 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
// }

Dodatkowe zasoby

Minimalne punkty końcowe obsługują następujące typy zwracanych wartości:

  1. string — Dotyczy to również Task<string> i ValueTask<string>.
  2. T (Dowolny inny typ) — dotyczy to również Task<T> elementów i ValueTask<T>.
  3. IResult based — obejmuje to Task<IResult> i ValueTask<IResult>.

string zwracane wartości

Zachowanie Typ zawartości
Struktura zapisuje ciąg bezpośrednio w odpowiedzi. text/plain

Rozważmy następującą procedurę Hello world obsługi tras, która zwraca tekst.

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

Kod 200 stanu jest zwracany z nagłówkiem text/plain Content-Type i następującą zawartością.

Hello World

T (Dowolny inny typ) zwracane wartości

Zachowanie Typ zawartości
Struktura JSON serializuje odpowiedź. application/json

Rozważmy następującą procedurę obsługi tras, która zwraca typ anonimowy zawierający Message właściwość ciągu.

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

Kod 200 stanu jest zwracany z nagłówkiem application/json Content-Type i następującą zawartością.

{"message":"Hello World"}

IResult zwracane wartości

Zachowanie Typ zawartości
Struktura wywołuje metodę IResult.ExecuteAsync. Decyzja o wdrożeniu IResult .

Interfejs IResult definiuje kontrakt reprezentujący wynik punktu końcowego HTTP. Statyczna klasa Results i statyczny typdResults służą do tworzenia różnych obiektów reprezentujących różne IResult typy odpowiedzi.

TypedResults a wyniki

Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults jest typem równoważnej Results klasie. Jednak Results zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults pomocnika jest jednym z IResult typów implementacji. Różnica oznacza, że w przypadku Results pomocników wymagana jest konwersja, gdy wymagany jest konkretny typ, na przykład do testowania jednostkowego. Typy implementacji są definiowane Microsoft.AspNetCore.Http.HttpResults w przestrzeni nazw.

Zwracanie TypedResults zamiast Results ma następujące zalety:

Rozważ następujący punkt końcowy, dla którego 200 OK jest generowany kod stanu z oczekiwaną odpowiedzią JSON.

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

Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces rozszerzeń. Nie jest jednak konieczne wywołanie Produces metody , jeśli TypedResults jest używana zamiast Results, jak pokazano w poniższym kodzie. TypedResults automatycznie udostępnia metadane punktu końcowego.

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

Aby uzyskać więcej informacji na temat opisywania typu odpowiedzi, zobacz Obsługa interfejsu OpenAPI w minimalnych interfejsach API.

Jak wspomniano wcześniej, w przypadku korzystania z programu TypedResultskonwersja nie jest wymagana. Rozważmy następujący minimalny interfejs API, który zwraca klasę TypedResults

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

Następujące testy sprawdzają pełny typ konkretnego:

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

Ponieważ wszystkie metody zwracane Results IResult w podpisie, kompilator automatycznie wywnioskuje, że jako delegat żądania zwraca typ zwracany podczas zwracania różnych wyników z jednego punktu końcowego. TypedResults wymaga użycia z Results<T1, TN> takich delegatów.

Następująca metoda kompiluje się, ponieważ obie Results.Ok metody i Results.NotFound są deklarowane jako zwracane IResult, mimo że rzeczywiste konkretne typy zwracanych obiektów są różne:

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

Poniższa metoda nie jest kompilowana, ponieważ i TypedResults.NotFound są deklarowane jako zwracane różne typy, a TypedResults.Ok kompilator nie podejmie próby wywnioskowania najlepszego zgodnego typu:

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

Aby użyć TypedResultsmetody , typ zwracany musi być w pełni zadeklarowany, co w przypadku, gdy asynchroniczny wymaga Task<> otoki. Użycie TypedResults jest bardziej pełne, ale to kompromis w przypadku statycznego udostępniania informacji o typie i w związku z tym możliwość samodzielnego opisywania interfejsu 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());

Wyniki<TResult1, TResultN>

Użyj Results<TResult1, TResultN> jako typu zwracanego programu obsługi punktów końcowych IResult zamiast w następujących przypadkach:

  • Z programu obsługi punktu końcowego jest zwracanych wiele IResult typów implementacji.
  • Klasa statyczna TypedResult służy do tworzenia IResult obiektów.

Ta alternatywa jest lepsza niż zwracanie IResult , ponieważ typy związków ogólnych automatycznie zachowują metadane punktu końcowego. Results<TResult1, TResultN> Ponieważ typy unii implementują niejawne operatory rzutowania, kompilator może automatycznie konwertować typy określone w argumentach ogólnych na wystąpienie typu unii.

Ma to dodatkową zaletę zapewnienia sprawdzania czasu kompilacji, że program obsługi tras rzeczywiście zwraca tylko wyniki, które deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych, aby spowodować Results<> błąd kompilacji.

Rozważ następujący punkt końcowy, dla którego 400 BadRequest jest zwracany kod stanu, gdy orderId wartość jest większa niż 999. W przeciwnym razie tworzy element 200 OK z oczekiwaną zawartością.

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

Aby poprawnie udokumentować ten punkt końcowy, wywoływana jest metoda Produces rozszerzenia. Jednak ponieważ TypedResults pomocnik automatycznie zawiera metadane punktu końcowego, można zamiast tego zwrócić Results<T1, Tn> typ unii, jak pokazano w poniższym kodzie.

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

Wbudowane wyniki

Typowe pomocniki wyników istnieją w Results klasach statycznych i .TypedResults Zwracanie jest preferowane TypedResults do zwracania Resultswartości . Aby uzyskać więcej informacji, zobacz TypedResults vs Results (TypdResults a wyniki).

W poniższych sekcjach przedstawiono użycie typowych pomocników wyników.

JSON

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

WriteAsJsonAsync jest alternatywnym sposobem zwracania kodu JSON:

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

Niestandardowy kod stanu

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 przeciążenia umożliwiają dostęp do bazowego strumienia odpowiedzi HTTP bez buforowania. W poniższym przykładzie użyto elementu ImageSharp w celu zwrócenia mniejszego rozmiaru określonego obrazu:

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

Poniższy przykład przesyła strumieniowo obraz z usługi 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");
});

Poniższy przykład przesyła strumieniowo wideo z obiektu blob platformy 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);
});

Przekierowanie

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

Plik

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

Interfejsy HttpResult

Następujące interfejsy w Microsoft.AspNetCore.Http przestrzeni nazw umożliwiają wykrywanie IResult typu w czasie wykonywania, który jest typowym wzorcem implementacji filtrów:

Oto przykład filtru, który używa jednego z następujących interfejsów:

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

Aby uzyskać więcej informacji, zobacz Filtry w minimalnych aplikacjach interfejsu API i typach implementacji IResult.

Dostosowywanie odpowiedzi

Aplikacje mogą kontrolować odpowiedzi, implementując typ niestandardowy IResult . Poniższy kod jest przykładem typu wyniku 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);
    }
}

Zalecamy dodanie metody rozszerzenia w celu Microsoft.AspNetCore.Http.IResultExtensions zwiększenia możliwości odnajdywania tych niestandardowych wyników.

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

Ponadto typ niestandardowy IResult może zapewnić własną adnotację przez zaimplementowanie interfejsu IEndpointMetadataProvider . Na przykład poniższy kod dodaje adnotację do poprzedniego HtmlResult typu, który opisuje odpowiedź wygenerowaną przez punkt końcowy.

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

Jest ProducesHtmlMetadata to implementacja IProducesResponseTypeMetadata , która definiuje wygenerowany typ text/html zawartości odpowiedzi i kod 200 OKstanu .

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

    public int StatusCode => 200;

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

Alternatywnym podejściem jest użycie metody do Microsoft.AspNetCore.Mvc.ProducesAttribute opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata , aby użyć ProducesAttributemetody .

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

Konfigurowanie opcji serializacji JSON

Domyślnie minimalne aplikacje interfejsu API używają Web defaults opcji podczas serializacji i deserializacji JSON.

Globalne konfigurowanie opcji serializacji JSON

Opcje można skonfigurować globalnie dla aplikacji, wywołując ConfigureHttpJsonOptionsmetodę . Poniższy przykład zawiera pola publiczne i formaty danych wyjściowych JSON.

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

Ponieważ pola są uwzględniane, powyższy kod odczytuje NameField i zawiera je w wyjściowym formacie JSON.

Konfigurowanie opcji serializacji JSON dla punktu końcowego

Aby skonfigurować opcje serializacji dla punktu końcowego, wywołaj Results.Json i przekaż do niego JsonSerializerOptions obiekt, jak pokazano w poniższym przykładzie:

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

Alternatywnie należy użyć przeciążenia WriteAsJsonAsync , które akceptuje JsonSerializerOptions obiekt. W poniższym przykładzie użyto tego przeciążenia do sformatowania wyjściowego kodu 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
// }

Dodatkowe zasoby