Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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:
-
string- Ini termasukTask<string>danValueTask<string>. -
T(Jenis lainnya) - Ini termasukTask<T>danValueTask<T>. -
IResultberbasis - Ini termasukTask<IResult>danValueTask<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:
-
TypedResultsfungsi pembantu mengembalikan objek dengan tipe yang kuat, yang dapat meningkatkan keterbacaan kode, pengujian unit, dan mengurangi kemungkinan kesalahan pada waktu runtime. - Jenis implementasi secara otomatis menyediakan metadata jenis respons untuk OpenAPI untuk menjelaskan titik akhir.
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
IResultjenis implementasi dikembalikan dari handler endpoint. - Kelas statis
TypedResultdigunakan untuk membuatIResultobjek.
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:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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:
-
string- Ini termasukTask<string>danValueTask<string>. -
T(Jenis lainnya) - Ini termasukTask<T>danValueTask<T>. -
IResultberbasis - Ini termasukTask<IResult>danValueTask<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:
-
TypedResultsfungsi pembantu mengembalikan objek dengan tipe yang kuat, yang dapat meningkatkan keterbacaan kode, pengujian unit, dan mengurangi kemungkinan kesalahan pada waktu runtime. - Jenis implementasi secara otomatis menyediakan metadata jenis respons untuk OpenAPI untuk menjelaskan titik akhir.
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
IResultjenis implementasi dikembalikan dari handler endpoint. - Kelas statis
TypedResultdigunakan untuk membuatIResultobjek.
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:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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:
-
string- Ini termasukTask<string>danValueTask<string>. -
T(Jenis lainnya) - Ini termasukTask<T>danValueTask<T>. -
IResultberbasis - Ini termasukTask<IResult>danValueTask<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:
-
TypedResultsfungsi pembantu mengembalikan objek dengan tipe yang kuat, yang dapat meningkatkan keterbacaan kode, pengujian unit, dan mengurangi kemungkinan kesalahan pada waktu runtime. - Jenis implementasi secara otomatis menyediakan metadata jenis respons untuk OpenAPI untuk menjelaskan titik akhir.
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
IResultjenis implementasi dikembalikan dari handler endpoint. - Kelas statis
TypedResultdigunakan untuk membuatIResultobjek.
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:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
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
ASP.NET Core