Partilhar via


Use o HttpContext no ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

HttpContext encapsula toda a informação sobre um pedido e resposta HTTP individual. Uma HttpContext instância é inicializada quando um pedido HTTP é recebido. A instância HttpContext é acessível através de middleware e frameworks de aplicações como Blazor Web Apps, controladores de API Web, Razor Pages, SignalR, gRPC, entre outros.

HttpRequest

HttpContext.Request fornece acesso a HttpRequest. HttpRequest tem informação sobre o pedido HTTP recebido, e é inicializado quando um pedido HTTP é recebido pelo servidor. HttpRequest não é apenas de leitura, e o middleware pode alterar os valores dos pedidos no pipeline.

Os membros mais usados em HttpRequest incluem:

Propriedade Description Example
HttpRequest.Path O caminho da solicitação. /en/article/getstarted
HttpRequest.Method O método de pedido. GET
HttpRequest.Headers Uma coleção de cabeçalhos de pedidos. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Uma coleção de valores de rotas. A coleção é definida quando a requisição é correspondida a uma rota. language=en
article=getstarted
HttpRequest.Query Uma coleção de valores de consulta analisados a partir de QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Um método que lê o corpo do pedido como um formulário e devolve uma coleção de valores de formulário. Para informações sobre o motivo pelo qual ReadFormAsync deve ser usado para aceder a dados de formulários, veja Preferir ReadFormAsync em vez de Request.Form. email=user@contoso.com
HttpRequest.Body A Stream para ler o corpo do pedido. Carga útil UTF-8 JSON

Obter cabeçalhos de requisição

HttpRequest.Headers fornece acesso aos cabeçalhos do pedido enviados com o pedido HTTP. Existem duas formas de aceder a cabeçalhos usando esta coleção:

  • Forneça o nome do cabeçalho ao indexador na coleção de cabeçalhos. O nome do cabeçalho não distingue entre maiúsculas e minúsculas. O indexador pode aceder a 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 forma rápida e baseada no IntelliSense de aceder aos 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 informações sobre como lidar eficientemente com cabeçalhos que aparecem mais do que uma vez, veja Uma breve análise a StringValues.

Ler o corpo do pedido

Um pedido HTTP pode incluir um corpo de pedido. O corpo do pedido são dados associados ao pedido, como o conteúdo de um formulário HTML, carga útil UTF-8 JSON ou um ficheiro.

HttpRequest.Bodypermite que o corpo do pedido 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 utilizado com outras APIs que aceitam fluxo.

Observação

As APIs mínimas suportam a ligação HttpRequest.Body direta a um Stream parâmetro.

Ativar o buffering do corpo de pedidos

O corpo do pedido só pode ser lido uma vez, do início ao fim. A leitura apenas direta do corpo do pedido evita a sobrecarga de armazenar em buffer todo o corpo do pedido e reduz o uso de memória. No entanto, em alguns cenários, é necessário ler o corpo do pedido várias vezes. Por exemplo, o middleware pode precisar de ler o corpo do pedido e depois rebobinar o corpo do pedido para que fique disponível para o endpoint.

O EnableBuffering método de extensão permite o buffering do corpo do pedido HTTP e é a forma recomendada para permitir múltiplas leituras. Como um pedido pode ter qualquer tamanho, EnableBuffering suporta opções para armazenar grandes corpos de pedido no disco ou rejeitá-los completamente.

O middleware no exemplo seguinte:

  • Permite múltiplas leituras com EnableBuffering. Deve ser chamado antes de ler o conteúdo do pedido.
  • Ler o corpo do pedido.
  • Rebobina o corpo do pedido até ao início para que outro middleware ou o endpoint possam 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 forma alternativa de ler o corpo do pedido é usar a HttpRequest.BodyReader propriedade. A BodyReader propriedade expõe o corpo do pedido como um PipeReader. Esta API provém dos pipelines de I/O, uma forma avançada e de alto desempenho de ler o corpo do pedido.

