Bagikan melalui


Cara membuat respons di aplikasi API Minimal

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 10 dari artikel ini.

Peringatan

Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 9 dari artikel ini.

Artikel ini menjelaskan cara membuat respons untuk titik akhir API minimal di ASP.NET Core. API minimal menyediakan beberapa cara untuk mengembalikan data dan kode status HTTP.

Titik akhir minimal mendukung jenis nilai pengembalian berikut:

  1. string - Ini termasuk Task<string> dan ValueTask<string>.
  2. T (Jenis lainnya) - Ini termasuk Task<T> dan ValueTask<T>.
  3. IResult berbasis - Ini termasuk Task<IResult> dan ValueTask<IResult>.

Penting

Dimulai dengan ASP.NET Core 10, titik akhir API yang diketahui tidak lagi dialihkan ke halaman masuk saat menggunakan cookie autentikasi. Sebaliknya, mereka mengembalikan kode status 401/403. Untuk detailnya, lihat Perilaku autentikasi titik akhir API di ASP.NET Core.

string mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menulis string langsung ke respons. text/plain

Pertimbangkan handler rute berikut, yang mengembalikan teks Hello world.

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

Kode 200 status dikembalikan dengan text/plain header Tipe Konten dan konten berikut.

Hello World

T (Tipe lainnya) mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menserialisasikan respons menjadi JSON. application/json

Pertimbangkan handler rute berikut, yang mengembalikan jenis anonim berisi properti string Message.

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

Kode 200 status dikembalikan dengan application/json header Tipe Konten dan konten berikut.

{"message":"Hello World"}

IResult mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja memanggil IResult.ExecuteAsync. Implementasi IResult yang memutuskan.

Antarmuka IResult mendefinisikan kontrak yang mewakili hasil titik akhir HTTP. Kelas statis Results dan TypedResults digunakan untuk membuat berbagai objek IResult yang mewakili jenis respons yang berbeda.

TypedResults vs Hasil

Kelas Results statis dan TypedResults menyediakan set pembantu hasil yang sama. Kelas TypedResults adalah setara bertipe dari kelas . Namun, jenis pengembalian dari pembantu Results adalah IResult, sedangkan jenis pengembalian dari setiap pembantu TypedResults adalah salah satu jenis implementasi IResult. Perbedaan ini berarti bahwa konversi dibutuhkan untuk Results helpers ketika tipe konkret dibutuhkan, misalnya, untuk pengujian unit. Jenis implementasi didefinisikan dalam namespace Microsoft.AspNetCore.Http.HttpResults.

Mengembalikan TypedResults daripada Results memiliki keuntungan berikut:

Pertimbangkan endpoint berikut, di mana kode status 200 OK dengan respons JSON yang diharapkan dihasilkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode ekstensi Produces dipanggil. Namun, tidak perlu memanggil Produces jika TypedResults digunakan alih-alih Results, seperti yang ditunjukkan dalam kode berikut. TypedResults secara otomatis menyediakan metadata untuk titik akhir.

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

Untuk informasi selengkapnya tentang menjelaskan jenis respons, lihat Dukungan OpenAPI dalam API minimal.

Untuk contoh tentang jenis hasil pengujian, lihat dokumentasi Uji.

Karena semua metode ketika Results mengembalikan IResult dalam tanda tangan metode tersebut, pengkompilasi secara otomatis menyimpulkan itu sebagai tipe pengembalian dari delegasi permintaan saat mengembalikan hasil yang berbeda dari satu titik akhir. TypedResults memerlukan penggunaan Results<T1, TN> oleh delegasi tersebut.

Metode berikut telah dikompilasi karena baik Results.Ok dan Results.NotFound dinyatakan sebagai mengembalikan IResult, meskipun jenis konkret aktual dari objek yang dikembalikan berbeda:

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

