Использование HttpContext в ASP.NET Core

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
password=TNkt4taM
HttpRequest.Body A 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 используется из конвейеров ввода-вывода, расширенный высокопроизводительный способ чтения текста запроса.

Средство чтения напрямую обращается к тексту запроса и управляет памятью от имени вызывающего объекта. В отличие от 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-ответ может содержать текст ответа. Текст ответа — это данные, связанные с ответом, например сгенерированным содержимым веб-страницы, полезными данными 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 используется из конвейеров ввода-вывода, и это расширенный высокопроизводительный способ записи ответа.

Средство записи предоставляет прямой доступ к тексту ответа и управляет памятью от имени вызывающего объекта. В отличие от 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 =>
{