O leitor acede diretamente ao corpo do pedido e gere a memória em nome do chamador. Ao contrário de HttpRequest.Body, o leitor não copia os dados do pedido para um buffer. No entanto, um leitor é mais complicado de usar do que um stream e deve ser usado com cautela.

Para informações sobre como ler conteúdo de BodyReader, veja PipeReader de pipelines de I/O.

HttpResponse

HttpContext.Response fornece acesso a HttpResponse. HttpResponse é usado para definir a informação da resposta HTTP enviada de volta ao cliente.

Os membros mais usados em HttpResponse incluem:

Propriedade Description Example
HttpResponse.StatusCode O código de resposta. Deve ser definido antes de escrever para o corpo da resposta. 200
HttpResponse.ContentType O cabeçalho da resposta content-type. Deve ser definido antes de escrever para o corpo da resposta. application/json
HttpResponse.Headers Uma coleção de cabeçalhos de resposta. Deve ser definido antes de escrever para o corpo da resposta. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body A Stream para escrever o corpo da resposta. Página web gerada

Definir cabeçalhos de resposta

HttpResponse.Headers fornece acesso aos cabeçalhos de resposta enviados com a resposta HTTP. Existem duas formas de aceder a cabeçalhos usando esta coleção:

  • Forneça o nome do cabeçalho ao indexador na coleção de cabeçalhos. O nome do cabeçalho não distingue entre maiúsculas e minúsculas. O indexador pode aceder a qualquer valor de cabeçalho.
  • Use as propriedades da coleção de cabeçalhos para obter e definir cabeçalhos HTTP mais comuns. As propriedades fornecem uma forma rápida e baseada no IntelliSense de aceder aos 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();

Uma aplicação não pode modificar cabeçalhos depois de a resposta ter começado. Assim que a resposta começa, os cabeçalhos são enviados ao cliente. Uma resposta é iniciada ao limpar o corpo da resposta ou ao chamar HttpResponse.StartAsync(CancellationToken). A HttpResponse.HasStarted propriedade indica se a resposta já começou. Um erro é lançado ao tentar modificar cabeçalhos depois de a resposta ter começado:

System.InvalidOperationException: Os cabeçalhos são só de leitura, a resposta já começou.

Observação

A menos que o buffering de resposta esteja ativado, todas as operações de escrita (por exemplo, WriteAsync) limpam internamente o corpo da resposta e marcam a resposta como iniciada. O buffering de resposta está desativado por padrão.

Escrever o corpo da resposta

Uma resposta HTTP pode incluir um corpo de resposta. O corpo da resposta são dados associados à resposta, como conteúdo de página web gerado, payload UTF-8 JSON ou um ficheiro.

HttpResponse.Body permite que o corpo da resposta seja escrito 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 escrito diretamente ou usado com outras APIs que escrevem num fluxo.

BodyWriter

Uma forma alternativa de escrever o corpo da resposta é usar a HttpResponse.BodyWriter propriedade. A BodyWriter propriedade expõe o corpo de resposta como um PipeWriter. Esta API é oriunda de I/O pipelines, e é uma forma avançada e de alto desempenho de escrever a resposta.

O escritor fornece acesso direto ao corpo da resposta e gere a memória em nome do chamador. Ao contrário de HttpResponse.Body, a operação de escrita não copia os dados da solicitação para um buffer. No entanto, um gravador é mais complicado de usar do que um stream, e o código do gravador deve ser testado minuciosamente.

Para informações sobre como escrever conteúdo para BodyWriter, consulte PipeWriter de pipelines de I/O.

Trailers de resposta

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

O seguinte código define os reboques 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 HttpContext.RequestAborted token de cancelamento pode ser usado para notificar que o pedido HTTP foi abortado pelo cliente ou servidor. O token de cancelamento deve ser passado para tarefas de longa duração para que possam ser canceladas caso a requisição seja interrompida. Por exemplo, abortar uma consulta à base de dados ou um pedido HTTP para obter dados de volta 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 RequestAborted token de cancelamento não precisa de ser usado para operações de leitura do corpo do pedido porque as leituras são sempre lançadas imediatamente quando o pedido é abortado. O RequestAborted token também é geralmente desnecessário ao escrever corpos de resposta, porque escreve imediatamente no-op quando o pedido é abortado.