Metode berikut tidak dikompilasi, karena TypedResults.Ok dan TypedResults.NotFound dinyatakan sebagai mengembalikan jenis yang berbeda dan pengkompilasi tidak akan mencoba menyimpulkan jenis pencocokan terbaik:

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

Untuk menggunakan TypedResults, jenis pengembalian harus sepenuhnya dideklarasikan; ketika metode asinkron, deklarasi memerlukan pembungkusan jenis pengembalian dalam Task<>. Menggunakan TypedResults lebih verbose, tetapi itulah trade-off untuk memiliki informasi tipe tersedia secara statis, sehingga bisa secara otomatis menjelaskan ke 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());

Hasil<TResult1, TResultN>

Gunakan Results<TResult1, TResultN> sebagai jenis pengembalian handler titik akhir alih-alih IResult saat:

  • Beberapa IResult jenis implementasi dikembalikan dari handler endpoint.
  • Kelas statis TypedResult digunakan untuk membuat IResult objek.

Alternatif ini lebih baik daripada mengembalikan IResult karena jenis serikat generik secara otomatis mempertahankan metadata titik akhir. Dan karena Results<TResult1, TResultN> union types menerapkan operator konversi implisit, kompiler dapat secara otomatis mengonversi jenis yang ditentukan dalam argumen generik menjadi instans jenis union.

Ini memiliki manfaat tambahan dengan menyediakan pemeriksaan pada saat kompilasi bahwa handler rute sebenarnya hanya mengembalikan hasil yang dinyatakannya. Mencoba mengembalikan jenis yang tidak dinyatakan sebagai salah satu argumen generik untuk Results<> menghasilkan kesalahan kompilasi.

Pertimbangkan endpoint berikut, yang mana kode status 400 BadRequest akan dikembalikan ketika orderId lebih besar dari 999. Selain itu, ini menghasilkan 200 OK dengan konten yang diharapkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode Produces pada ekstensi dipanggil. Namun, karena pembantu TypedResults secara otomatis menyertakan metadata untuk titik akhir, Anda dapat mengembalikan Results<T1, Tn> jenis gabungan sebagai gantinya, seperti yang ditunjukkan dalam kode berikut.

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

Hasil bawaan

Pengaya hasil umum terdapat di kelas statis Results dan TypedResults. Mengembalikan TypedResults lebih disukai daripada mengembalikan Results. Untuk informasi selengkapnya, lihat TypedResults vs Results.

Bagian berikut mendemonstrasikan penggunaan pembantu hasil yang umum digunakan.

JSON

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

WriteAsJsonAsync adalah cara alternatif untuk mengembalikan JSON:

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

Kode Status Kustom

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

Kesalahan Server Internal

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

Contoh sebelumnya mengembalikan kode status 500.

Masalah dan Validasi Masalah

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

Menyesuaikan respons kesalahan validasi menggunakan IProblemDetailsService

Sesuaikan respons kesalahan dari logika validasi API minimal dengan IProblemDetailsService implementasi. Daftarkan layanan ini dalam kumpulan layanan aplikasi Anda untuk mengaktifkan respons kesalahan yang lebih konsisten dan spesifik pengguna. Dukungan untuk validasi API minimal diperkenalkan di ASP.NET Core di .NET 10.

Untuk menerapkan respons kesalahan validasi kustom:

  • Menerapkan IProblemDetailsService atau menggunakan implementasi default
  • Mendaftarkan layanan dalam kontainer DI
  • Sistem validasi secara otomatis menggunakan layanan terdaftar untuk memformat respons kesalahan validasi

Contoh berikut menunjukkan cara mendaftar dan mengonfigurasi IProblemDetailsService untuk menyesuaikan respons kesalahan validasi:

using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        if (context.ProblemDetails.Status == 400)
        {
            context.ProblemDetails.Title = "Validation error occurred";
            context.ProblemDetails.Extensions["support"] = "Contact support@example.com";
            context.ProblemDetails.Extensions["traceId"] = Guid.NewGuid().ToString();
        }
    };
});

