在 ASP.NET Core 中使用 HttpContext
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本。
HttpContext 會封裝關於個別 HTTP 要求和回應的所有資訊。 收到 HTTP 要求時,會初始化 HttpContext
執行個體。 該 HttpContext
執行個體可透過中介軟體和應用程式架構來存取,例如 Web API 控制器、Razor 頁面、SignalR、gRPC 等等。
如需關於存取 HttpContext
的詳細資訊,請參閱存取 ASP.NET Core 中的 HttpContext。
HttpRequest
HttpContext.Request 提供對 HttpRequest 的存取權。 HttpRequest
包含傳入 HTTP 要求的相關資訊,並且會在伺服器收到 HTTP 要求時初始化。 HttpRequest
並非唯讀項目,中介軟體可以變更中介軟體管線中的要求值。
HttpRequest
的常用成員,包括:
屬性 | 說明 | 範例 |
---|---|---|
HttpRequest.Path | 要求路徑。 | /en/article/getstarted |
HttpRequest.Method | 要求方法。 | GET |
HttpRequest.Headers | 要求標頭集合。 | user-agent=Edge x-custom-header=MyValue |
HttpRequest.RouteValues | 路由值的集合。 要求符合路由時,就會設定集合。 | language=en article=getstarted |
HttpRequest.Query | 從 QueryString 剖析的查詢值集合。 | filter=hello page=1 |
HttpRequest.ReadFormAsync() | 將要求本文作為表單讀取,並傳回表單值集合的方法。 如需為何應該使用 ReadFormAsync 來存取表單資料的相關資訊,請參閱偏好 ReadFormAsync 而不是 Request.Form。 |
email=user@contoso.com |
HttpRequest.Body | Stream 用於讀取要求本文。 | UTF-8 JSON 承載 |
取得要求標頭
HttpRequest.Headers 提供對與 HTTP 要求一起傳送之要求標頭的存取權。 您可以採用以下兩種方式,使用這個集合來存取標頭:
- 向標頭集合上的索引子提供標頭名稱。 標頭名稱不區分大小寫。 索引子可以存取任何標頭值。
- 標頭集合還具有用來取得和設定常用 HTTP 標頭的屬性。 這些屬性提供一種快速、IntelliSense 驅動的方式來存取標頭。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpRequest request) =>
{
var userAgent = request.Headers.UserAgent;
var customHeader = request.Headers["x-custom-header"];
return Results.Ok(new { userAgent = userAgent, customHeader = customHeader });
});
app.Run();
若要了解如何有效率地處理出現多次的標頭,請參閱 StringValues 簡介。
讀取要求本文
HTTP 要求可以包含要求本文。 要求本文泛指與要求相關聯的資料,例如 HTML 表單的內容、UTF-8 JSON 承載,或是檔案。
HttpRequest.Body 允許使用 Stream 來讀取要求本文:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await context.Request.Body.CopyToAsync(writeStream);
});
app.Run();
HttpRequest.Body
可以直接讀取,也可以與其他接受資料流的 API 搭配使用。
注意
基本 API 支援將 HttpRequest.Body 直接繫結至 Stream 參數。
啟用要求本文緩衝處理
要求本文從頭到尾只能讀取一次。 順向讀取要求本文可避免緩衝處理整個要求本文的額外負荷,並減少記憶體使用量。 不過,在某些情況下,需要多次讀取要求本文。 例如,中介軟體可能需要讀取要求本文,然後倒轉,以供端點使用。
EnableBuffering 擴充方法可以緩衝處理 HTTP 要求本文,是啟用多次讀取的建議方式。 由於要求可以是任何大小,因此 EnableBuffering
支援您選擇將大型要求本文緩衝處理至磁碟或完全拒絕。
下列範例中的中介軟體,將:
- 使用
EnableBuffering
啟用多次讀取。 讀取要求本文之前,必須先呼叫該擴充方法。 - 讀取要求本文。
- 將要求本文倒轉至開始,以便其他中介軟體或端點可以進行讀取。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
await ReadRequestBody(context.Request.Body);
context.Request.Body.Position = 0;
await next.Invoke();
});
app.Run();
BodyReader
讀取要求本文的另一種方式是使用 HttpRequest.BodyReader 屬性。 屬性 BodyReader
會將要求本文公開為 PipeReader。 此 API 來自 I/O 管線,是一種讀取要求本文的進階、高效能方式。
讀取器會直接存取要求本文,並代表呼叫端管理記憶體。 與 HttpRequest.Body
不同,讀取器不會將要求資料複製到緩衝區中。 然而,讀取器使用起來比資料流更加複雜,請務必謹慎使用。
如需關於如何讀取 BodyReader
內容的相關資訊,請參閱 I/O 管線 PipeReader。
HttpResponse
HttpContext.Response 提供對 HttpResponse 的存取權。 HttpResponse
用來設定傳回給用戶端之 HTTP 回應的相關資訊。
HttpResponse
的常用成員,包括:
屬性 | 說明 | 範例 |
---|---|---|
HttpResponse.StatusCode | 回應碼。 必須在寫入回應本文之前進行設定。 | 200 |
HttpResponse.ContentType | 回應 content-type 標頭。 必須在寫入回應本文之前進行設定。 |
application/json |
HttpResponse.Headers | 回應標頭的集合。 必須在寫入回應本文之前進行設定。 | server=Kestrel x-custom-header=MyValue |
HttpResponse.Body | Stream 用於寫入回應本文。 | 產生的網頁 |
設定回應標頭
HttpResponse.Headers 提供對與 HTTP 回應一起傳送之回應標頭的存取權。 您可以採用以下兩種方式,以使用這個集合來存取標頭:
- 向標頭集合上的索引子提供標頭名稱。 標頭名稱不區分大小寫。 索引子可以存取任何標頭值。
- 標頭集合還具有用來取得和設定常用 HTTP 標頭的屬性。 這些屬性提供一種快速、IntelliSense 驅動的方式來存取標頭。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
response.Headers.CacheControl = "no-cache";
response.Headers["x-custom-header"] = "Custom value";
return Results.File(File.OpenRead("helloworld.txt"));
});
app.Run();
應用程式無法在回應啟動後修改標頭。 一旦回應啟動,標頭就會傳送至用戶端。 回應是透過排清回應本文或呼叫 HttpResponse.StartAsync(CancellationToken) 來啟動。 屬性 HttpResponse.HasStarted 會指出回應是否已啟動。 若在回應啟動後嘗試修改標頭,會擲回錯誤:
System.InvalidOperationException:標頭是唯讀項目,回應已啟動。
注意
除非啟用回應緩衝處理,否則所有寫入作業 (例如 WriteAsync) 都會在內部排清回應主體,並將回應標示為已啟動。 預設會停用回應緩衝處理。
寫入回應本文
HTTP 回應可以包含回應本文。 回應本文泛指與回應相關聯的資料,例如:產生的網頁內容、UTF-8 JSON 承載,或是檔案。
HttpResponse.Body 允許使用 Stream 來寫入回應本文:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/downloadfile", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], "helloworld.txt");
await using var fileStream = File.OpenRead(filePath);
await fileStream.CopyToAsync(context.Response.Body);
});
app.Run();
HttpResponse.Body
可以直接寫入,或與其他寫入資料流的 API 搭配使用。
BodyWriter
寫入回應本文的另一種方式是使用 HttpResponse.BodyWriter 屬性。 屬性 BodyWriter
會將回應本文公開為 PipeWriter。 此 API 來自 I/O 管線,是一種寫入回應的高效進階方式。
寫入器提供對回應本文的直接存取權,並代表呼叫端管理記憶體。 與 HttpResponse.Body
不同,寫入不會將要求資料複製到緩衝區中。 然而,寫入器使用起來比資料流更加複雜,請務必充分測試寫入器程式碼。
如需關於如何將內容寫入 BodyWriter
的相關資訊,請參閱 I/O 管線 PipeWriter。
設定回應結尾
HTTP/2 和 HTTP/3 都支援回應結尾。 結尾是在回應本文完成之後與回應一起傳送的標頭。 由於結尾是在回應本文之後傳送的,因此隨時都可以將結尾新增至回應中。
下列程式碼使用 AppendTrailer 來設定結尾:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
// Write body
response.WriteAsync("Hello world");
if (response.SupportsTrailers())
{
response.AppendTrailer("trailername", "TrailerValue");
}
});
app.Run();
RequestAborted
HttpContext.RequestAborted 取消權杖可用來通知 HTTP 要求已被用戶端或伺服器中止。 務必將取消權杖傳遞給長時間執行的工作,以便在要求中止時取消。 例如,中止資料庫查詢或 HTTP 要求取得要在回應中傳回的資料。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var httpClient = new HttpClient();
app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
var stream = await httpClient.GetStreamAsync(
$"http://contoso/books/{bookId}.json", context.RequestAborted);
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
不需要將 RequestAborted
取消權杖用於要求本文讀取作業,因為在要求中止時一律會立即執行讀取。 寫入回應本文時,RequestAborted
權杖通常也是不必要的,因為在要求中止時會立即不執行任何寫入。
在某些情況下,將 RequestAborted
權杖傳遞給寫入作業是強制寫入迴圈使用 OperationCanceledException 提前退出的便捷方式。 不過,通常最好是將 RequestAborted
權杖傳遞給負責擷取回應本文內容的任何非同步作業。
注意
基本 API 支援將 HttpContext.RequestAborted 直接繫結至 CancellationToken 參數。
Abort()
HttpContext.Abort() 方法可用來中止來自伺服器的 HTTP 要求。 中止 HTTP 要求會立即觸發 HttpContext.RequestAborted 取消權杖,並向用戶端傳送通知,指出伺服器已中止要求。
下列範例中的中介軟體,將:
- 新增惡意要求的自訂檢查。
- 如果是惡意要求,則中止 HTTP 要求。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
if (RequestAppearsMalicious(context.Request))
{
// Malicious requests don't even deserve an error response (e.g. 400).
context.Abort();
return;
}
await next.Invoke();
});
app.Run();
User
屬性 HttpContext.User 可用來取得或設定要求的使用者 (由 ClaimsPrincipal 表示)。 ClaimsPrincipal 通常由 ASP.NET Core 驗證設定。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/user/current", [Authorize] async (HttpContext context) =>
{
var user = await GetUserAsync(context.User.Identity.Name);
return Results.Ok(user);
});
app.Run();
注意
基本 API 支援將 HttpContext.User 直接繫結至 ClaimsPrincipal 參數。
Features
屬性 HttpContext.Features 提供對目前要求之功能介面集合的存取權。 由於功能集合即使在要求內容中都是可變動的,因此可以使用中介軟體來修改該集合,並新增其他功能的支援。 某些進階功能只有透過功能集合存取關聯的介面才能使用。
下列範例將:
- 從功能集合中取得 IHttpMinRequestBodyDataRateFeature。
- 將 MinDataRate 設定為 Null。 這會移除用戶端必須為此 HTTP 要求傳送要求本文的最低資料速率。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/long-running-stream", async (HttpContext context) =>
{
var feature = context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
if (feature != null)
{
feature.MinDataRate = null;
}
// await and read long-running stream from request body.
await Task.Yield();
});
app.Run();
如需關於使用要求功能和 HttpContext
的詳細資訊,請參閱 ASP.NET Core 中的要求功能。
HttpContext 不是安全執行緒
本文主要討論在來自 Razor Pages、控制器、中介軟體等的要求和回應流程中使用 HttpContext
。在要求和回應流程之外使用 HttpContext
時,請考慮下列事項:
HttpContext
不是安全執行緒,從多個執行緒進行存取,可能會導致例外狀況、資料損毀和通常無法預測的結果。- 請務必謹慎使用 IHttpContextAccessor 介面。 一如既往,不得在要求流程之外擷取
HttpContext
。IHttpContextAccessor
:- 依賴 AsyncLocal<T>,可能會對非同步呼叫造成負面影響。
- 建立對「環境狀態」的相依性,可能會使測試變得更加困難。
- 如果在要求流程之外進行存取,IHttpContextAccessor.HttpContext 可能會是
null
。 - 若要從要求流程之外的
HttpContext
存取資訊,請複製要求流程內的資訊。 請小心複製實際資料,而不只是參考資料。 例如,在離開要求流程之前,不是複製IHeaderDictionary
的參考資料,而是複製相關標頭值,或逐一複製整個字典。 - 不要在建構函式中擷取
IHttpContextAccessor.HttpContext
。
下列範例會在從 /branch
端點要求時記錄 GitHub 分支:
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();
builder.Services.AddTransient<UserAgentHeaderHandler>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
var response = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
app.Logger.LogInformation($"/branches request: " +
$"{JsonSerializer.Serialize(response)}");
return Results.Ok(response);
});
app.Run();
GitHub API 需要兩個標頭。 標頭 User-Agent
由 UserAgentHeaderHandler
動態新增:
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// The GitHub API requires two headers. The Use-Agent header is added
// dynamically through UserAgentHeaderHandler
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();
builder.Services.AddTransient<UserAgentHeaderHandler>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
HttpContext context, Logger<Program> logger) =>
{
var httpClient = httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (!httpResponseMessage.IsSuccessStatusCode)
return Results.BadRequest();
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
var response = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
app.Logger.LogInformation($"/branches request: " +
$"{JsonSerializer.Serialize(response)}");
return Results.Ok(response);
});
app.Run();
UserAgentHeaderHandler
:
using Microsoft.Net.Http.Headers;
namespace HttpContextInBackgroundThread;
public class UserAgentHeaderHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
ILogger<UserAgentHeaderHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
protected override async Task<HttpResponseMessage>
SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var contextRequest = _httpContextAccessor.HttpContext?.Request;
string? userAgentString = contextRequest?.Headers["user-agent"].ToString();
if (string.IsNullOrEmpty(userAgentString))
{
userAgentString = "Unknown";
}
request.Headers.Add(HeaderNames.UserAgent, userAgentString);
_logger.LogInformation($"User-Agent: {userAgentString}");
return await base.SendAsync(request, cancellationToken);
}
}
在上述程式碼中,若 HttpContext
為 null
,userAgent
字串就會設定為 "Unknown"
。 可能的話,務必將 HttpContext
明確傳遞給服務。 明確傳入 HttpContext
資料,可以:
- 使服務 API 在要求流程之外更有用處。
- 效能更好。
- 使程式碼比依賴環境狀態更加容易理解和推理。
在服務必須存取 HttpContext
時,應該考慮到在未從要求執行緒呼叫時 HttpContext
為 null
的可能性。
應用程式還包括 PeriodicBranchesLoggerService
,每 30 秒記錄一次指定存放庫的開啟的 GitHub 分支:
using System.Text.Json;
namespace HttpContextInBackgroundThread;
public class PeriodicBranchesLoggerService : BackgroundService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
private readonly PeriodicTimer _timer;
public PeriodicBranchesLoggerService(IHttpClientFactory httpClientFactory,
ILogger<PeriodicBranchesLoggerService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
// Cancel sending the request to sync branches if it takes too long
// rather than miss sending the next request scheduled 30 seconds from now.
// Having a single loop prevents this service from sending an unbounded
// number of requests simultaneously.
using var syncTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",
stoppingToken);
if (httpResponseMessage.IsSuccessStatusCode)
{
await using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);
// Sync the response with preferred datastore.
var response = await JsonSerializer.DeserializeAsync<
IEnumerable<GitHubBranch>>(contentStream, cancellationToken: stoppingToken);
_logger.LogInformation(
$"Branch sync successful! Response: {JsonSerializer.Serialize(response)}");
}
else
{
_logger.LogError(1, $"Branch sync failed! HTTP status code: {httpResponseMessage.StatusCode}");
}
}
catch (Exception ex)
{
_logger.LogError(1, ex, "Branch sync failed!");
}
}
}
public override Task StopAsync(CancellationToken stoppingToken)
{
// This will cause any active call to WaitForNextTickAsync() to return false immediately.
_timer.Dispose();
// This will cancel the stoppingToken and await ExecuteAsync(stoppingToken).
return base.StopAsync(stoppingToken);
}
}
PeriodicBranchesLoggerService
是一項託管服務,在要求和回應流程之外執行。 從 PeriodicBranchesLoggerService
記錄的 HttpContext
為 null。 PeriodicBranchesLoggerService
已寫為不依賴 HttpContext
。
using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();
builder.Services.AddHttpClient("GitHub", httpClient =>
{