Uso de HttpContext en ASP.NET Core

HttpContext encapsula toda la información sobre una solicitud y una respuesta HTTP individual. Una instancia de HttpContext se inicializa cuando se recibe una solicitud HTTP. Se puede acceder a la instancia de HttpContext mediante middleware y marcos de aplicaciones, como controladores de API web, Razor Pages, SignalR, gRPC, etc.

Para obtener más información sobre el acceso a HttpContext, vea Acceso a HttpContext en ASP.NET Core.

HttpRequest

HttpContext.Request proporciona acceso a HttpRequest. HttpRequest tiene información sobre la solicitud HTTP entrante y se inicializa cuando el servidor recibe una solicitud HTTP. HttpRequest no es de solo lectura y el middleware puede cambiar los valores de solicitud en la canalización de middleware.

Los miembros usados habitualmente en HttpRequest incluyen:

Propiedad Descripción Ejemplo
HttpRequest.Path Ruta de acceso de la solicitud. /en/article/getstarted
HttpRequest.Method El método de solicitud. GET
HttpRequest.Headers Colección de encabezados de solicitud. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Una colección de valores de ruta. La colección se establece cuando la solicitud coincide con una ruta. language=en
article=getstarted
HttpRequest.Query Colección de valores de consulta analizados a partir de QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Método que lee el cuerpo de la solicitud como un formulario y devuelve una colección de valores de formulario. Para obtener información sobre por qué ReadFormAsync se debe usar para acceder a los datos del formulario, vea Preferir ReadFormAsync sobre Request.Form. email=user@contoso.com
password=TNkt4taM
HttpRequest.Body Stream para leer el cuerpo de la solicitud. Carga de UTF-8 JSON

Obtención de los encabezados de solicitud

HttpRequest.Headers proporciona acceso a los encabezados de solicitud enviados con la solicitud HTTP. Hay dos maneras de acceder a los encabezados mediante esta colección:

  • Proporcione el nombre del encabezado al indexador en la colección de encabezados. El nombre del encabezado no distingue mayúsculas de minúsculas. El indexador puede tener acceso a cualquier valor de encabezado.
  • La colección de encabezados también tiene propiedades para obtener y establecer encabezados HTTP de uso común. Las propiedades proporcionan una manera rápida y controlada por IntelliSense de acceder a los encabezados.
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 obtener información sobre cómo controlar de forma eficaz los encabezados que aparecen más de una vez, vea Un breve vistazo a StringValues.

Lectura del cuerpo de la solicitud

Una solicitud HTTP puede incluir un cuerpo de la solicitud. El cuerpo de la solicitud son datos asociados a la solicitud, como el contenido de un formulario HTML, la carga UTF-8 JSON o un archivo.

HttpRequest.Body permite que el cuerpo de la solicitud se lea con 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 se puede leer directamente o usar con otras API que aceptan flujos.

Nota

Las API mínimas permiten enlazar HttpRequest.Body directamente a un parámetro Stream.

Habilitación del almacenamiento en búfer del cuerpo de la solicitud

El cuerpo de la solicitud solo se puede leer una vez, de principio a fin. La lectura de solo avance del cuerpo de la solicitud evita la sobrecarga de almacenar en búfer todo el cuerpo de la solicitud y reduce el uso de la memoria. Sin embargo, en algunos escenarios, es necesario leer el cuerpo de la solicitud varias veces. Por ejemplo, es posible que el middleware tenga que leer el cuerpo de la solicitud y, a continuación, rebobinarlo para que esté disponible para el punto de conexión.

El método de extensión EnableBuffering habilita el almacenamiento en búfer del cuerpo de la solicitud HTTP y es la forma recomendada de habilitar varias lecturas. Dado que una solicitud puede tener cualquier tamaño, EnableBuffering admite opciones para almacenar en búfer cuerpos de solicitud grandes en el disco o rechazarlos por completo.

El middleware en el ejemplo siguiente:

  • Habilita varias lecturas con EnableBuffering. Se debe llamar antes de leer el cuerpo de la solicitud.
  • Lee el cuerpo de la solicitud.
  • Rebobina el cuerpo de la solicitud hasta el principio para que otro middleware o el punto de conexión puedan leerlo.
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

Una manera alternativa de leer el cuerpo de la solicitud es usar la propiedad HttpRequest.BodyReader. La propiedad BodyReader expone el cuerpo de la solicitud como PipeReader. Esta API procede de canalizaciones de E/S, una manera avanzada y de alto rendimiento para leer el cuerpo de la solicitud.

El lector accede directamente al cuerpo de la solicitud y administra la memoria en nombre del autor de la llamada. A diferencia de HttpRequest.Body, el lector no copia los datos de solicitud en un búfer. Sin embargo, es más complicado usar un lector que un flujo y debe utilizarse con precaución.