Ketika kesalahan validasi terjadi, IProblemDetailsService akan digunakan untuk menghasilkan respons kesalahan, termasuk kustomisasi apa pun yang CustomizeProblemDetails ditambahkan dalam panggilan balik.

Untuk contoh aplikasi lengkap, lihat aplikasi sampel API Minimal yang menunjukkan cara mengkustomisasi respons kesalahan validasi menggunakan IProblemDetailsService api inti minimal ASP.NET.

Teks

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 kelebihan beban memungkinkan akses ke aliran respons HTTP yang mendasar tanpa buffering. Contoh berikut menggunakan ImageSharp untuk mengembalikan ukuran gambar yang ditentukan yang dikurangi:

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

Contoh berikut mengalirkan gambar dari 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");
});

Contoh berikut memutar video dari Azure Blob.

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Peristiwa Server-Sent (SSE)

API TypedResults.ServerSentEvents mendukung pengembalian hasil ServerSentEvents.

Server-Sent Events adalah teknologi pendorongan server yang memungkinkan server mengirim aliran pesan peristiwa ke klien melalui satu koneksi HTTP. Di .NET, pesan peristiwa direpresentasikan sebagai SseItem<T> objek, yang mungkin berisi jenis peristiwa, ID, dan payload data jenis T.

Kelas TypedResults memiliki metode statis yang disebut ServerSentEvents yang dapat digunakan untuk mengembalikan hasil ServerSentEvents . Parameter pertama untuk metode ini adalah IAsyncEnumerable<SseItem<T>> yang mewakili aliran pesan peristiwa yang akan dikirim ke klien.

Contoh berikut mengilustrasikan cara menggunakan TypedResults.ServerSentEvents API untuk mengembalikan aliran peristiwa denyut jantung sebagai objek JSON kepada klien:

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

Untuk informasi selengkapnya, lihat aplikasi sampel API Minimal menggunakan TypedResults.ServerSentEvents API untuk mengembalikan aliran peristiwa denyut jantung sebagai string, ServerSentEvents, dan objek JSON ke klien.

Pengalihan

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

Arsip

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

Antarmuka HttpResult

Antarmuka berikut di namespace Microsoft.AspNetCore.Http menyediakan cara untuk mendeteksi tipe IResult pada runtime, yang merupakan pola umum dalam implementasi filter:

Berikut adalah contoh filter yang menggunakan salah satu antarmuka ini:

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

Untuk informasi selengkapnya, lihat Filter di aplikasi API Minimal dan jenis implementasi IResult.

Mengubah Tajuk

Gunakan objek HttpResponse untuk mengubah header respons:

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

Menyesuaikan respons

Aplikasi dapat mengontrol respons dengan menerapkan jenis kustom IResult . Kode berikut adalah contoh jenis hasil 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);
    }
}

Sebaiknya tambahkan metode ekstensi untuk Microsoft.AspNetCore.Http.IResultExtensions membuat hasil kustom ini lebih dapat ditemukan.

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

Selain itu, jenis kustom IResult dapat memberikan anotasi dengan mengimplementasikan antarmuka IEndpointMetadataProvider sendiri. Misalnya, kode berikut menambahkan anotasi ke jenis sebelumnya HtmlResult yang menjelaskan respons yang dihasilkan oleh titik akhir.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata adalah implementasi IProducesResponseTypeMetadata yang mendefinisikan jenis konten respons text/html yang dihasilkan dan kode status 200 OK.

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

    public int StatusCode => 200;

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

Pendekatan alternatif adalah menggunakan Microsoft.AspNetCore.Mvc.ProducesAttribute untuk menggambarkan respons yang dihasilkan. Kode berikut mengubah PopulateMetadata metode untuk menggunakan ProducesAttribute.

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

Mengonfigurasi opsi serialisasi JSON

Secara default, aplikasi API minimal menggunakan Web defaults opsi selama serialisasi dan deserialisasi JSON.

