Cara membuat respons di aplikasi API Minimal

Catatan

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

Penting

Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.

Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.

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 Content-Type
Kerangka kerja menulis string langsung ke respons. text/plain

Pertimbangkan handler rute berikut, yang mengembalikan Hello world teks.

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

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

Hello World

T (Jenis lainnya) mengembalikan nilai

Perilaku Content-Type
Kerangka kerja JSON-serialisasi respons. application/json

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

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 Content-Type
Kerangka kerja memanggil IResult.ExecuteAsync. Diputuskan IResult oleh implementasi.

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

TypedResults vs Hasil

Kelas Results statis dan TypedResults menyediakan set pembantu hasil yang sama. Kelas TypedResults ini setara dengan Results kelas. Namun, jenis pengembalian pembantu Results adalah IResult, sementara setiap TypedResults jenis pengembalian pembantu adalah salah IResult satu jenis implementasi. Perbedaannya berarti bahwa untuk Results pembantu, konversi diperlukan ketika jenis konkret diperlukan, misalnya, untuk pengujian unit. Jenis implementasi didefinisikan dalam Microsoft.AspNetCore.Http.HttpResults namespace layanan.

TypedResults Mengembalikan daripada Results memiliki keuntungan berikut:

Pertimbangkan titik akhir berikut, di mana 200 OK kode status dengan respons ON yang diharapkan JSdiproduksi.

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

Untuk mendokumen titik akhir ini dengan benar, metode Produces ekstensi 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 TypedResults kelas

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

Pengujian berikut memeriksa jenis beton lengkap:

