Sử dụng HttpContext trong ASP.NET Core
HttpContext đóng gói tất cả thông tin về một HTTP request và response riêng lẻ. Một instance HttpContext được khởi tạo khi nhận được HTTP request. Instance HttpContext có thể truy cập bởi middleware và các app framework như Blazor Web Apps, Web API controllers, Razor Pages, SignalR, gRPC, và nhiều hơn nữa.
HttpRequest
HttpContext.Request cung cấp quyền truy cập vào HttpRequest. HttpRequest có thông tin về incoming HTTP request, và nó được khởi tạo khi server nhận được HTTP request. HttpRequest không chỉ đọc, và middleware có thể thay đổi các giá trị request trong middleware pipeline.
Các thành viên thường được sử dụng trên HttpRequest bao gồm:
| Thuộc tính | Mô tả | Ví dụ |
|---|---|---|
HttpRequest.Path | Đường dẫn request. | /en/article/getstarted |
HttpRequest.Method | Phương thức request. | GET |
HttpRequest.Headers | Tập hợp các request header. | user-agent=Edge, x-custom-header=MyValue |
HttpRequest.RouteValues | Tập hợp các route value. Tập hợp được đặt khi request được khớp với một route. | language=en, article=getstarted |
HttpRequest.Query | Tập hợp các query value được phân tích từ QueryString. | filter=hello, page=1 |
HttpRequest.ReadFormAsync() | Phương thức đọc request body dưới dạng form và trả về tập hợp giá trị form. | email=user@contoso.com |
HttpRequest.Body | Stream để đọc request body. | UTF-8 JSON payload |
Lấy request headers
HttpRequest.Headers cung cấp quyền truy cập vào request headers được gửi kèm với HTTP request. Có hai cách truy cập headers bằng collection này:
- Cung cấp tên header cho indexer trên header collection. Tên header không phân biệt chữ hoa chữ thường. Indexer có thể truy cập bất kỳ giá trị header nào.
- Header collection cũng có các thuộc tính để lấy và đặt các HTTP header thường dùng. Các thuộc tính cung cấp cách truy cập headers nhanh, được hỗ trợ bởi 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();Đọc request body
Một HTTP request có thể bao gồm request body. Request body là dữ liệu liên quan đến request, chẳng hạn như nội dung của HTML form, UTF-8 JSON payload, hoặc file.
HttpRequest.Body cho phép đọc request body với 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();Kích hoạt request body buffering (đệm)
Request body chỉ có thể được đọc một lần, từ đầu đến cuối. Đọc chỉ tiến của request body tránh overhead của việc đệm toàn bộ request body và giảm sử dụng bộ nhớ. Tuy nhiên, trong một số trường hợp, cần phải đọc request body nhiều lần. Ví dụ, middleware có thể cần đọc request body và sau đó tua lại để endpoint có thể đọc.
Extension method EnableBuffering cho phép đệm HTTP request body và là cách được khuyến nghị để kích hoạt nhiều lần đọc. Vì request có thể có bất kỳ kích thước nào, EnableBuffering hỗ trợ các tùy chọn để đệm các request body lớn vào đĩa, hoặc từ chối hoàn toàn.
Middleware trong ví dụ sau:
- Kích hoạt nhiều lần đọc với
EnableBuffering. Nó phải được gọi trước khi đọc request body. - Đọc request body.
- Tua lại request body về đầu để middleware khác hoặc endpoint có thể đọc.
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
Một cách thay thế để đọc request body là sử dụng thuộc tính HttpRequest.BodyReader. Thuộc tính BodyReader expose request body như một PipeReader. API này từ I/O pipelines, là cách tiên tiến, hiệu suất cao để đọc request body.
Reader truy cập trực tiếp vào request body và quản lý bộ nhớ thay cho người gọi. Không giống HttpRequest.Body, reader không sao chép dữ liệu request vào buffer. Tuy nhiên, reader phức tạp hơn để sử dụng so với stream và nên được sử dụng cẩn thận.
HttpResponse
HttpContext.Response cung cấp quyền truy cập vào HttpResponse. HttpResponse được sử dụng để đặt thông tin trên HTTP response được gửi lại cho client.
Các thành viên thường được sử dụng trên HttpResponse bao gồm:
| Thuộc tính | Mô tả | Ví dụ |
|---|---|---|
HttpResponse.StatusCode | Mã response. Phải được đặt trước khi ghi vào response body. | 200 |
HttpResponse.ContentType | Header content-type của response. Phải được đặt trước khi ghi vào response body. | application/json |
HttpResponse.Headers | Tập hợp các response header. Phải được đặt trước khi ghi vào response body. | server=Kestrel, x-custom-header=MyValue |
HttpResponse.Body | Stream để ghi response body. | Trang web được tạo |
Đặt response headers
HttpResponse.Headers cung cấp quyền truy cập vào response headers được gửi kèm với HTTP response. Có hai cách truy cập headers bằng collection này:
- Cung cấp tên header cho indexer trên header collection. Tên header không phân biệt chữ hoa chữ thường.
- Sử dụng các thuộc tính của header collection để lấy và đặt các HTTP header thường dùng.
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();Ứng dụng không thể sửa đổi headers sau khi response đã bắt đầu. Sau khi response bắt đầu, headers được gửi cho client. Response được bắt đầu bằng cách flush response body hoặc gọi HttpResponse.StartAsync(CancellationToken). Thuộc tính HttpResponse.HasStarted cho biết response đã bắt đầu hay chưa. Lỗi được ném khi cố gắng sửa đổi headers sau khi response đã bắt đầu:
System.InvalidOperationException: Headers are read-only, response has already started.
Ghi response body
Một HTTP response có thể bao gồm response body. Response body là dữ liệu liên quan đến response, chẳng hạn như nội dung trang web được tạo, UTF-8 JSON payload, hoặc file.
HttpResponse.Body cho phép ghi response body với 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();BodyWriter
Một cách thay thế để ghi response body là sử dụng thuộc tính HttpResponse.BodyWriter. Thuộc tính BodyWriter expose response body như một PipeWriter. API này từ I/O pipelines, và đây là cách tiên tiến, hiệu suất cao để ghi response.
Đặt response trailers
HTTP/2 và HTTP/3 hỗ trợ response trailers. Trailers là headers được gửi kèm với response sau khi response body hoàn tất. Vì trailers được gửi sau response body, trailers có thể được thêm vào response bất cứ lúc nào.
Code sau đây đặt trailers bằng cách sử dụng 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
Cancellation token HttpContext.RequestAborted có thể được sử dụng để thông báo rằng HTTP request đã bị hủy bởi client hoặc server. Cancellation token nên được truyền cho các task chạy lâu để chúng có thể bị hủy nếu request bị hủy. Ví dụ, hủy một database query hoặc HTTP request để lấy dữ liệu trả về trong response.
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();Cancellation token RequestAborted không cần được sử dụng cho các thao tác đọc request body vì các lần đọc luôn ném ngay lập tức khi request bị hủy. Token RequestAborted cũng thường không cần thiết khi ghi response body, vì các lần ghi ngay lập tức no-op khi request bị hủy.
Abort()
Phương thức HttpContext.Abort() có thể được sử dụng để hủy một HTTP request từ phía server. Hủy HTTP request ngay lập tức kích hoạt cancellation token HttpContext.RequestAborted và gửi thông báo cho client rằng server đã hủy request.
Middleware trong ví dụ sau:
- Thêm kiểm tra tùy chỉnh cho các request độc hại.
- Hủy HTTP request nếu request là độc hại.
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
Thuộc tính HttpContext.User được sử dụng để lấy hoặc đặt user, được biểu diễn bởi ClaimsPrincipal, cho request. ClaimsPrincipal thường được đặt bởi ASP.NET Core authentication (xác thực).
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();Features
Thuộc tính HttpContext.Features cung cấp quyền truy cập vào tập hợp các feature interface cho request hiện tại. Vì feature collection có thể thay đổi ngay cả trong context của một request, middleware có thể được sử dụng để sửa đổi collection và thêm hỗ trợ cho các tính năng bổ sung. Một số tính năng nâng cao chỉ có thể truy cập bằng cách sử dụng interface liên quan thông qua feature collection.
Ví dụ sau:
- Lấy
IHttpMinRequestBodyDataRateFeaturetừ features collection. - Đặt
MinDataRatethành null. Điều này loại bỏ tốc độ dữ liệu tối thiểu mà request body phải được gửi bởi client cho HTTP request này.
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 không thread safe
Bài viết này chủ yếu thảo luận về việc sử dụng HttpContext trong request và response flow từ các component Blazor Web App, Razor Pages, controllers, middleware, v.v. Hãy xem xét điều sau khi sử dụng HttpContext bên ngoài request và response flow:
HttpContextKHÔNG thread safe. Truy cập nó từ nhiều thread có thể dẫn đến kết quả không dự đoán được, chẳng hạn như exception và data corruption.- Interface
IHttpContextAccessornên được sử dụng cẩn thận. Như thường lệ,HttpContextkhông được được capture bên ngoài request flow.IHttpContextAccessor: - Phụ thuộc vào
AsyncLocal<T>, có thể có tác động tiêu cực đến hiệu suất trên các lần gọi bất đồng bộ. - Tạo dependency vào "ambient state" có thể làm cho testing khó khăn hơn.
IHttpContextAccessor.HttpContextcó thể lànullnếu được truy cập bên ngoài request flow.- Để truy cập thông tin từ
HttpContextbên ngoài request flow, hãy sao chép thông tin bên trong request flow. Hãy cẩn thận để sao chép dữ liệu thực sự chứ không chỉ là các tham chiếu. - Không capture
IHttpContextAccessor.HttpContexttrong constructor.
Ví dụ sau ghi log các GitHub branch khi được yêu cầu từ endpoint /branch:
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/");
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 thêm header User-Agent một cách động:
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);
}
}Trong code trên, khi HttpContext là null, chuỗi userAgent được đặt thành "Unknown". Nếu có thể, HttpContext nên được truyền tường minh vào service. Truyền tường minh dữ liệu HttpContext:
- Làm cho service API có thể sử dụng nhiều hơn bên ngoài request flow.
- Tốt hơn cho hiệu suất.
- Làm cho code dễ hiểu hơn so với việc phụ thuộc vào ambient state.
Ứng dụng cũng bao gồm PeriodicBranchesLoggerService, là hosted service (dịch vụ được host) chạy bên ngoài request và response flow. Logging từ PeriodicBranchesLoggerService có HttpContext là null. PeriodicBranchesLoggerService được viết để không phụ thuộc vào HttpContext.