Mengonfigurasi opsi serialisasi JSON secara global

Opsi dapat dikonfigurasi secara global untuk aplikasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format output 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
// }

Karena bidang disertakan, kode sebelumnya membaca NameField dan menyertakannya dalam output JSON.

Mengonfigurasi opsi serialisasi JSON untuk titik akhir

Untuk mengonfigurasi opsi serialisasi untuk titik akhir, panggil Results.Json dan teruskan objek JsonSerializerOptions , seperti yang ditunjukkan dalam contoh berikut:

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

Sebagai alternatif, gunakan pendefinisian ulang fungsi WriteAsJsonAsync yang menerima JsonSerializerOptions sebagai parameter. Contoh berikut menggunakan kelebihan beban ini untuk memformat output 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
// }

Sumber Tambahan

Titik akhir minimal mendukung jenis nilai pengembalian berikut:

  1. string - Ini termasuk Task<string> dan ValueTask<string>.
  2. T (Jenis lainnya) - Ini termasuk Task<T> dan ValueTask<T>.
  3. IResult berbasis - Ini termasuk Task<IResult> dan ValueTask<IResult>.

string mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menulis string langsung ke respons. text/plain

Pertimbangkan handler rute berikut, yang mengembalikan teks Hello world.

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

Kode 200 status dikembalikan dengan text/plain header Tipe Konten dan konten berikut.

Hello World

T (Tipe lainnya) mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menserialisasikan respons menjadi JSON. application/json

Pertimbangkan handler rute berikut, yang mengembalikan jenis anonim berisi properti string Message.

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

Kode 200 status dikembalikan dengan application/json header Tipe Konten dan konten berikut.

{"message":"Hello World"}

IResult mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja memanggil IResult.ExecuteAsync. Implementasi IResult yang memutuskan.

Antarmuka IResult mendefinisikan kontrak yang mewakili hasil titik akhir HTTP. Kelas statis Results dan TypedResults digunakan untuk membuat berbagai objek IResult yang mewakili jenis respons yang berbeda.

TypedResults vs Hasil

Kelas Results statis dan TypedResults menyediakan set pembantu hasil yang sama. Kelas TypedResults adalah setara bertipe dari kelas . Namun, jenis pengembalian dari pembantu Results adalah IResult, sedangkan jenis pengembalian dari setiap pembantu TypedResults adalah salah satu jenis implementasi IResult. Perbedaan ini berarti bahwa konversi dibutuhkan untuk Results helpers ketika tipe konkret dibutuhkan, misalnya, untuk pengujian unit. Jenis implementasi didefinisikan dalam namespace Microsoft.AspNetCore.Http.HttpResults.

Mengembalikan TypedResults daripada Results memiliki keuntungan berikut:

Pertimbangkan endpoint berikut, di mana kode status 200 OK dengan respons JSON yang diharapkan dihasilkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode ekstensi Produces dipanggil. Namun, tidak perlu memanggil Produces jika TypedResults digunakan alih-alih Results, seperti yang ditunjukkan dalam kode berikut. TypedResults secara otomatis menyediakan metadata untuk titik akhir.

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

Untuk informasi selengkapnya tentang menjelaskan jenis respons, lihat Dukungan OpenAPI dalam API minimal.

Seperti disebutkan sebelumnya, saat menggunakan TypedResults, konversi tidak diperlukan. Pertimbangkan API minimal berikut yang mengembalikan kelas TypedResults

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

Pemeriksaan berikut memeriksa tipe beton sepenuhnya.

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

Karena semua metode ketika Results mengembalikan IResult dalam tanda tangan metode tersebut, pengkompilasi secara otomatis menyimpulkan itu sebagai tipe pengembalian dari delegasi permintaan saat mengembalikan hasil yang berbeda dari satu titik akhir. TypedResults memerlukan penggunaan Results<T1, TN> oleh delegasi tersebut.

