Использование HttpContext в ASP.NET Core
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 8 этой статьи.
HttpContext инкапсулирует все сведения о отдельном HTTP-запросе и ответе. Экземпляр HttpContext
инициализируется при получении HTTP-запроса. Экземпляр HttpContext
доступен по промежуточному слоям и платформам приложений, таким как контроллеры веб-API, Razor Pages, SignalRgRPC и многое другое.
Дополнительные сведения о доступе к файлу HttpContext
см. в статье Access HttpContext в ASP.NET Core.
HttpRequest
HttpContext.Request предоставляет доступ к HttpRequest. HttpRequest
содержит сведения о входящем HTTP-запросе и инициализируется при получении HTTP-запроса сервером. HttpRequest
не доступно только для чтения, и ПО промежуточного слоя может изменять значения запросов в конвейере ПО промежуточного слоя.
Часто используемые элементы для HttpRequest
включения:
Свойство | Description | Пример |
---|---|---|
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 следует использовать для доступа к данным формы, см. в статье Prefer ReadFormAsync over Request.Form. |
email=user@contoso.com |
HttpRequest.Body | A Stream для чтения текста запроса. | Полезные данные JSON UTF-8 |
Получение заголовков запросов
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 используется из конвейеров ввода-вывода, расширенный высокопроизводительный способ чтения текста запроса.
Средство чтения напрямую обращается к тексту запроса и управляет памятью от имени вызывающего объекта. В отличие от HttpRequest.Body
этого, средство чтения не копирует данные запроса в буфер. Однако средство чтения более сложно использовать, чем поток, и его следует использовать с осторожностью.
Сведения о том, как считывать содержимое из BodyReader
, см. в разделе конвейеров ввода-вывода PipeReader.
HttpResponse
HttpContext.Response предоставляет доступ к HttpResponse. HttpResponse
используется для задания сведений о http-ответе, отправляемом клиенту.
Часто используемые элементы для HttpResponse
включения:
Свойство | Description | Пример |
---|---|---|
HttpResponse.StatusCode | Код ответа. Необходимо задать перед записью в текст ответа. | 200 |
HttpResponse.ContentType | Заголовок ответа content-type . Необходимо задать перед записью в текст ответа. |
application/json |
HttpResponse.Headers | Коллекция заголовков ответов. Необходимо задать перед записью в текст ответа. | server=Kestrel x-custom-header=MyValue |
HttpResponse.Body | A 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-ответ может содержать текст ответа. Текст ответа — это данные, связанные с ответом, такие как созданное содержимое веб-страницы, полезные данные JSON UTF-8 или файл.
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 используется из конвейеров ввода-вывода, и это расширенный высокопроизводительный способ записи ответа.
Средство записи предоставляет прямой доступ к тексту ответа и управляет памятью от имени вызывающего объекта. В отличие от HttpResponse.Body
этого, запись не копирует данные запроса в буфер. Однако модуль записи сложнее использовать, чем код потока и записи, следует тщательно протестировать.
Сведения о том, как записывать содержимое BodyWriter
в , см. в разделе конвейеров ввода-вывода 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 не является потокобезопасным
В этой статье в основном рассматривается использование HttpContext
в потоке запросов и ответов из Razor Pages, контроллеров, ПО промежуточного слоя и т. д. При использовании HttpContext
за пределами потока запроса и ответов учитывайте следующее:
HttpContext
НЕ является потокобезопасным, доступ к нему из нескольких потоков может привести к исключениям, повреждению данных и, как правило, непредсказуемым результатам.- Интерфейс IHttpContextAccessor следует использовать с осторожностью. Как правило,
HttpContext
не рекомендуется захватывать за пределами потока запроса.IHttpContextAccessor
:- Использует AsyncLocal<T>, что может негативно повлиять на производительность асинхронных вызовов.
- Создает зависимость от "внешнего состояния", что может усложнить тестирование.
- IHttpContextAccessor.HttpContext может иметь значение
null
за пределами потока запроса. - Чтобы получить доступ к информации из
HttpContext
за пределами потока запросов, скопируйте данные внутри потока запросов. Следует скопировать фактические данные, а не просто ссылки. Например, вместо копирования ссылки на объектIHeaderDictionary
, скопируйте соответствующие значения заголовков или скопируйте весь словарь от ключа к ключу перед выходом из потока запросов. - Не захватывайте
IHttpContextAccessor.HttpContext
в конструкторе.
В следующих примерах регистрируются ветви GitHub при запросе из конечной точки /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/");
// 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();
Для API GitHub требуется два заголовка. Заголовок 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 =>
{
ASP.NET Core