Para obtener información sobre cómo leer contenido de BodyReader, consulte PipeReader para canalizaciones de E/S.

HttpResponse

HttpContext.Response proporciona acceso a HttpResponse. HttpResponse se usa para establecer información sobre la respuesta HTTP devuelta al cliente.

Los miembros usados habitualmente en HttpResponse incluyen:

Propiedad Descripción Ejemplo
HttpResponse.StatusCode Código de respuesta. Debe establecerse antes de escribir en el cuerpo de la respuesta. 200
HttpResponse.ContentType Encabezado content-type de respuesta. Debe establecerse antes de escribir en el cuerpo de la respuesta. application/json
HttpResponse.Headers Una colección de encabezados de respuesta. Debe establecerse antes de escribir en el cuerpo de la respuesta. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body Stream para escribir el cuerpo de la respuesta. Página web generada

Establecimiento de los encabezado de respuesta

HttpResponse.Headers proporciona acceso a los encabezados de respuesta enviados con la respuesta HTTP. Hay dos maneras de acceder a los encabezados mediante esta colección:

  • Proporcione el nombre del encabezado al indexador en la colección de encabezados. El nombre del encabezado no distingue mayúsculas de minúsculas. El indexador puede tener acceso a cualquier valor de encabezado.
  • La colección de encabezados también tiene propiedades para obtener y establecer encabezados HTTP de uso común. Las propiedades proporcionan una manera rápida y controlada por IntelliSense de acceder a los encabezados.
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();

Una aplicación no puede modificar los encabezados una vez iniciada la respuesta. Una vez iniciada la respuesta, los encabezados se envían al cliente. Para iniciar una respuesta, se vacía el cuerpo de la respuesta o se llama a HttpResponse.StartAsync(CancellationToken). La propiedad HttpResponse.HasStarted indica si se ha iniciado la respuesta. Se produce un error al intentar modificar los encabezados una vez iniciada la respuesta:

System.InvalidOperationException: los encabezados son de solo lectura, y la respuesta ya se ha iniciado.

Nota:

A menos que el almacenamiento en búfer de respuesta esté habilitado, todas las operaciones de escritura (por ejemplo, WriteAsync) vacían el cuerpo de la respuesta internamente y marcan la respuesta como iniciada. El almacenamiento en búfer de respuesta está deshabilitado de forma predeterminada.

Escritura del cuerpo de la respuesta

Una respuesta HTTP puede incluir un cuerpo de respuesta. El cuerpo de la respuesta son datos asociados a la respuesta, como el contenido generado de la página web, la carga de UTF-8 JSON o un archivo.

HttpResponse.Body permite que el cuerpo de la respuesta se escriba con 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 se puede escribir directamente o se puede usar con otras API que escriben en un flujo.

BodyWriter

Una manera alternativa de escribir el cuerpo de la respuesta es usar la propiedad HttpResponse.BodyWriter. La propiedad BodyWriter expone el cuerpo de la respuesta como PipeWriter. Esta API procede de canalizaciones de E/S y es una manera avanzada y de alto rendimiento de escribir la respuesta.

El escritor proporciona acceso directo al cuerpo de la respuesta y administra la memoria en nombre del autor de la llamada. A diferencia de HttpResponse.Body, el escritor no copia los datos de solicitud en un búfer. Sin embargo, es más complicado usar un escritor que un flujo, y el código del escritor debe probarse exhaustivamente.

Para obtener información sobre cómo escribir contenido en BodyWriter, consulte PipeWriter para canalizaciones de E/S.

Establecimiento de finalizadores de respuesta

HTTP/2 y HTTP/3 admiten finalizadores de respuesta. Los finalizadores son encabezados enviados con la respuesta una vez completado el cuerpo de la respuesta. Dado que los finalizadores se envían después del cuerpo de la respuesta, los finalizadores se pueden agregar a la respuesta en cualquier momento.

El código siguiente establece finalizadores mediante 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

El token de cancelación HttpContext.RequestAborted se puede usar para notificar que el cliente o el servidor han anulado la solicitud HTTP. El token de cancelación debe pasarse a tareas de larga duración para que se puedan cancelar si se anula la solicitud. Por ejemplo, anular una consulta de base de datos o una solicitud HTTP para obtener datos que se devuelven en la respuesta.

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

No es necesario usar el token de cancelación RequestAborted para las operaciones de lectura del cuerpo de la solicitud porque las lecturas siempre se inician inmediatamente cuando se anula la solicitud. El token RequestAborted también suele ser innecesario al escribir cuerpos de respuesta, ya que escribe inmediatamente sin operación cuando se anula la solicitud.