Metode berikut telah dikompilasi karena baik Results.Ok dan Results.NotFound dinyatakan sebagai mengembalikan IResult, meskipun jenis konkret aktual dari objek yang dikembalikan berbeda:

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

Metode berikut tidak dikompilasi, karena TypedResults.Ok dan TypedResults.NotFound dinyatakan sebagai mengembalikan jenis yang berbeda dan pengkompilasi tidak akan mencoba menyimpulkan jenis pencocokan terbaik:

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

Untuk menggunakan TypedResults, jenis pengembalian harus sepenuhnya dideklarasikan, yang dalam kondisi asinkron memerlukan Task<> sebagai pembungkus. Menggunakan TypedResults lebih verbose, tetapi itulah trade-off untuk memiliki informasi tipe tersedia secara statis, sehingga bisa secara otomatis menjelaskan ke 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());

Hasil<TResult1, TResultN>

Gunakan Results<TResult1, TResultN> sebagai jenis pengembalian handler titik akhir alih-alih IResult saat:

  • Beberapa IResult jenis implementasi dikembalikan dari handler endpoint.
  • Kelas statis TypedResult digunakan untuk membuat IResult objek.

Alternatif ini lebih baik daripada mengembalikan IResult karena jenis serikat generik secara otomatis mempertahankan metadata titik akhir. Dan karena Results<TResult1, TResultN> union types menerapkan operator konversi implisit, kompiler dapat secara otomatis mengonversi jenis yang ditentukan dalam argumen generik menjadi instans jenis union.

Ini memiliki manfaat tambahan dengan menyediakan pemeriksaan pada saat kompilasi bahwa handler rute sebenarnya hanya mengembalikan hasil yang dinyatakannya. Mencoba mengembalikan jenis yang tidak dinyatakan sebagai salah satu argumen generik untuk Results<> menghasilkan kesalahan kompilasi.

Pertimbangkan endpoint berikut, yang mana kode status 400 BadRequest akan dikembalikan ketika orderId lebih besar dari 999. Selain itu, ini menghasilkan 200 OK dengan konten yang diharapkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode Produces pada ekstensi dipanggil. Namun, karena pembantu TypedResults secara otomatis menyertakan metadata untuk titik akhir, Anda dapat mengembalikan Results<T1, Tn> jenis gabungan sebagai gantinya, seperti yang ditunjukkan dalam kode berikut.

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

Hasil bawaan

Pengaya hasil umum terdapat di kelas statis Results dan TypedResults. Mengembalikan TypedResults lebih disukai daripada mengembalikan Results. Untuk informasi selengkapnya, lihat TypedResults vs Results.

Bagian berikut mendemonstrasikan penggunaan pembantu hasil yang umum digunakan.

JSON

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

WriteAsJsonAsync adalah cara alternatif untuk mengembalikan JSON:

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

Kode Status Kustom

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

Kesalahan Server Internal

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

Contoh sebelumnya mengembalikan kode status 500.

Masalah dan Validasi Masalah

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

Teks

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 kelebihan beban memungkinkan akses ke aliran respons HTTP yang mendasar tanpa buffering. Contoh berikut menggunakan ImageSharp untuk mengembalikan ukuran gambar yang ditentukan yang dikurangi:

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

Contoh berikut mengalirkan gambar dari 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");
});

Contoh berikut memutar video dari Azure Blob.

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Pengalihan

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

Arsip

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

Antarmuka HttpResult

Antarmuka berikut di namespace Microsoft.AspNetCore.Http menyediakan cara untuk mendeteksi tipe IResult pada runtime, yang merupakan pola umum dalam implementasi filter:

Berikut adalah contoh filter yang menggunakan salah satu antarmuka ini:

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

Untuk informasi selengkapnya, lihat Filter di aplikasi API Minimal dan jenis implementasi IResult.

Mengubah Tajuk

Gunakan objek HttpResponse untuk mengubah header respons:

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

Menyesuaikan respons

