Cara membuat respons di aplikasi API Minimal
Catatan
Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 9 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.
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 9 dari artikel ini.
Titik akhir minimal mendukung jenis nilai pengembalian berikut:
-
string
- Ini termasukTask<string>
danValueTask<string>
. -
T
(Jenis lainnya) - Ini termasukTask<T>
danValueTask<T>
. -
IResult
berbasis - Ini termasukTask<IResult>
danValueTask<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 menserialisasikan 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 IResult
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:
-
TypedResults
pembantu mengembalikan objek yang sangat diketik, yang dapat meningkatkan keterbacaan kode, pengujian unit, dan mengurangi kemungkinan kesalahan runtime. - Jenis implementasi secara otomatis menyediakan metadata jenis respons untuk OpenAPI untuk menjelaskan titik akhir.
Pertimbangkan titik akhir berikut, di mana 200 OK
kode status dengan respons JSON yang diharapkan dihasilkan.
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.Ok
Results.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 membuatIResult
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.
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 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:
- 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 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 OK
status .
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 JSON output.
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 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:
-
string
- Ini termasukTask<string>
danValueTask<string>
. -
T
(Jenis lainnya) - Ini termasukTask<T>
danValueTask<T>
. -
IResult
berbasis - Ini termasukTask<IResult>
danValueTask<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 menserialisasikan 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 IResult
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:
-
TypedResults
pembantu mengembalikan objek yang sangat diketik, yang dapat meningkatkan keterbacaan kode, pengujian unit, dan mengurangi kemungkinan kesalahan runtime. - Jenis implementasi secara otomatis menyediakan metadata jenis respons untuk OpenAPI untuk menjelaskan titik akhir.
Pertimbangkan titik akhir berikut, di mana 200 OK
kode status dengan respons JSON yang diharapkan dihasilkan.
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.Ok
Results.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 membuatIResult
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.
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 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:
- 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 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 OK
status .
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 JSON output.
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 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
ASP.NET Core