Em alguns casos, passar o RequestAborted token nas operações de escrita pode ser uma forma conveniente de forçar um loop de escrita a sair mais cedo com um OperationCanceledException. No entanto, normalmente é melhor passar o RequestAborted token para operações assíncronas que são responsáveis por obter o conteúdo do corpo da resposta.

Observação

As APIs mínimas suportam a ligação HttpContext.RequestAborted direta a um CancellationToken parâmetro.

Abort()

O HttpContext.Abort() método pode ser usado para abortar um pedido HTTP do servidor. Abortar o pedido HTTP dispara imediatamente o HttpContext.RequestAborted token de cancelamento e envia uma notificação ao cliente de que o servidor abortou o pedido.

O middleware no exemplo seguinte:

  • Adiciona uma verificação personalizada para pedidos maliciosos.
  • Aborta o pedido HTTP se o pedido for malicioso.
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 HttpContext.User propriedade é usada para obter ou definir o utilizador, representado por ClaimsPrincipal, para o pedido. O ClaimsPrincipal é normalmente definido por autenticação 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

As APIs mínimas suportam a ligação HttpContext.User direta a um ClaimsPrincipal parâmetro.

Features

A HttpContext.Features propriedade fornece acesso à coleção de interfaces de funcionalidades para o pedido atual. Como a coleção de funcionalidades é mutável mesmo no contexto de um pedido, o middleware pode ser usado para modificar a coleção e adicionar suporte para funcionalidades adicionais. Algumas funcionalidades avançadas só estão disponíveis acedendo à interface associada através da coleção de funcionalidades.

O exemplo a seguir:

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 mais informações sobre o uso de funcionalidades de pedido e HttpContext, veja Funcionalidades de Pedido em ASP.NET Core.

O HttpContext não é seguro para threads

Este artigo discute principalmente a utilização de HttpContext no fluxo de pedidos e respostas de componentes Blazor Web App, Páginas Razor, controladores, middleware, entre outros. Considere o seguinte ao usar HttpContext fora do fluxo de pedido e resposta:

  • HttpContext é seguro para rosca. Aceder a ela a partir de múltiplos threads pode resultar em resultados imprevisíveis, como exceções e corrupção de dados.
  • A IHttpContextAccessor interface deve ser usada com cautela. Como sempre, o HttpContextnão deve ser capturado fora do fluxo de requisição. IHttpContextAccessor:
    • Depende de AsyncLocal<T>, o que pode ter um impacto negativo no desempenho das chamadas assíncronas.
    • Cria uma dependência do "estado ambiente", o que pode tornar os testes mais difíceis.
  • IHttpContextAccessor.HttpContext Pode ser null , se for acedido fora do fluxo de pedidos.
  • Para aceder à informação fora HttpContext do fluxo de pedido, copie a informação dentro do fluxo de pedido. Tem cuidado em 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 relevantes do cabeçalho ou copie toda a chave do dicionário, uma por uma, antes de sair do fluxo de solicitação.
  • Não capture IHttpContextAccessor.HttpContext num construtor.

O seguinte exemplo regista os ramos do GitHub quando solicitado do /branch endpoint:

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 User-Agent cabeçalho é 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();

A 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 o HttpContext é null, a cadeia userAgent é definida como "Unknown". Se possível, HttpContext deve ser explicitamente transmitido ao serviço. Passagem explícita dos dados HttpContext :

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

Quando o serviço tem de aceder a HttpContext, deve ter em conta a possibilidade de HttpContext ser null chamado quando não é chamado de um thread de pedido.

A aplicação também inclui PeriodicBranchesLoggerService, que regista as ramificações abertas 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 alojado, que funciona fora do fluxo de pedidos e respostas. O registo a partir do PeriodicBranchesLoggerService tem um null HttpContext. 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 =>
{

Recursos adicionais

Para mais informações sobre como aceder ao HttpContext, veja Aceder ao HttpContext no ASP.NET Core.