Aplikasi dapat mengontrol respons dengan menerapkan jenis kustom IResult . Kode berikut adalah contoh jenis hasil 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);
    }
}

Sebaiknya tambahkan metode ekstensi untuk Microsoft.AspNetCore.Http.IResultExtensions membuat hasil kustom ini lebih dapat ditemukan.

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

Selain itu, jenis kustom IResult dapat memberikan anotasi dengan mengimplementasikan antarmuka IEndpointMetadataProvider sendiri. Misalnya, kode berikut menambahkan anotasi ke jenis sebelumnya HtmlResult yang menjelaskan respons yang dihasilkan oleh titik akhir.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata adalah implementasi IProducesResponseTypeMetadata yang mendefinisikan jenis konten respons text/html yang dihasilkan dan kode status 200 OK.

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

    public int StatusCode => 200;

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

Pendekatan alternatif adalah menggunakan Microsoft.AspNetCore.Mvc.ProducesAttribute untuk menggambarkan respons yang dihasilkan. Kode berikut mengubah PopulateMetadata metode untuk menggunakan ProducesAttribute.

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

Mengonfigurasi opsi serialisasi JSON

Secara default, aplikasi API minimal menggunakan Web defaults opsi selama serialisasi dan deserialisasi JSON.

Mengonfigurasi opsi serialisasi JSON secara global

Opsi dapat dikonfigurasi secara global untuk aplikasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format output 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
// }

Karena bidang disertakan, kode sebelumnya membaca NameField dan menyertakannya dalam output JSON.

Mengonfigurasi opsi serialisasi JSON untuk titik akhir

Untuk mengonfigurasi opsi serialisasi untuk titik akhir, panggil Results.Json dan teruskan objek JsonSerializerOptions , seperti yang ditunjukkan dalam contoh berikut:

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

Sebagai alternatif, gunakan pendefinisian ulang fungsi WriteAsJsonAsync yang menerima JsonSerializerOptions sebagai parameter. Contoh berikut menggunakan kelebihan beban ini untuk memformat output 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
// }

Sumber Tambahan

Titik akhir minimal mendukung jenis nilai pengembalian berikut:

  1. string - Ini termasuk Task<string> dan ValueTask<string>.
  2. T (Jenis lainnya) - Ini termasuk Task<T> dan ValueTask<T>.
  3. IResult berbasis - Ini termasuk Task<IResult> dan ValueTask<IResult>.

string mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menulis string langsung ke respons. text/plain

Pertimbangkan handler rute berikut, yang mengembalikan teks Hello world.

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

Kode 200 status dikembalikan dengan text/plain header Tipe Konten dan konten berikut.

Hello World

T (Tipe lainnya) mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja menserialisasikan respons menjadi JSON. application/json

Pertimbangkan handler rute berikut, yang mengembalikan jenis anonim berisi properti string Message.

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

Kode 200 status dikembalikan dengan application/json header Tipe Konten dan konten berikut.

{"message":"Hello World"}

IResult mengembalikan nilai

Perilaku Jenis-Konten
Kerangka kerja memanggil IResult.ExecuteAsync. Implementasi IResult yang memutuskan.

Antarmuka IResult mendefinisikan kontrak yang mewakili hasil titik akhir HTTP. Kelas statis Results dan TypedResults digunakan untuk membuat berbagai objek IResult yang mewakili jenis respons yang berbeda.

TypedResults vs Hasil

Kelas Results statis dan TypedResults menyediakan set pembantu hasil yang sama. Kelas TypedResults adalah setara bertipe dari kelas . Namun, jenis pengembalian dari pembantu Results adalah IResult, sedangkan jenis pengembalian dari setiap pembantu TypedResults adalah salah satu jenis implementasi IResult. Perbedaan ini berarti bahwa konversi dibutuhkan untuk Results helpers ketika tipe konkret dibutuhkan, misalnya, untuk pengujian unit. Jenis implementasi didefinisikan dalam namespace Microsoft.AspNetCore.Http.HttpResults.

