在 ASP.NET Core 中使用 HttpContext

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
password=TNkt4taM
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 提供對目前要求之功能介面集合的存取權。 由於功能集合即使在要求內容中都是可變動的,因此可以使用中介軟體來修改該集合,並新增其他功能的支援。 某些進階功能只有透過功能集合存取關聯的介面才能使用。

下列範例將:

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 介面。 一如既往,不得在要求流程之外擷取 HttpContextIHttpContextAccessor:
    • 依賴 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-AgentUserAgentHeaderHandler 動態新增:

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);
    }
}

在上述程式碼中,若 HttpContextnulluserAgent 字串就會設定為 "Unknown"。 可能的話,務必將 HttpContext 明確傳遞給服務。 明確傳入 HttpContext 資料,可以:

  • 使服務 API 在要求流程之外更有用處。
  • 效能更好。
  • 使程式碼比依賴環境狀態更加容易理解和推理。

在服務必須存取 HttpContext 時,應該考慮到在未從要求執行緒呼叫時 HttpContextnull 的可能性。

應用程式還包括 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 =>
{