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 .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 .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 .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> i ValueTask<T>.
  3. IResult oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world.

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

Kod stanu 200 jest zwracany wraz z nagłówkiem Content-Type text/plain oraz 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 stanu 200 jest zwracany wraz z nagłówkiem Content-Type application/json oraz 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 kontra wyniki

Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults jest typowanym odpowiednikiem klasy Results. Jednak Results zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults pomocnika jest jednym z IResult typów implementacji. Różnica polega na tym, że dla Results pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.

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

Rozważ następujący punkt końcowy, dla którego generowany jest kod stanu 200 OK 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 rozszerzeń Produces. 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 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ący test sprawdza pełny typ konkretnego obiektu.

[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 w Results zwracają IResult w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego. TypedResults wymaga użycia Results<T1, TN> przez takich delegatów.

Następująca metoda kompiluje się, ponieważ zarówno Results.Ok, jak i Results.NotFound są zadeklarowane jako zwracające 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ż TypedResults.Ok i TypedResults.NotFound są zadeklarowane jako zwracające różne typy, a 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ć TypedResults, typ zwracany musi być w pełni zadeklarowany. Gdy metoda jest asynchroniczna, deklaracja wymaga opakowania typu zwracanego w Task<>. Użycie TypedResults jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult w następujących przypadkach:

  • Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>, skutkuje błędem kompilacji.

Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest jest zwracany, gdy orderId jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces. 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 rezultaty

Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults jest preferowane od zwracania Results. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.

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

JSON (JavaScript Object Notation)

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.

Problem i walidacja problemu

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

Tekst

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

Strumień

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

zdarzenia Server-Sent (SSE)

Interfejs API TypedResults.ServerSentEvents obsługuje zwracanie wyniku ServerSentEvents .

Server-Sent Events to technologia serwerowa typu push, która umożliwia serwerowi wysyłanie strumienia wiadomości o zdarzeniach do klienta za pośrednictwem jednego połączenia HTTP. Na platformie .NET komunikaty o zdarzeniach są reprezentowane jako SseItem<T> obiekty, które mogą zawierać typ zdarzenia, identyfikator i ładunek danych typu T.

Klasa TypedResults ma metodę statyczną o nazwie ServerSentEvents , która może służyć do zwracania ServerSentEvents wyniku. Pierwszym parametrem tej metody jest to IAsyncEnumerable<SseItem<T>> , który reprezentuje strumień komunikatów zdarzeń, które mają być wysyłane do klienta.

W poniższym przykładzie pokazano, jak używać interfejsu TypedResults.ServerSentEvents API do zwracania strumienia zdarzeń tętna jako obiektów JSON do klienta:

app.MapGet("sse-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<SseItem<int>> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return new SseItem<int>(heartRate, eventType: "heartRate")
            {
                ReconnectionInterval = TimeSpan.FromMinutes(1)
            };
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken));
});

Aby uzyskać więcej informacji, zobacz przykładową aplikację Minimal API zwracającą strumień zdarzeń tętna jako ciąg, TypedResults.ServerSentEvents oraz obiekty JSON do klienta.

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.

Modyfikowanie nagłówków

Użyj obiektu HttpResponse, aby zmodyfikować nagłówki odpowiedzi:

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

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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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 implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html i kod stanu 200 OK.

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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata, aby używać ProducesAttribute.

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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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> i ValueTask<T>.
  3. IResult oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world.

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

Kod stanu 200 jest zwracany wraz z nagłówkiem Content-Type text/plain oraz 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 stanu 200 jest zwracany wraz z nagłówkiem Content-Type application/json oraz 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 kontra wyniki

Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults jest typowanym odpowiednikiem klasy Results. Jednak Results zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults pomocnika jest jednym z IResult typów implementacji. Różnica polega na tym, że dla Results pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.

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

Rozważ następujący punkt końcowy, dla którego generowany jest kod stanu 200 OK 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 rozszerzeń Produces. 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 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ący test sprawdza pełny typ konkretnego obiektu.