Mengembalikan TypedResults daripada Results memiliki keuntungan berikut:

Pertimbangkan endpoint berikut, di mana kode status 200 OK dengan respons JSON yang diharapkan dihasilkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode ekstensi Produces dipanggil. Namun, tidak perlu memanggil Produces jika TypedResults digunakan alih-alih Results, seperti yang ditunjukkan dalam kode berikut. TypedResults secara otomatis menyediakan metadata untuk titik akhir.

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

Untuk informasi selengkapnya tentang menjelaskan jenis respons, lihat Dukungan OpenAPI dalam API minimal.

Seperti disebutkan sebelumnya, saat menggunakan TypedResults, konversi tidak diperlukan. Pertimbangkan API minimal berikut yang mengembalikan kelas TypedResults

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

Pemeriksaan berikut memeriksa tipe beton sepenuhnya.

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

Karena semua metode ketika Results mengembalikan IResult dalam tanda tangan metode tersebut, pengkompilasi secara otomatis menyimpulkan itu sebagai tipe pengembalian dari delegasi permintaan saat mengembalikan hasil yang berbeda dari satu titik akhir. TypedResults memerlukan penggunaan Results<T1, TN> oleh delegasi tersebut.

Metode berikut telah dikompilasi karena baik Results.Ok dan Results.NotFound dinyatakan sebagai mengembalikan IResult, meskipun jenis konkret aktual dari objek yang dikembalikan berbeda:

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

Metode berikut tidak dikompilasi, karena TypedResults.Ok dan TypedResults.NotFound dinyatakan sebagai mengembalikan jenis yang berbeda dan pengkompilasi tidak akan mencoba menyimpulkan jenis pencocokan terbaik:

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

Untuk menggunakan TypedResults, jenis pengembalian harus sepenuhnya dideklarasikan, yang dalam kondisi asinkron memerlukan Task<> sebagai pembungkus. Menggunakan TypedResults lebih verbose, tetapi itulah trade-off untuk memiliki informasi tipe tersedia secara statis, sehingga bisa secara otomatis menjelaskan ke 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());

Hasil<TResult1, TResultN>

Gunakan Results<TResult1, TResultN> sebagai jenis pengembalian handler titik akhir alih-alih IResult saat:

  • Beberapa IResult jenis implementasi dikembalikan dari handler endpoint.
  • Kelas statis TypedResult digunakan untuk membuat IResult objek.

Alternatif ini lebih baik daripada mengembalikan IResult karena jenis serikat generik secara otomatis mempertahankan metadata titik akhir. Dan karena Results<TResult1, TResultN> union types menerapkan operator konversi implisit, kompiler dapat secara otomatis mengonversi jenis yang ditentukan dalam argumen generik menjadi instans jenis union.

Ini memiliki manfaat tambahan dengan menyediakan pemeriksaan pada saat kompilasi bahwa handler rute sebenarnya hanya mengembalikan hasil yang dinyatakannya. Mencoba mengembalikan jenis yang tidak dinyatakan sebagai salah satu argumen generik untuk Results<> menghasilkan kesalahan kompilasi.

Pertimbangkan endpoint berikut, yang mana kode status 400 BadRequest akan dikembalikan ketika orderId lebih besar dari 999. Selain itu, ini menghasilkan 200 OK dengan konten yang diharapkan.

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

Untuk mendokumentasikan titik akhir ini dengan benar, metode Produces pada ekstensi dipanggil. Namun, karena pembantu TypedResults secara otomatis menyertakan metadata untuk titik akhir, Anda dapat mengembalikan Results<T1, Tn> jenis gabungan sebagai gantinya, seperti yang ditunjukkan dalam kode berikut.

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

Hasil bawaan

Pengaya hasil umum terdapat di kelas statis Results dan TypedResults. Mengembalikan TypedResults lebih disukai daripada mengembalikan Results. Untuk informasi selengkapnya, lihat TypedResults vs Results.

