Usar HttpContext no ASP.NET Core

HttpContext encapsula todas as informações específicas sobre uma solicitação e uma resposta HTTP específicas. Uma instância HttpContext é inicializada quando uma solicitação HTTP é recebida. A instância HttpContext pode ser acessada por middleware e estruturas de aplicativo, como controladores de API Web, Razor Pages, SignalR, gRPC e muito mais.

Para obter mais informações sobre como acessar o HttpContext, confira Accessar HttpContext no ASP.NET Core.

HttpRequest

HttpContext.Request fornece acesso ao HttpRequest. HttpRequest tem informações sobre a solicitação HTTP de entrada e é inicializada quando uma solicitação HTTP é recebida pelo servidor. HttpRequest não é somente leitura, e o middleware pode alterar os valores de solicitação no pipeline de middleware.

Membros comumente usados de HttpRequest incluem:

Propriedade Descrição Exemplo
HttpRequest.Path O caminho da solicitação. /en/article/getstarted
HttpRequest.Method O método da solicitação. GET
HttpRequest.Headers Uma coleção de cabeçalhos de solicitação. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Uma coleção de valores de rota. A coleção é definida quando a solicitação é correspondida a uma rota. language=en
article=getstarted
HttpRequest.Query Uma coleção de valores de consulta analisados de QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Um método que lê o corpo da solicitação como um formulário e retorna uma coleção de valores de formulário. Para obter informações sobre por que ReadFormAsync deve ser usado para acessar dados de formulário, confira Preferir ReadFormAsync em vez de Request.Form. email=user@contoso.com
password=TNkt4taM
HttpRequest.Body Um Stream para ler o corpo da solicitação. Conteúdo JSON UTF-8

Obter cabeçalhos de solicitação

HttpRequest.Headers fornece acesso aos cabeçalhos de solicitação enviados com a solicitação HTTP. Há duas maneiras de acessar cabeçalhos usando essa coleção:

  • Forneça o nome do cabeçalho ao indexador na coleção de cabeçalhos. O nome do cabeçalho não diferencia maiúsculas de minúsculas. O indexador pode acessar qualquer valor de cabeçalho.
  • A coleção de cabeçalhos também tem propriedades para obter e definir cabeçalhos HTTP comumente usados. As propriedades fornecem uma maneira rápida e controlada pelo IntelliSense de acessar cabeçalhos.
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();

Para obter informações sobre como lidar com eficiência com cabeçalhos que aparecem mais de uma vez, confira Visão geral de StringValues.

Corpo da solicitação de leitura

Uma solicitação HTTP pode incluir um corpo da solicitação. O corpo da solicitação compreende dados associados à solicitação, como o conteúdo de um formulário HTML, conteúdo JSON UTF-8 ou um arquivo.

HttpRequest.Body permite que o corpo da solicitação seja lido com 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 pode ser lido diretamente ou usado com outras APIs que aceitam fluxo.

Observação

APIs mínimas dão suporte à associação de HttpRequest.Body diretamente a um parâmetro Stream.

Habilitar o armazenamento em buffer do corpo da solicitação

O corpo da solicitação só pode ser lido uma vez, do início ao fim. A leitura somente de encaminhamento do corpo da solicitação evita a sobrecarga de armazenar em buffer todo o corpo da solicitação e reduz o uso de memória. No entanto, em alguns cenários, há a necessidade de ler o corpo da solicitação várias vezes. Por exemplo, o middleware pode precisar ler o corpo da solicitação e retroceder a fim de disponibilizá-lo para o ponto de extremidade.

O método de extensão EnableBuffering habilita o buffer do corpo da solicitação HTTP e é a maneira recomendada de habilitar várias leituras. Como uma solicitação pode ter qualquer tamanho, EnableBuffering dá suporte a opções para armazenar em buffer grandes corpos de solicitação em disco ou rejeitá-los inteiramente.

O middleware no exemplo abaixo:

  • Habilita várias leituras com EnableBuffering. Ele precisa ser chamado antes da leitura do corpo da solicitação.
  • Lê o corpo da solicitação.
  • Volta ao início do corpo da solicitação para que outro middleware ou ponto de extremidade possa lê-lo.
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

Uma maneira alternativa de ler o corpo da solicitação é usar a propriedade HttpRequest.BodyReader. A propriedade BodyReader expõe o corpo da solicitação como um PipeReader. Essa API é proveniente de pipelines de E/S, uma maneira avançada e de alto desempenho de ler o corpo da solicitação.

