ASP.NET Core: Cách tạo phản hồi (response) trong ứng dụng API tối thiểu
Điểm cuối tối thiểu hỗ trợ các kiểu giá trị trả về sau:
string
- Bao gồmTask<string>
vàValueTask<string>
.T
(Bất kỳ kiểu nào) - Bao gồmTask<T>
vàValueTask<T>
.IResult
dựa trên - Bao gồmTask<IResult>
vàValueTask<IResult>
.
Giá trị trả về kiểu string
Hành vi | Loại nội dung |
---|---|
Framework viết chuỗi trực tiếp vào phản hồi. | text/plain |
Xem xét trình xử lý route sau, trả về văn bản là Hello world
.
app.MapGet("/hello", () => "Hello World");
Mã trạng thái 200
được trả về với header Content-Type text/plain
và nội dung sau.
Hello World
Trả về giá trị T
(Bất kỳ kiểu nào)
Hành vi | Loại nội dung |
---|---|
Framework JSON tuần tự hóa phản hồi. | application/json |
Hãy xem xét trình xử lý định tuyến sau, trình xử lý này trả về một kiểu ẩn danh có chứa property chuỗi Message
.
app.MapGet("/hello", () => new { Message = "Hello World" });
Mã trạng thái 200
được trả về với header Content-Type application/json
và nội dung sau.
{"message":"Hello World"}
Giá trị trả về kiểu IResult
Hành vi | Loại nội dung |
---|---|
Framework gọi IResult.ExecuteAsync. | Quyết định bởi việc thực thi IResult . |
Interface IResult
xác định một hợp đồng đại diện cho kết quả của một điểm cuối HTTP. Lớp Result tĩnh và TypedResults tĩnh được sử dụng để tạo các đối tượng IResult
khác nhau đại diện cho các loại phản hồi khác nhau.
TypedResults so với Results
Các lớp tĩnh Results và TypedResults cung cấp các trình trợ giúp kết quả tương tự. Lớp TypedResults
là tương đương với typed của lớp Results
. Tuy nhiên, kiểu trả về của trình trợ giúp Results
là IResult, trong khi mỗi kiểu trả về của trình trợ giúp TypedResults
là một trong các kiểu thực thi IResult
. Sự khác biệt có nghĩa là đối với trình trợ giúp Results
, việc chuyển đổi là cần thiết khi cần kiểu cụ thể, ví dụ như để thử nghiệm đơn vị. Các kiểu thực thi được xác định trong namespace Microsoft.AspNetCore.Http.HttpResults.
Trả về TypedResults
thay vì Results
có những lợi thế sau:
- Trình trợ giúp
TypedResults
zzztrả về các đối tượng được nhập mạnh, có thể cải thiện khả năng đọc mã, kiểm tra đơn vị và giảm khả năng xảy ra lỗi thời gian chạy. - Loại triển khai tự động cung cấp siêu dữ liệu loại phản hồi cho OpenAPI để mô tả điểm cuối.
Hãy xem xét điểm cuối sau đây, mã 200 OK
trạng thái có phản hồi JSON dự kiến được tạo.
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
Để ghi lại chính xác điểm cuối này, phương thức tiện ích mở rộng Produces
được gọi. Tuy nhiên, không cần thiết phải gọi Produces
if TypedResults
được sử dụng thay vì Results
, như được hiển thị trong đoạn mã sau. TypedResults
tự động cung cấp siêu dữ liệu cho điểm cuối.
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Để biết thêm thông tin về cách mô tả loại phản hồi, hãy xem hỗ trợ OpenAPI trong các API tối thiểu .
Như đã đề cập trước đây, khi sử dụng TypedResults
, không cần chuyển đổi. Hãy xem xét API tối thiểu sau đây trả về một TypedResults
lớp
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
Phép thử sau đây kiểm tra loại bê tông đầy đủ:
[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);
});
}
Bởi vì tất cả các phương thức được Results
trả về IResult
trong chữ ký của chúng, trình biên dịch sẽ tự động suy ra đó là kiểu trả về của đại biểu yêu cầu khi trả về các kết quả khác nhau từ một điểm cuối duy nhất. TypedResults
yêu cầu sử dụng Results<T1, TN>
từ đại biểu như vậy.
Phương thức sau đây biên dịch vì cả Results.Ok và Results.NotFound đều được khai báo là return IResult
, mặc dù kiểu cụ thể thực tế của các đối tượng được trả về là khác nhau:
app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Phương thức sau đây không biên dịch, bởi vì TypedResults.Ok
và TypedResults.NotFound
được khai báo là trả về các kiểu khác nhau và trình biên dịch sẽ không cố suy ra kiểu phù hợp nhất:
app.MapGet("/todos/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Để sử dụng TypedResults
, kiểu trả về phải được khai báo đầy đủ, khi không đồng bộ yêu cầu Task<>
trình bao bọc. Việc sử dụng TypedResults
dài dòng hơn, nhưng đó là sự đánh đổi để có sẵn thông tin loại tĩnh và do đó có khả năng tự mô tả thành OpenAPI:
app.MapGet("/todos/{id}", async Task<Results<Ok<Todo>, NotFound>> (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
Kết quả<TResult1, TResultN>
Sử dụng Kết quả<TResult1, TResultN> làm loại trả về trình xử lý điểm cuối thay vì IResult
khi:
- Nhiều
IResult
loại triển khai được trả về từ trình xử lý điểm cuối. - Lớp tĩnh
TypedResult
được sử dụng để tạoIResult
các đối tượng.
Phương án thay thế này tốt hơn là quay lại IResult
vì các loại kết hợp chung tự động giữ lại siêu dữ liệu điểm cuối. Và vì Results<TResult1, TResultN>
các kiểu kết hợp triển khai các toán tử ép kiểu ẩn, nên trình biên dịch có thể tự động chuyển đổi các kiểu được chỉ định trong các đối số chung thành một thể hiện của kiểu kết hợp.
Điều này có thêm lợi ích là cung cấp kiểm tra thời gian biên dịch rằng trình xử lý tuyến đường thực sự chỉ trả về kết quả mà nó tuyên bố. Cố gắng trả về một loại không được khai báo là một trong các đối số chung dẫn Results<>
đến lỗi biên dịch.
Hãy xem xét điểm cuối sau đây, mã 400 BadRequest
trạng thái được trả về khi orderId
lớn hơn 999
. Mặt khác, nó tạo ra một 200 OK
nội dung mong đợi.
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
Để ghi lại chính xác điểm cuối này, phương thức mở rộng Produces
được gọi. Tuy nhiên, vì TypedResults
trình trợ giúp tự động bao gồm siêu dữ liệu cho điểm cuối, nên Results<T1, Tn>
thay vào đó, bạn có thể trả về loại kết hợp, như được minh họa trong đoạn mã sau.
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
Kết quả tích hợp
Trình trợ giúp kết quả phổ biến tồn tại trong các lớp tĩnh Kết quả và TypedResults . Quay lại TypedResults
được ưu tiên hơn quay lại Results
. Để biết thêm thông tin, hãy xem TypedResults so với Kết quả .
Các phần sau đây minh họa cách sử dụng các trình trợ giúp kết quả phổ biến.
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync là một cách khác để trả về JSON:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
Mã trạng thái tùy chỉnh
app.MapGet("/405", () => Results.StatusCode(405));
Chữ
app.MapGet("/text", () => Results.Text("This is some text"));
Suối
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://consoto/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Quá tải results.Stream cho phép truy cập vào luồng phản hồi HTTP bên dưới mà không cần lưu vào bộ đệm. Ví dụ sau sử dụng ImageSharp để trả về kích thước đã giảm của hình ảnh đã chỉ định:
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);
}
Ví dụ sau truyền hình ảnh từ bộ lưu trữ 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");
});
Ví dụ sau truyền phát video từ 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);
});
chuyển hướng
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Tài liệu
app.MapGet("/download", () => Results.File("myfile.text"));
Giao diện HttpResult
Các giao diện sau trong không gian tên Microsoft.AspNetCore.Http cung cấp một cách để phát hiện IResult
loại trong thời gian chạy, đây là mẫu phổ biến trong triển khai bộ lọc:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
Dưới đây là ví dụ về bộ lọc sử dụng một trong các giao diện sau:
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
};
});
Để biết thêm thông tin, hãy xem Bộ lọc trong các ứng dụng API tối thiểu và các loại triển khai IResult .
tùy chỉnh câu trả lời
Các ứng dụng có thể kiểm soát phản hồi bằng cách triển khai loại IResult tùy chỉnh . Đoạn mã sau là một ví dụ về loại kết quả 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);
}
}
Chúng tôi khuyên bạn nên thêm phương pháp tiện ích mở rộng vào Microsoft.AspNetCore.Http.IResultExtensions để làm cho các kết quả tùy chỉnh này dễ khám phá hơn.
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();
Ngoài ra, một IResult
loại tùy chỉnh có thể cung cấp chú thích của riêng nó bằng cách triển khai giao diện IEndpointMetadataProvider . Ví dụ: đoạn mã sau thêm chú thích vào HtmlResult
loại trước mô tả phản hồi do điểm cuối tạo ra.
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());
}
}
Việc ProducesHtmlMetadata
triển khai IProducesResponseTypeMetadata xác định loại nội dung phản hồi được tạo text/html
và mã trạng thái 200 OK
.
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
Một cách tiếp cận khác là sử dụng Microsoft.AspNetCore.Mvc.ProducesAttribute để mô tả phản hồi được tạo ra. Đoạn mã sau thay đổi PopulateMetadata
phương thức sử dụng ProducesAttribute
.
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
Định cấu hình các tùy chọn tuần tự hóa JSON
Theo mặc định, các ứng dụng API tối thiểu sử dụng các tùy chọn mặc định của Web trong quá trình tuần tự hóa và giải tuần tự hóa JSON.
Định cấu hình các tùy chọn tuần tự hóa JSON trên toàn cầu
Các tùy chọn có thể được định cấu hình trên toàn cầu cho một ứng dụng bằng cách gọi ConfigureHttpJsonOptions . Ví dụ sau bao gồm các trường công khai và định dạng đầu ra 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
// }
Vì các trường được bao gồm nên mã trước đó sẽ đọc NameField
và đưa nó vào JSON đầu ra.
Định cấu hình tùy chọn tuần tự hóa JSON cho điểm cuối
Để định cấu hình các tùy chọn tuần tự hóa cho một điểm cuối, hãy gọi Results.Json và truyền cho nó một đối tượng JsonSerializerOptions , như minh họa trong ví dụ sau:
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
// }
Thay vào đó, hãy sử dụng quá tải WriteAsJsonAsync chấp nhận đối tượng JsonSerializerOptions . Ví dụ sau sử dụng quá tải này để định dạng JSON đầu ra:
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
// }