如何在基本 API 應用程式中建立回應
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
基本端點支援下列型別的傳回值:
string
- 這包括Task<string>
和ValueTask<string>
。T
(任何其他型別) - 這包括Task<T>
和ValueTask<T>
。- 以
IResult
為基礎 - 這包括Task<IResult>
和ValueTask<IResult>
。
string
傳回值
行為 | 內容-類型 |
---|---|
架構會將字串直接寫入回應。 | text/plain |
請考慮下列會傳回 Hello world
文字的路由處理常式。
app.MapGet("/hello", () => "Hello World");
200
狀態碼會以 text/plain
Content-Type 標頭和下列內容傳回。
Hello World
T
(任何其他型別) 傳回值
行為 | 內容-類型 |
---|---|
架構 JSON 序列化回應。 | application/json |
請考慮下列會傳回包含 Message
字串屬性之匿名型別的路由處理常式。
app.MapGet("/hello", () => new { Message = "Hello World" });
200
狀態碼會以 application/json
Content-Type 標頭和下列內容傳回。
{"message":"Hello World"}
IResult
傳回值
行為 | 內容-類型 |
---|---|
架構會呼叫 IResult.ExecuteAsync。 | 由 IResult 實作決定。 |
IResult
介面會定義代表 HTTP 端點結果的合約。 靜態 Results 類別和靜態 TypedResults 可用來建立代表不同回應型別的各種 IResult
物件。
TypedResults 與 Results 的比較
Results 和 TypedResults 靜態類別提供類似的結果協助程式集合。 TypedResults
類別是 Results
類別的同等型別。 不過,Results
協助程式的傳回型別是 IResult,而每個 TypedResults
協助程式的傳回型別則是其中一種 IResult
實作型別。 差異意味著對於 Results
協助程式而言,當需要實體型別時則需要轉換,例如適用於單元測試。 實作型別會在 Microsoft.AspNetCore.Http.HttpResults 命名空間中定義。
傳回 TypedResults
而非 Results
具有下列優勢:
TypedResults
協助程式會傳回強式型別物件,這可改善程式碼可讀性、單元測試,並降低執行階段錯誤的機會。- 實作型別會自動為 OpenAPI 提供回應型別中繼資料,以描述端點。
請考慮下列端點,其中會產生具有預期 JSON 回應的 200 OK
狀態程式碼。
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
為了正確記錄此端點,會呼叫擴充方法 Produces
。 不過,如果是使用 TypedResults
而非 Results
,則不一定需要呼叫 Produces
(如下程式碼所示)。 TypedResults
會自動提供此端點的中繼資料。
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
如需描述回應型別的詳細資訊,請參閱基本 API 中的 OpenAPI 支援。
如先前所述,使用 TypedResults
時則不需要轉換。 請考慮下列會傳回 TypedResults
類別的基本 API
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
下列測試會檢查完整的實體型別:
[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);
});
}
由於所有 Results
上的方法在其簽章中都傳回 IResult
,編譯器會在從單一端點傳回不同的結果時,自動將其推斷為要求委派傳回型別。 TypedResults
需要從這類委派使用 Results<T1, TN>
。
下列方法會進行編譯,因為 Results.Ok
和 Results.NotFound
都宣告為傳回 IResult
,即使傳回之物件的實際實體型別並不相同:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
下列方法不會進行編譯,因為 TypedResults.Ok
和 TypedResults.NotFound
宣告為傳回不同的型別,而且編譯器不會嘗試推斷最佳的比對型別:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
若要使用 TypedResults
,傳回型別必須是完整宣告 (此為當非同步需要 Task<>
包裝函式時)。 使用 TypedResults
會更為詳細,但這是讓型別資訊靜態可用,且因此可對 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());
Results<TResult1, TResultN>
下列情況時,請使用 Results<TResult1, TResultN>
作為端點處理常式傳回型別,而非 IResult
:
- 從端點處理常式傳回多個
IResult
實作型別。 - 使用靜態
TypedResult
類別來建立IResult
物件。
這個替代方法比傳回 IResult
還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN>
等位型別會實作隱含轉換運算子,編譯器可以自動將泛型引數中指定的型別轉換成等位型別的執行個體。
這會有的附加優點為提供編譯時間檢查,確定路由處理常式實際上只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<>
會產生編譯錯誤。
請考慮下列當 orderId
大於 999
時會傳回 400 BadRequest
狀態碼的端點。 否則,它會產生具有預期內容的 200 OK
。
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
為了正確記錄此端點,會呼叫擴充方法 Produces
。 不過,由於 TypedResults
協助程式會自動包含端點的中繼資料,您可以改為傳回 Results<T1, Tn>
等位型別,如下程式碼所示。
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
內建結果
常見的結果協助程式存在於 Results 和 TypedResults 靜態類別中。 相較於傳回 Results
,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
下列各節會說明常見結果協助程式的使用方式。
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync 是傳回 JSON 的替代方式:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
自訂狀態碼
app.MapGet("/405", () => Results.StatusCode(405));
內部伺服器錯誤
app.MapGet("/500", () => Results.InternalServerError("Something went wrong!"));
上述範例會傳回 500 狀態碼。
Text
app.MapGet("/text", () => Results.Text("This is some text"));
資料流
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
多載允許存取基礎 HTTP 回應串流,不需緩衝處理。 下列範例會使用 ImageSharp 以傳回指定映像的縮減大小:
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);
}
下列範例會從 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");
});
下列範例會從 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);
});
重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
檔案
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult 介面
下列 Microsoft.AspNetCore.Http 命名空間中的介面提供在執行階段偵測 IResult
型別的方法,這是篩選實作中的常見模式:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
以下是使用其中這些介面之一的篩選範例:
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
};
});
如需詳細資訊,請參閱最小 API 應用程式中的篩選,以及 IResult 實作型別。
自訂回應
應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 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);
}
}
建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。
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();
此外,自訂 IResult
型別可以藉由實作 IEndpointMetadataProvider 介面來提供自己的註釋。 例如,下列程式碼會將註釋新增至上述 HtmlResult
型別,可描述端點產生的回應。
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
是 IProducesResponseTypeMetadata 的實作,會定義產生的回應內容型別 text/html
和狀態碼 200 OK
。
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
替代方法是使用 Microsoft.AspNetCore.Mvc.ProducesAttribute 來描述產生的回應。 下列程式碼會將 PopulateMetadata
方法變更為使用 ProducesAttribute
。
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
設定 JSON 序列化選項
根據預設,基本 API 應用程式會在 JSON 序列化和還原序列化期間使用 Web defaults
選項。
全域設定 JSON 還原序列化選項
您可以叫用 ConfigureHttpJsonOptions 來全域設定應用程式的選項。 下列範例包含公用欄位和格式 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
// }
由於有包含欄位,上述程式碼會讀取 NameField
並將其包含在輸出 JSON 中。
為端點設定 JSON 序列化選項
若要設定端點的序列化選項,請叫用 Results.Json 並將其傳遞至 JsonSerializerOptions 物件,如下範例所示:
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
// }
或者,請使用接受 JsonSerializerOptions 物件的 WriteAsJsonAsync 多載。 下列範例會使用此多載來格式化輸出 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
// }
其他資源
基本端點支援下列型別的傳回值:
string
- 這包括Task<string>
和ValueTask<string>
。T
(任何其他型別) - 這包括Task<T>
和ValueTask<T>
。- 以
IResult
為基礎 - 這包括Task<IResult>
和ValueTask<IResult>
。
string
傳回值
行為 | 內容-類型 |
---|---|
架構會將字串直接寫入回應。 | text/plain |
請考慮下列會傳回 Hello world
文字的路由處理常式。
app.MapGet("/hello", () => "Hello World");
200
狀態碼會以 text/plain
Content-Type 標頭和下列內容傳回。
Hello World
T
(任何其他型別) 傳回值
行為 | 內容-類型 |
---|---|
架構 JSON 序列化回應。 | application/json |
請考慮下列會傳回包含 Message
字串屬性之匿名型別的路由處理常式。
app.MapGet("/hello", () => new { Message = "Hello World" });
200
狀態碼會以 application/json
Content-Type 標頭和下列內容傳回。
{"message":"Hello World"}
IResult
傳回值
行為 | 內容-類型 |
---|---|
架構會呼叫 IResult.ExecuteAsync。 | 由 IResult 實作決定。 |
IResult
介面會定義代表 HTTP 端點結果的合約。 靜態 Results 類別和靜態 TypedResults 可用來建立代表不同回應型別的各種 IResult
物件。
TypedResults 與 Results 的比較
Results 和 TypedResults 靜態類別提供類似的結果協助程式集合。 TypedResults
類別是 Results
類別的同等型別。 不過,Results
協助程式的傳回型別是 IResult,而每個 TypedResults
協助程式的傳回型別則是其中一種 IResult
實作型別。 差異意味著對於 Results
協助程式而言,當需要實體型別時則需要轉換,例如適用於單元測試。 實作型別會在 Microsoft.AspNetCore.Http.HttpResults 命名空間中定義。
傳回 TypedResults
而非 Results
具有下列優勢:
TypedResults
協助程式會傳回強式型別物件,這可改善程式碼可讀性、單元測試,並降低執行階段錯誤的機會。- 實作型別會自動為 OpenAPI 提供回應型別中繼資料,以描述端點。
請考慮下列端點,其中會產生具有預期 JSON 回應的 200 OK
狀態程式碼。
app.MapGet("/hello", () => Results.Ok(new Message() { Text = "Hello World!" }))
.Produces<Message>();
為了正確記錄此端點,會呼叫擴充方法 Produces
。 不過,如果是使用 TypedResults
而非 Results
,則不一定需要呼叫 Produces
(如下程式碼所示)。 TypedResults
會自動提供此端點的中繼資料。
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
如需描述回應型別的詳細資訊,請參閱基本 API 中的 OpenAPI 支援。
如先前所述,使用 TypedResults
時則不需要轉換。 請考慮下列會傳回 TypedResults
類別的基本 API
public static async Task<Ok<Todo[]>> GetAllTodos(TodoGroupDbContext database)
{
var todos = await database.Todos.ToArrayAsync();
return TypedResults.Ok(todos);
}
下列測試會檢查完整的實體型別:
[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);
});
}
由於所有 Results
上的方法在其簽章中都傳回 IResult
,編譯器會在從單一端點傳回不同的結果時,自動將其推斷為要求委派傳回型別。 TypedResults
需要從這類委派使用 Results<T1, TN>
。
下列方法會進行編譯,因為 Results.Ok
和 Results.NotFound
都宣告為傳回 IResult
,即使傳回之物件的實際實體型別並不相同:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
下列方法不會進行編譯,因為 TypedResults.Ok
和 TypedResults.NotFound
宣告為傳回不同的型別,而且編譯器不會嘗試推斷最佳的比對型別:
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound());
若要使用 TypedResults
,傳回型別必須是完整宣告 (此為當非同步需要 Task<>
包裝函式時)。 使用 TypedResults
會更為詳細,但這是讓型別資訊靜態可用,且因此可對 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());
Results<TResult1, TResultN>
下列情況時,請使用 Results<TResult1, TResultN>
作為端點處理常式傳回型別,而非 IResult
:
- 從端點處理常式傳回多個
IResult
實作型別。 - 使用靜態
TypedResult
類別來建立IResult
物件。
這個替代方法比傳回 IResult
還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN>
等位型別會實作隱含轉換運算子,編譯器可以自動將泛型引數中指定的型別轉換成等位型別的執行個體。
這會有的附加優點為提供編譯時間檢查,確定路由處理常式實際上只會傳回其宣告會執行的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<>
會產生編譯錯誤。
請考慮下列當 orderId
大於 999
時會傳回 400 BadRequest
狀態碼的端點。 否則,它會產生具有預期內容的 200 OK
。
app.MapGet("/orders/{orderId}", IResult (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)))
.Produces(400)
.Produces<Order>();
為了正確記錄此端點,會呼叫擴充方法 Produces
。 不過,由於 TypedResults
協助程式會自動包含端點的中繼資料,您可以改為傳回 Results<T1, Tn>
等位型別,如下程式碼所示。
app.MapGet("/orders/{orderId}", Results<BadRequest, Ok<Order>> (int orderId)
=> orderId > 999 ? TypedResults.BadRequest() : TypedResults.Ok(new Order(orderId)));
內建結果
常見的結果協助程式存在於 Results 和 TypedResults 靜態類別中。 相較於傳回 Results
,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
下列各節會說明常見結果協助程式的使用方式。
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
WriteAsJsonAsync 是傳回 JSON 的替代方式:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsJsonAsync
(new { Message = "Hello World" }));
自訂狀態碼
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
資料流
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
多載允許存取基礎 HTTP 回應串流,不需緩衝處理。 下列範例會使用 ImageSharp 以傳回指定映像的縮減大小:
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);
}
下列範例會從 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");
});
下列範例會從 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);
});
重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
檔案
app.MapGet("/download", () => Results.File("myfile.text"));
HttpResult 介面
下列 Microsoft.AspNetCore.Http 命名空間中的介面提供在執行階段偵測 IResult
型別的方法,這是篩選實作中的常見模式:
- IContentTypeHttpResult
- IFileHttpResult
- INestedHttpResult
- IStatusCodeHttpResult
- IValueHttpResult
- IValueHttpResult<TValue>
以下是使用其中這些介面之一的篩選範例:
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
};
});
如需詳細資訊,請參閱最小 API 應用程式中的篩選,以及 IResult 實作型別。
自訂回應
應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 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);
}
}
建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。
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();
此外,自訂 IResult
型別可以藉由實作 IEndpointMetadataProvider 介面來提供自己的註釋。 例如,下列程式碼會將註釋新增至上述 HtmlResult
型別,可描述端點產生的回應。
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
是 IProducesResponseTypeMetadata 的實作,會定義產生的回應內容型別 text/html
和狀態碼 200 OK
。
internal sealed class ProducesHtmlMetadata : IProducesResponseTypeMetadata
{
public Type? Type => null;
public int StatusCode => 200;
public IEnumerable<string> ContentTypes { get; } = new[] { MediaTypeNames.Text.Html };
}
替代方法是使用 Microsoft.AspNetCore.Mvc.ProducesAttribute 來描述產生的回應。 下列程式碼會將 PopulateMetadata
方法變更為使用 ProducesAttribute
。
public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesAttribute(MediaTypeNames.Text.Html));
}
設定 JSON 序列化選項
根據預設,基本 API 應用程式會在 JSON 序列化和還原序列化期間使用 Web defaults
選項。
全域設定 JSON 還原序列化選項
您可以叫用 ConfigureHttpJsonOptions 來全域設定應用程式的選項。 下列範例包含公用欄位和格式 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
// }
由於有包含欄位,上述程式碼會讀取 NameField
並將其包含在輸出 JSON 中。
為端點設定 JSON 序列化選項
若要設定端點的序列化選項,請叫用 Results.Json 並將其傳遞至 JsonSerializerOptions 物件,如下範例所示:
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
// }
或者,請使用接受 JsonSerializerOptions 物件的 WriteAsJsonAsync 多載。 下列範例會使用此多載來格式化輸出 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
// }