En algunos casos, pasar el token RequestAborted a las operaciones de escritura puede ser una manera cómoda de forzar que un bucle de escritura se cierre pronto con OperationCanceledException. Sin embargo, normalmente es mejor pasar el token RequestAborted a cualquier operación asincrónica responsable de recuperar el contenido del cuerpo de la respuesta en su lugar.

Nota

Las API mínimas permiten enlazar HttpContext.RequestAborted directamente a un parámetro CancellationToken.

Abort()

El método HttpContext.Abort() se puede usar para anular una solicitud HTTP del servidor. La anulación de la solicitud HTTP desencadena inmediatamente el token de cancelación HttpContext.RequestAborted y envía una notificación al cliente de que el servidor ha anulado la solicitud.

El middleware en el ejemplo siguiente:

  • Agrega una comprobación personalizada para solicitudes malintencionadas.
  • Anula la solicitud HTTP si la solicitud es malintencionada.
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

La propiedad HttpContext.User se usa para obtener o establecer el usuario, representado por ClaimsPrincipal, para la solicitud. ClaimsPrincipal normalmente se establece mediante la autenticación de 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();

Nota

Las API mínimas permiten enlazar HttpContext.User directamente a un parámetro ClaimsPrincipal.

Features

La propiedad HttpContext.Features proporciona acceso a la colección de interfaces de características para la solicitud actual. Puesto que la colección de características es mutable incluso en el contexto de una solicitud, se puede usar middleware para modificarla y para agregar compatibilidad con más características. Algunas características avanzadas solo están disponibles al acceder a la interfaz asociada a través de la colección de características.

En el ejemplo siguiente:

  • Obtiene IHttpMinRequestBodyDataRateFeature de la colección de características.
  • Se establece MinDataRate en null. Esto quita la velocidad de datos mínima que el cliente debe enviar en el cuerpo de la solicitud para esta solicitud 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 obtener más información sobre el uso de características de solicitud y HttpContext, vea Características de solicitud de ASP.NET Core.

HttpContext no es seguro para subprocesos.

En este artículo se describe principalmente el uso de HttpContext en el flujo de solicitud y respuesta de Razor Pages, controladores, middleware, etc. Tenga en cuenta lo siguiente al usar HttpContext fuera del flujo de solicitud y respuesta:

  • HttpContextNO es seguro para subprocesos, el acceso a él desde varios subprocesos puede dar lugar a excepciones, daños en los datos y, por lo general, resultados impredecibles.
  • La interfaz IHttpContextAccessor se debe utilizar con precaución. Como siempre, HttpContextno se debe capturar fuera del flujo de solicitud. IHttpContextAccessor:
    • Se basa en AsyncLocal<T> que puede tener un impacto negativo en el rendimiento en las llamadas asincrónicas.
    • Crea una dependencia de "estado ambiente" que puede dificultar las pruebas.
  • IHttpContextAccessor.HttpContext puede ser si se tiene acceso a null fuera del flujo de solicitud.
  • Para acceder a la información desde fuera del flujo de solicitud HttpContext, copie la información dentro del flujo de solicitud. Tenga cuidado de copiar los datos reales y no solo las referencias. Por ejemplo, en lugar de copiar una referencia a IHeaderDictionary, copie los valores de encabezado pertinentes o copie la clave de diccionario completa por clave antes de salir del flujo de solicitud.
  • No capture IHttpContextAccessor.HttpContext en un constructor.

El ejemplo siguiente registra ramas de GitHub cuando se solicita desde el punto de conexión /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();

La API de GitHub requiere dos encabezados. El encabezado User-Agent se agrega dinámicamente mediante 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();

El 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);
    }
}

En el código anterior, cuando HttpContext es null, la cadena userAgent se establece en "Unknown". Si es posible, HttpContext debe pasarse explícitamente al servicio. El paso explícito de datos HttpContext:

  • Hace que la API del servicio sea más utilizable fuera del flujo de solicitud.
  • Es mejor para el rendimiento.
  • Hace que el código sea más fácil de entender y razonar que basarse en el estado ambiente.

Cuando el servicio debe tener acceso a HttpContext, debe tener en cuenta la posibilidad de HttpContext ser null cuando se llama desde un subproceso de solicitud.

La aplicación también incluye PeriodicBranchesLoggerService, que registra las ramas de GitHub abiertas del repositorio especificado 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 es un servicio hospedado, que se ejecuta fuera del flujo de solicitud y respuesta. El registro de PeriodicBranchesLoggerService tiene un valor NULL HttpContext. PeriodicBranchesLoggerService se ha escrito para que no dependa de 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 =>
{