[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 saat Results kembali IResult dalam tanda tangan mereka, pengkompilasi secara otomatis menyimpulkan bahwa sebagai jenis pengembalian delegasi permintaan saat mengembalikan hasil yang berbeda dari satu titik akhir. TypedResults memerlukan penggunaan Results<T1, TN> dari delegasi tersebut.

Metode berikut dikompilasi karena dan Results.OkResults.NotFound dinyatakan sebagai mengembalikan IResult, meskipun jenis beton 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 ketika asinkron memerlukan pembungkus Task<> . Menggunakan TypedResults lebih verbose, tetapi itulah trade-off untuk memiliki informasi jenis tersedia secara statis dan dengan demikian mampu menjelaskan sendiri 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 titik akhir.
  • 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> jenis serikat menerapkan operator cast implisit, pengkompilasi dapat secara otomatis mengonversi jenis yang ditentukan dalam argumen generik ke instans jenis union.

Ini memiliki manfaat tambahan untuk memberikan pemeriksaan waktu 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 titik akhir berikut, yang 400 BadRequest kode statusnya dikembalikan ketika orderId lebih besar dari 999. Jika tidak, ini menghasilkan dengan konten yang 200 OK diharapkan.

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

Untuk mendokumen titik akhir ini dengan benar, metode Produces 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

Pembantu hasil umum ada di Results kelas statis dan TypedResults . TypedResults Mengembalikan lebih disukai untuk mengembalikan Results. Untuk informasi selengkapnya, lihat TypedResults vs Results.

Bagian berikut menunjukkan penggunaan pembantu hasil umum.

JSPADA

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.

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 penyimpanan Azure Blob:

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

File

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

Antarmuka HttpResult

Antarmuka berikut di Microsoft.AspNetCore.Http namespace layanan menyediakan cara untuk mendeteksi IResult jenis 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 anotasinya sendiri dengan mengimplementasikan IEndpointMetadataProvider antarmuka. 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 yang IProducesResponseTypeMetadata mendefinisikan jenis text/html konten respons yang dihasilkan dan kode 200 OKstatus .

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 JSopsi serialisasi ON

Secara default, aplikasi API minimal menggunakan Web defaults opsi selama JSserialisasi DAN deserialisasi ON.

Mengonfigurasi JSopsi serialisasi ON secara global

Opsi dapat dikonfigurasi secara global untuk aplikasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format JSoutput ON.

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 JSAKTIF.

Mengonfigurasi JSopsi serialisasi ON 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 kelebihan beban WriteAsJsonAsync yang menerima JsonSerializerOptions objek. 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 Content-Type
Kerangka kerja menulis string langsung ke respons. text/plain

Pertimbangkan handler rute berikut, yang mengembalikan Hello world teks.

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

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

Hello World

T (Jenis lainnya) mengembalikan nilai

Perilaku Content-Type
Kerangka kerja JSON-serialisasi respons. application/json

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

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 Content-Type
Kerangka kerja memanggil IResult.ExecuteAsync. Diputuskan IResult oleh implementasi.

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

TypedResults vs Hasil

Kelas Results statis dan TypedResults menyediakan set pembantu hasil yang sama. Kelas TypedResults ini setara dengan Results kelas. Namun, jenis pengembalian pembantu Results adalah IResult, sementara setiap TypedResults jenis pengembalian pembantu adalah salah IResult satu jenis implementasi. Perbedaannya berarti bahwa untuk Results pembantu, konversi diperlukan ketika jenis konkret diperlukan, misalnya, untuk pengujian unit. Jenis implementasi didefinisikan dalam Microsoft.AspNetCore.Http.HttpResults namespace layanan.

TypedResults Mengembalikan daripada Results memiliki keuntungan berikut:

Pertimbangkan titik akhir berikut, di mana 200 OK kode status dengan respons ON yang diharapkan JSdiproduksi.

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

Untuk mendokumen titik akhir ini dengan benar, metode Produces ekstensi 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 TypedResults kelas

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

Pengujian berikut memeriksa jenis beton lengkap:

[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 saat Results kembali IResult dalam tanda tangan mereka, pengkompilasi secara otomatis menyimpulkan bahwa sebagai jenis pengembalian delegasi permintaan saat mengembalikan hasil yang berbeda dari satu titik akhir. TypedResults memerlukan penggunaan Results<T1, TN> dari delegasi tersebut.

Metode berikut dikompilasi karena dan Results.OkResults.NotFound dinyatakan sebagai mengembalikan IResult, meskipun jenis beton 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 ketika asinkron memerlukan pembungkus Task<> . Menggunakan TypedResults lebih verbose, tetapi itulah trade-off untuk memiliki informasi jenis tersedia secara statis dan dengan demikian mampu menjelaskan sendiri 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 titik akhir.
  • 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> jenis serikat menerapkan operator cast implisit, pengkompilasi dapat secara otomatis mengonversi jenis yang ditentukan dalam argumen generik ke instans jenis union.

Ini memiliki manfaat tambahan untuk memberikan pemeriksaan waktu 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 titik akhir berikut, yang 400 BadRequest kode statusnya dikembalikan ketika orderId lebih besar dari 999. Jika tidak, ini menghasilkan dengan konten yang 200 OK diharapkan.

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

Untuk mendokumen titik akhir ini dengan benar, metode Produces 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

Pembantu hasil umum ada di Results kelas statis dan TypedResults . TypedResults Mengembalikan lebih disukai untuk mengembalikan Results. Untuk informasi selengkapnya, lihat TypedResults vs Results.

Bagian berikut menunjukkan penggunaan pembantu hasil umum.

JSPADA

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 penyimpanan Azure Blob:

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

File

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

Antarmuka HttpResult

Antarmuka berikut di Microsoft.AspNetCore.Http namespace layanan menyediakan cara untuk mendeteksi IResult jenis 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 anotasinya sendiri dengan mengimplementasikan IEndpointMetadataProvider antarmuka. 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 yang IProducesResponseTypeMetadata mendefinisikan jenis text/html konten respons yang dihasilkan dan kode 200 OKstatus .

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 JSopsi serialisasi ON

Secara default, aplikasi API minimal menggunakan Web defaults opsi selama JSserialisasi DAN deserialisasi ON.

Mengonfigurasi JSopsi serialisasi ON secara global

Opsi dapat dikonfigurasi secara global untuk aplikasi dengan memanggil ConfigureHttpJsonOptions. Contoh berikut mencakup bidang publik dan format JSoutput ON.

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 JSAKTIF.

Mengonfigurasi JSopsi serialisasi ON 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 kelebihan beban WriteAsJsonAsync yang menerima JsonSerializerOptions objek. 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