在 ASP.NET Core 中使用 HttpContext
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
HttpContext 封装了有关个别 HTTP 请求和响应的所有信息。 收到 HTTP 请求时,HttpContext
实例会进行初始化。 HttpContext
实例可通过中间件和应用框架(如 Web API 控制器、Razor Pages、SignalR、gRPC 等)访问。
有关访问 HttpContext
的详细信息,请参阅在 ASP.NET Core 中访问 HttpContext。
HttpRequest
HttpContext.Request 提供对 HttpRequest 的访问。 HttpRequest
包含有关传入 HTTP 请求的信息,并在服务器收到 HTTP 请求时被初始化。 HttpRequest
不是只读的,中间件可以在中间件管道中更改请求值。
HttpRequest
上常用的成员包括:
properties | 说明 | 示例 |
---|---|---|
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
上常用的成员包括:
properties | 说明 | 示例 |
---|---|---|
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 =>
{