Bagian berikut mendemonstrasikan penggunaan pembantu hasil yang umum digunakan.

JSON

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

WriteAsJsonAsync adalah cara alternatif untuk mengembalikan JSON:

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

Kode Status Kustom

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

Teks

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 kelebihan beban memungkinkan akses ke aliran respons HTTP yang mendasar tanpa buffering. Contoh berikut menggunakan ImageSharp untuk mengembalikan ukuran gambar yang ditentukan yang dikurangi:

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

Contoh berikut mengalirkan gambar dari 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");
});

Contoh berikut memutar video dari Azure Blob.

// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
     async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
    var conStr = builder.Configuration["blogConStr"];
    BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
    BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
    
    var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
    
    DateTimeOffset lastModified = properties.Value.LastModified;
    long length = properties.Value.ContentLength;
    
    long etagHash = lastModified.ToFileTime() ^ length;
    var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
    
    http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";

    return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), 
        contentType: "video/mp4",
        lastModified: lastModified,
        entityTag: entityTag,
        enableRangeProcessing: true);
});

Pengalihan

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

Arsip

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

Antarmuka HttpResult

Antarmuka berikut di namespace Microsoft.AspNetCore.Http menyediakan cara untuk mendeteksi tipe IResult pada runtime, yang merupakan pola umum dalam implementasi filter:

Berikut adalah contoh filter yang menggunakan salah satu antarmuka ini:

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

Untuk informasi selengkapnya, lihat Filter di aplikasi API Minimal dan jenis implementasi IResult.

Menyesuaikan respons

Aplikasi dapat mengontrol respons dengan menerapkan jenis kustom IResult . Kode berikut adalah contoh jenis hasil 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);
    }
}

Sebaiknya tambahkan metode ekstensi untuk Microsoft.AspNetCore.Http.IResultExtensions membuat hasil kustom ini lebih dapat ditemukan.

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

Selain itu, jenis kustom IResult dapat memberikan anotasi dengan mengimplementasikan antarmuka IEndpointMetadataProvider sendiri. Misalnya, kode berikut menambahkan anotasi ke jenis sebelumnya HtmlResult yang menjelaskan respons yang dihasilkan oleh titik akhir.

class HtmlResult : IResult, IEndpointMetadataProvider
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }

    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
    {
        builder.Metadata.Add(new ProducesHtmlMetadata());
    }
}

ProducesHtmlMetadata adalah implementasi IProducesResponseTypeMetadata yang mendefinisikan jenis konten respons text/html yang dihasilkan dan kode status 200 OK.

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

    public int StatusCode => 200;

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

Pendekatan alternatif adalah menggunakan Microsoft.AspNetCore.Mvc.ProducesAttribute untuk menggambarkan respons yang dihasilkan. Kode berikut mengubah PopulateMetadata metode untuk menggunakan ProducesAttribute.

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

Mengonfigurasi opsi serialisasi JSON

Secara default, aplikasi API minimal menggunakan Web defaults opsi selama serialisasi dan deserialisasi JSON.

Mengonfigurasi opsi serialisasi JSON secara global

Opsi dapat dikonfigurasi secara global untuk aplikasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format output 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
// }

Karena bidang disertakan, kode sebelumnya membaca NameField dan menyertakannya dalam output JSON.

Mengonfigurasi opsi serialisasi JSON untuk titik akhir

Untuk mengonfigurasi opsi serialisasi untuk titik akhir, panggil Results.Json dan teruskan objek JsonSerializerOptions , seperti yang ditunjukkan dalam contoh berikut:

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

Sebagai alternatif, gunakan pendefinisian ulang fungsi WriteAsJsonAsync yang menerima JsonSerializerOptions sebagai parameter. Contoh berikut menggunakan kelebihan beban ini untuk memformat output 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
// }

Sumber Tambahan