O leitor acessa diretamente o corpo da solicitação e gerencia a memória em nome do chamador. Diferentemente de HttpRequest.Body, o leitor não copia dados de solicitação em um buffer. No entanto, um leitor é mais complicado de usar do que um fluxo e deve ser usado com cuidado.

Para obter informações sobre como ler o conteúdo de BodyReader, confira PipeReader de pipelines de E/S.

HttpResponse

HttpContext.Response fornece acesso ao HttpResponse. HttpResponse é usado para definir informações sobre a resposta HTTP enviada de volta ao cliente.

Membros comumente usados de HttpResponse incluem:

Propriedade Descrição Exemplo
HttpResponse.StatusCode O código de resposta. Precisa ser definido antes de gravar no corpo da resposta. 200
HttpResponse.ContentType O cabeçalho content-type de resposta. Precisa ser definido antes de gravar no corpo da resposta. application/json
HttpResponse.Headers A coleção dos cabeçalhos de resposta. Precisa ser definido antes de gravar no corpo da resposta. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body Um Stream para escrever o corpo da resposta. Página da Web gerada

Definir cabeçalhos de resposta

HttpResponse.Headers fornece acesso aos cabeçalhos de resposta enviados com a resposta HTTP. Há duas maneiras de acessar cabeçalhos usando essa coleção:

  • Forneça o nome do cabeçalho ao indexador na coleção de cabeçalhos. O nome do cabeçalho não diferencia maiúsculas de minúsculas. O indexador pode acessar qualquer valor de cabeçalho.
  • A coleção de cabeçalhos também tem propriedades para obter e definir cabeçalhos HTTP comumente usados. As propriedades fornecem uma maneira rápida e controlada pelo IntelliSense de acessar cabeçalhos.
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();

Um aplicativo não pode modificar cabeçalhos após o início da resposta. Depois que a resposta é iniciada, os cabeçalhos são enviados ao cliente. Uma resposta é iniciada com a liberação do corpo da resposta ou a chamada de HttpResponse.StartAsync(CancellationToken). Uma propriedade HttpResponse.HasStarted indica se a resposta foi iniciada. Um erro é gerado ao tentar modificar cabeçalhos após o início da resposta:

System.InvalidOperationException: os cabeçalhos são somente leitura; a resposta já foi iniciada.

Observação

A menos que o buffer de resposta esteja habilitado, todas as operações de gravação (por exemplo, WriteAsync) liberam o corpo da resposta internamente e marcam a resposta como iniciada. O buffer de resposta é desabilitado por padrão.

Gravar corpo da resposta

Uma resposta HTTP pode incluir um corpo da resposta. O corpo da resposta compreende dados associados à resposta, como conteúdo de página da Web gerado, carga JSON UTF-8 ou um arquivo.

HttpResponse.Body permite que o corpo da resposta seja gravado com 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 pode ser gravado diretamente ou usado com outras APIs que gravam em um fluxo.

BodyWriter

Uma maneira alternativa de escrever o corpo da resposta é usar a propriedade HttpResponse.BodyWriter. A propriedade BodyWriter expõe o corpo da resposta como um PipeWriter. Essa API é de pipelines de E/S e é uma maneira avançada e de alto desempenho de gravar a resposta.

O gravador fornece acesso direto ao corpo da resposta e gerencia a memória em nome do chamador. Diferentemente de HttpResponse.Body, o gravador não copia dados de solicitação em um buffer. No entanto, um gravador é mais complicado de usar do que um fluxo, e um código de gravador deve ser testado minuciosamente.

Para obter informações sobre como gravar conteúdo no BodyWriter, confira PipeWriter de pipelines de E/S.

Definir trailers de resposta

Http/2 e HTTP/3 dão suporte a trailers de resposta. Os trailers são cabeçalhos enviados com a resposta após a conclusão do corpo da resposta. Como os trailers são enviados após o corpo da resposta, os trailers podem ser adicionados à resposta a qualquer momento.

O código a seguir define trailers usando 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

O token de cancelamento HttpContext.RequestAborted pode ser usado para notificar que a solicitação HTTP foi anulada pelo cliente ou servidor. O token de cancelamento deve ser passado para tarefas de execução prolongada para que elas possam ser canceladas se a solicitação for anulada. Por exemplo, a anulação de uma consulta de banco de dados ou uma solicitação HTTP para obter dados a serem retornados na resposta.

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

O token de cancelamento RequestAborted não precisa ser usado em operações de leitura do corpo da solicitação porque as leituras sempre lançam imediatamente quando a solicitação é anulada. O token RequestAborted também costuma ser desnecessário ao gravar corpos de resposta, pois ele grava imediatamente sem operações quando a solicitação é anulada.

