注意
這不是這篇文章的最新版本。 關於目前版本,請參閱 本文的 .NET 10 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
本文說明如何在 ASP.NET Core 中為最小 API 端點建立回應。 最小 API 提供數種方式來傳回資料和 HTTP 狀態碼。
基本端點支援下列型別的傳回值:
-
string- 這包括Task<string>和ValueTask<string>。 -
T(任何其他型別) - 這包括Task<T>和ValueTask<T>。 - 以
IResult為基礎 - 這包括Task<IResult>和ValueTask<IResult>。
重要
從 ASP.NET Core 10 開始,使用驗證時 cookie ,已知 API 端點不再重新導向至登入頁面。 相反,它們會傳回 401/403 狀態碼。 如需詳細資訊,請參閱 ASP.NET Core 中的 API 端點驗證行為。
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。 不過,如果是使用 Produces 而非 TypedResults,則不一定需要呼叫 Results (如下程式碼所示)。
TypedResults 會自動提供此端點的中繼資料。
app.MapGet("/hello2", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
如需描述回應型別的詳細資訊,請參閱基本 API 中的 OpenAPI 支援。
如需測試結果類型的範例,請參閱 測試文件。
由於所有 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());
結果<TResult1, TResultN>
下列情況時,請使用 Results<TResult1, TResultN> 作為端點處理常式傳回型別,而非 IResult:
- 從端點處理程序傳回多個
IResult實作型別。 - 使用靜態
TypedResult類別來建立IResult物件。
這個替代方法比傳回 IResult 還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN> 聯合型別會實作隱式轉換運算子,編譯器會自動將泛型引數中指定的型別轉換成聯合型別的實例。
這額外的好處在於提供編譯期檢查功能,以確保路由處理常式實際上只傳回其所宣告的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<> 會產生編譯錯誤。
請考慮下列當 400 BadRequest 大於 orderId 時會傳回 999 狀態碼的端點。 否則,它會產生具有預期內容的 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 靜態類別中。 偏好傳回 TypedResults,而不是傳回 Results。 如需詳細資訊,請參閱 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 狀態碼。
問題與驗證問題
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
使用 IProblemDetailsService 自訂驗證錯誤回應
使用實作從 IProblemDetailsService 最小的 API 驗證邏輯自訂錯誤回應。 在應用程式的服務集合中註冊此服務,以啟用更一致且使用者特定的錯誤回應。 .NET 10 的 ASP.NET Core 中引進了對最小 API 驗證的支援。
若要實作自訂驗證錯誤回應:
- 實作 IProblemDetailsService 或使用預設實作
- 在 DI 容器中註冊服務
- 驗證系統會自動使用已登錄的服務來格式化驗證錯誤回應
下列範例顯示如何註冊和設定 以 IProblemDetailsService 自訂驗證錯誤回應:
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();
}
};
});
當發生驗證錯誤時,將 IProblemDetailsService 用於產生錯誤回應,包括回呼中 CustomizeProblemDetails 新增的任何自訂。
如需完整的應用程式範例,請參閱 Minimal API 範例應用程式 ,示範如何使用 ASP.NET Core Minimal API 中的 自訂 IProblemDetailsService 驗證錯誤回應。
文字
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 多載允許存取基礎 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);
});
Server-Sent 事件(SSE)
TypedResults.ServerSentEvents API 支援傳回 ServerSentEvents 結果。
Server-Sent 事件 是一種伺服器推送技術,可讓伺服器透過單一 HTTP 連線將事件訊息串流傳送至用戶端。 在 .NET 中,事件訊息會表示為 SseItem<T> 物件,其中可能包含事件類型、標識碼,以及類型的 T數據承載。
TypedResults 類別具有稱為 ServerSentEvents 的靜態方法,可用來傳回ServerSentEvents結果。 這個方法的第一個 IAsyncEnumerable<SseItem<T>> 參數是 ,表示要傳送至用戶端的事件訊息數據流。
下列範例說明如何使用 TypedResults.ServerSentEvents API,將心率事件的數據流當做 JSON 物件傳回給用戶端:
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));
});
如需詳細資訊,請參閱使用 API 將心率事件的數據流當做字串、TypedResults.ServerSentEvents和 JSON 物件傳回用戶端的ServerSentEvents。
重新導向
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 實作型別。
修改標頭
使用 HttpResponse 物件來修改回應標頭:
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";
});
自訂回應
應用程式可以藉由實作自訂 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
// }
或者,請使用接受 WriteAsJsonAsync 物件的 JsonSerializerOptions 多載。 下列範例會使用此多載來格式化輸出 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。 不過,如果是使用 Produces 而非 TypedResults,則不一定需要呼叫 Results (如下程式碼所示)。
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());
結果<TResult1, TResultN>
下列情況時,請使用 Results<TResult1, TResultN> 作為端點處理常式傳回型別,而非 IResult:
- 從端點處理程序傳回多個
IResult實作型別。 - 使用靜態
TypedResult類別來建立IResult物件。
這個替代方法比傳回 IResult 還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN> 聯合型別會實作隱式轉換運算子,編譯器會自動將泛型引數中指定的型別轉換成聯合型別的實例。
這額外的好處在於提供編譯期檢查功能,以確保路由處理常式實際上只傳回其所宣告的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<> 會產生編譯錯誤。
請考慮下列當 400 BadRequest 大於 orderId 時會傳回 999 狀態碼的端點。 否則,它會產生具有預期內容的 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 靜態類別中。 偏好傳回 TypedResults,而不是傳回 Results。 如需詳細資訊,請參閱 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 狀態碼。
問題與驗證問題
app.MapGet("/problem", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions",
extensions: extensions);
});
文字
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 多載允許存取基礎 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 實作型別。
修改標頭
使用 HttpResponse 物件來修改回應標頭:
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";
});
自訂回應
應用程式可以藉由實作自訂 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
// }
或者,請使用接受 WriteAsJsonAsync 物件的 JsonSerializerOptions 多載。 下列範例會使用此多載來格式化輸出 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。 不過,如果是使用 Produces 而非 TypedResults,則不一定需要呼叫 Results (如下程式碼所示)。
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());
結果<TResult1, TResultN>
下列情況時,請使用 Results<TResult1, TResultN> 作為端點處理常式傳回型別,而非 IResult:
- 從端點處理程序傳回多個
IResult實作型別。 - 使用靜態
TypedResult類別來建立IResult物件。
這個替代方法比傳回 IResult 還好,因為泛型等位型別會自動保留端點中繼資料。 而且由於 Results<TResult1, TResultN> 聯合型別會實作隱式轉換運算子,編譯器會自動將泛型引數中指定的型別轉換成聯合型別的實例。
這額外的好處在於提供編譯期檢查功能,以確保路由處理常式實際上只傳回其所宣告的結果。 嘗試傳回未宣告為其中一個泛型引數的型別至 Results<> 會產生編譯錯誤。
請考慮下列當 400 BadRequest 大於 orderId 時會傳回 999 狀態碼的端點。 否則,它會產生具有預期內容的 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 靜態類別中。 偏好傳回 TypedResults,而不是傳回 Results。 如需詳細資訊,請參閱 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("/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 多載允許存取基礎 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
// }
或者,請使用接受 WriteAsJsonAsync 物件的 JsonSerializerOptions 多載。 下列範例會使用此多載來格式化輸出 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
// }