[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 w Results zwracają IResult w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego. TypedResults wymaga użycia Results<T1, TN> przez takich delegatów.

Następująca metoda kompiluje się, ponieważ zarówno Results.Ok, jak i Results.NotFound są zadeklarowane jako zwracające 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ż TypedResults.Ok i TypedResults.NotFound są zadeklarowane jako zwracające różne typy, a 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ć TypedResults, typ zwracany musi być w pełni zadeklarowany, co, jeśli jest asynchroniczny, wymaga Task<> otoki. Użycie TypedResults jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult w następujących przypadkach:

  • Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>, skutkuje błędem kompilacji.

Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest jest zwracany, gdy orderId jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces. 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 rezultaty

Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults jest preferowane od zwracania Results. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.

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

JSON (JavaScript Object Notation)

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.

Problem i walidacja problemu

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

Tekst

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

Strumień

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.

Modyfikowanie nagłówków

Użyj obiektu HttpResponse, aby zmodyfikować nagłówki odpowiedzi:

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

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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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 implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html i kod stanu 200 OK.

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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata, aby używać ProducesAttribute.

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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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> i ValueTask<T>.
  3. IResult oparty — 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ę obsługi ścieżki, która zwraca tekst Hello world.

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

Kod stanu 200 jest zwracany wraz z nagłówkiem Content-Type text/plain oraz 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 stanu 200 jest zwracany wraz z nagłówkiem Content-Type application/json oraz 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 kontra wyniki

Klasy Results i TypedResults statyczne udostępniają podobne zestawy pomocników wyników. Klasa TypedResults jest typowanym odpowiednikiem klasy Results. Jednak Results zwracany typ pomocnika to IResult, a zwracany typ każdego TypedResults pomocnika jest jednym z IResult typów implementacji. Różnica polega na tym, że dla Results pomocników konieczna jest konwersja, gdy potrzebny jest konkretny typ, na przykład podczas testowania jednostkowego. Typy implementacji są definiowane w przestrzeni nazw Microsoft.AspNetCore.Http.HttpResults.

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

Rozważ następujący punkt końcowy, dla którego generowany jest kod stanu 200 OK 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 rozszerzeń Produces. 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 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ący test sprawdza pełny typ konkretnego obiektu.

[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 w Results zwracają IResult w swojej sygnaturze, kompilator automatycznie wywnioskuje, że delegat żądania ma taki typ zwracany, gdy zwracane są różne wyniki z jednego punktu końcowego. TypedResults wymaga użycia Results<T1, TN> przez takich delegatów.

Następująca metoda kompiluje się, ponieważ zarówno Results.Ok, jak i Results.NotFound są zadeklarowane jako zwracające 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ż TypedResults.Ok i TypedResults.NotFound są zadeklarowane jako zwracające różne typy, a 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ć TypedResults, typ zwracany musi być w pełni zadeklarowany, co, jeśli jest asynchroniczny, wymaga Task<> otoki. Użycie TypedResults jest bardziej rozbudowane, ale to jest kompromis związany z tym, że informacje o typie są statycznie dostępne i mogą samodzielnie opisywać interfejs 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 zamiast IResult w następujących przypadkach:

  • Obsługiwacz punktu końcowego zwraca 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ę umożliwienia sprawdzenia w czasie kompilacji, czy handler trasy rzeczywiście zwraca wyłącznie wyniki, które faktycznie deklaruje. Próba zwrócenia typu, który nie jest zadeklarowany jako jeden z argumentów ogólnych Results<>, skutkuje błędem kompilacji.

Rozważ następujący punkt końcowy, dla którego kod stanu 400 BadRequest jest zwracany, gdy orderId jest większe 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 udokumentować ten punkt końcowy poprawnie, wywoływana jest metoda rozszerzenia Produces. 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 rezultaty

Typowe pomocniki wyników istnieją w klasach statycznych Results i TypedResults. Zwracanie TypedResults jest preferowane od zwracania Results. Aby uzyskać więcej informacji, zobacz TypedResults vs Results.

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

JSON (JavaScript Object Notation)

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

Tekst

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

Strumień

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 do Microsoft.AspNetCore.Http.IResultExtensions, aby zwiększyć 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 implementacją IProducesResponseTypeMetadata, która definiuje typ zawartości odpowiedzi text/html i kod stanu 200 OK.

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 Microsoft.AspNetCore.Mvc.ProducesAttribute do opisania wygenerowanej odpowiedzi. Poniższy kod zmienia metodę PopulateMetadata, aby używać ProducesAttribute.

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 ConfigureHttpJsonOptions. 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 akceptującego obiekt JsonSerializerOptions. 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