Em alguns casos, a transmissão do token RequestAborted para operações de gravação pode ser uma maneira conveniente de forçar um loop de gravação a parar mais cedo com um OperationCanceledException. No entanto, normalmente, é melhor transmitir o token RequestAborted para uma operação assíncrona responsável por recuperar o conteúdo do corpo da resposta.

Observação

APIs mínimas dão suporte à associação de HttpContext.RequestAborted diretamente a um parâmetro CancellationToken.

Abort()

O método HttpContext.Abort() pode ser usado para anular uma solicitação HTTP do servidor. A anulação da solicitação HTTP dispara imediatamente o token de cancelamento HttpContext.RequestAborted e envia uma notificação ao cliente de que o servidor anulou a solicitação.

O middleware no exemplo abaixo:

  • Adiciona uma verificação personalizada de solicitações mal-intencionadas.
  • Anula a solicitação HTTP se a solicitação for mal-intencionada.
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

A propriedade HttpContext.User é usada para obter ou definir o usuário, representado por ClaimsPrincipal, para a solicitação. O ClaimsPrincipal normalmente é definido pela autenticação do 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();

Observação

APIs mínimas dão suporte à associação de HttpContext.User diretamente a um parâmetro ClaimsPrincipal.

Features

A propriedade HttpContext.Features fornece acesso à coleção de interfaces de recurso para a solicitação atual. Como a coleção de recursos é mutável, mesmo no contexto de uma solicitação, o middleware pode ser usado para modificar a coleção e adicionar suporte para recursos adicionais. Alguns recursos avançados só estão disponíveis pelo acesso à interface associada por meio da coleção de recursos.

O exemplo a seguir:

  • Obtém IHttpMinRequestBodyDataRateFeature da coleção de recursos.
  • Define MinDataRate como nulo. Isso remove a taxa mínima de dados pela qual o corpo da solicitação deve ser enviado pelo cliente para essa solicitação 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();

Para obter mais informações sobre como usar recursos de solicitação e HttpContext, confira Recursos de Solicitação no ASP.NET Core.

HttpContext não é thread-safe

Este artigo aborda principalmente o uso de HttpContext no fluxo de solicitação e resposta de Razor Pages, controladores, middleware e afins. Considere o seguinte ao usar HttpContext fora do fluxo de solicitação e resposta:

  • O HttpContextNÃO é thread-safe; acessá-lo de vários threads pode resultar em exceções, corrupção de dados e consequências geralmente imprevisíveis.
  • A interface IHttpContextAccessor deve ser usada com cuidado. Como sempre, o HttpContextnão deve ser capturado fora do fluxo de solicitação. IHttpContextAccessor:
    • Depende de AsyncLocal<T>, que pode ter um impacto negativo no desempenho em chamadas assíncronas.
    • Cria uma dependência do "estado ambiente", o que pode dificultar o teste.
  • IHttpContextAccessor.HttpContext pode ser null se acessado fora do fluxo de solicitação.
  • Para acessar informações de HttpContext fora do fluxo de solicitação, copie as informações dentro do fluxo de solicitação. Tenha cuidado para copiar os dados reais e não apenas referências. Por exemplo, em vez de copiar uma referência para um IHeaderDictionary, copie os valores de cabeçalho relevantes ou copie todo o dicionário chave por chave antes de sair do fluxo de solicitação.
  • Não capture IHttpContextAccessor.HttpContext em um construtor.

O exemplo abaixo registra branches do GitHub quando solicitado do ponto de extremidade /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();

A API do GitHub requer dois cabeçalhos. O cabeçalho User-Agent é adicionado dinamicamente pelo 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);
    }
}

No código anterior, quando HttpContext for null, a cadeia de caracteres userAgent será definida como "Unknown". Se possível, HttpContext deve ser transmitido explicitamente ao serviço. A transmissão explícita de dados de HttpContext:

  • Torna a API de serviço mais utilizável fora do fluxo de solicitação.
  • É melhor para o desempenho.
  • Torna o código mais fácil de entender e raciocinar do que confiar no estado ambiente.

Quando o serviço precisa acessar HttpContext, ele deve considerar a possibilidade de HttpContext ser null quando não chamado de um thread de solicitação.

O aplicativo também inclui PeriodicBranchesLoggerService, que registra os branches abertos do GitHub do repositório especificado a cada 30 segundos:

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 é um serviço hospedado, que é executado fora do fluxo de solicitação e resposta. O registro em log do PeriodicBranchesLoggerService tem um valor de HttpContext nulo. O PeriodicBranchesLoggerService foi escrito para não depender do 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 =>
{