Start Debugging

Migrar de System.Web.HttpContext a Microsoft.AspNetCore.Http.HttpContext

Una migración práctica del System.Web.HttpContext de ASP.NET Framework al HttpContext de ASP.NET Core 11: HttpContext.Current, el mapa de propiedades, Server.MapPath, Session y el shim de los adaptadores System.Web para migraciones incrementales.

La única línea que rompe más migraciones de ASP.NET Framework que cualquier otra es HttpContext.Current. No existe en ASP.NET Core. No hay un contexto ambiental estático al que acceder desde una clase arbitraria, el tipo HttpContext es un tipo distinto en un espacio de nombres distinto (Microsoft.AspNetCore.Http.HttpContext, no System.Web.HttpContext), y la mayoría de las propiedades de las que dependías se movieron, cambiaron de forma o desaparecieron. Este artículo mapea la API antigua a la nueva para .NET 11 / ASP.NET Core 11, y luego muestra los dos caminos reales hacia adelante: una reescritura limpia para el código que controlas, y los adaptadores oficiales System.Web cuando tienes un montón de bibliotecas compartidas que pasan HttpContext de un lado a otro y no se pueden reescribir de una sola vez.

Para un handler pequeño o un único controlador, la reescritura es de una hora. Para un monolito donde HttpContext.Current está enhebrado a través de una capa de negocio en un ensamblado separado, calcula días y echa mano de los adaptadores para que las bibliotecas sigan compilando contra ambos frameworks mientras migras aplicación por aplicación. Nada de la semántica HTTP cambia; lo que cambia es cómo alcanzas la solicitud, que el ciclo de vida ahora está estrictamente vinculado a la solicitud, y que no hay afinidad de hilo en la que apoyarse.

Por qué esta migración no es un buscar-y-reemplazar

System.Web.HttpContext y Microsoft.AspNetCore.Http.HttpContext son objetos genuinamente distintos, y las brechas son de comportamiento, no solo cosméticas:

La propia guía de migración de HttpContext de Microsoft plantea esto como dos estrategias, y la elección dirige todo lo de abajo: reescritura completa, o adaptadores System.Web para un movimiento incremental.

Qué se rompe

ÁreaASP.NET FrameworkASP.NET Core 11Severidad
Contexto ambientalHttpContext.CurrentIHttpContextAccessor (registrar con AddHttpContextAccessor)alta
Ciclo de vida del contextoUsable tras la solicitud a vecesObjectDisposedException tras finalizar la solicitudalta
Seguridad de hilosSolicitud con afinidad de hiloSin afinidad de hilo a través de awaitalta
Escribir en la respuestaResponse.Write(s)await Response.WriteAsync(s)media
Leer formulario / cuerpoRequest.Form, Request.InputStream (sync)await Request.ReadFormAsync(), Request.Body (lectura única)media
Cabeceras / cookies de respuestaEstablecer en cualquier momentoEstablecer antes de que comience la respuesta (o vía OnStarting)media
Rutas físicasServer.MapPath("~/x")IWebHostEnvironment.ContentRootPath / WebRootPath + Path.Combinemedia
SessionSession["k"], auto-serializada, con bloqueoHttpContext.Session.GetString/SetString, basada en bytes, sin bloqueomedia
Codificación HTMLServer.HtmlEncodeSystem.Net.WebUtility.HtmlEncode / HtmlEncoderbaja
URL de la solicitudRequest.Url, Request.RawUrlRequest.Scheme/Host/Path/QueryString o GetDisplayUrl()baja

Lista de verificación previa

Pasos de migración

Paso 1: Registra el accesor y deja de recurrir a HttpContext.Current

Reemplaza el acceso ambiental con inyección explícita. En Program.cs:

// .NET 11, ASP.NET Core 11, C# 14
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor(); // enables IHttpContextAccessor

var app = builder.Build();
app.MapControllers();
app.Run();

Un servicio que antes leía HttpContext.Current ahora recibe IHttpContextAccessor:

// .NET 11, ASP.NET Core 11, C# 14
public sealed class CurrentUserService(IHttpContextAccessor accessor)
{
    public string? UserId =>
        accessor.HttpContext?.User.FindFirst("sub")?.Value;
}

No guardes accessor.HttpContext en un campo. Léelo en el punto de uso cada vez, porque el campo capturaría un contexto de una solicitud y lo entregaría a otra, o a ninguna. Dentro de un controlador o de una API mínima ya tienes HttpContext como propiedad o parámetro, así que prefiere pasarlo explícitamente y omite el accesor por completo.

Verifica: la solución compila sin referencias a System.Web en los proyectos reescritos, y una solicitud que ejercita CurrentUserService devuelve el id de usuario esperado.

Paso 2: Traduce las propiedades de la solicitud

La mayoría de los miembros de Request se movieron en lugar de desaparecer. El mapeo que cubre los casos comunes:

// .NET 11, ASP.NET Core 11, C# 14
string method      = httpContext.Request.Method;          // was HttpMethod
bool   isHttps     = httpContext.Request.IsHttps;         // was IsSecureConnection
string? remoteIp   = httpContext.Connection.RemoteIpAddress?.ToString(); // was UserHostAddress
string userAgent   = httpContext.Request.Headers.UserAgent.ToString();

// Query string: IQueryCollection, indexer never throws on a missing key
string q = httpContext.Request.Query["key"].ToString(); // "" if absent

// Full URL: no single Request.Url anymore
// using Microsoft.AspNetCore.Http.Extensions;
string url = httpContext.Request.GetDisplayUrl();

Leer el formulario o el cuerpo es asíncrono y el cuerpo es un stream de solo avance que puedes leer una vez:

// .NET 11, ASP.NET Core 11, C# 14
if (httpContext.Request.HasFormContentType)
{
    IFormCollection form = await httpContext.Request.ReadFormAsync();
    string firstName = form["firstname"].ToString();
}

Verifica: golpea un endpoint que lea query, formulario y una cabecera; comprueba que los valores coinciden con lo que la aplicación Framework devolvía para la misma solicitud.

Paso 3: Traduce la respuesta, y respeta cuándo se pueden establecer las cabeceras

Escribir es asíncrono, y las cabeceras y cookies deben establecerse antes de que el cuerpo empiece a fluir:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Response.StatusCode = StatusCodes.Status200OK;
httpContext.Response.ContentType = "application/json";
httpContext.Response.Headers["X-Custom"] = "value"; // before first write
await httpContext.Response.WriteAsync(payload);

Si estás en middleware y necesitas establecer cabeceras justo antes de que se envíe la respuesta, usa el callback en lugar de establecerlas tarde:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Response.OnStarting(static state =>
{
    var ctx = (HttpContext)state;
    ctx.Response.Headers["X-Late"] = "value";
    return Task.CompletedTask;
}, httpContext);

Verifica: inspecciona las cabeceras de respuesta con curl -i; confirma que la cabecera está presente y que no obtienes una excepción response has already started bajo carga.

Paso 4: Reemplaza Server.MapPath con IWebHostEnvironment

Server.MapPath("~/App_Data/x.json") no tiene equivalente. Inyecta IWebHostEnvironment y combina las rutas tú mismo:

// .NET 11, ASP.NET Core 11, C# 14
public sealed class FileService(IWebHostEnvironment env)
{
    public string DataPath(string name) =>
        Path.Combine(env.ContentRootPath, "App_Data", name); // project root
    public string AssetPath(string name) =>
        Path.Combine(env.WebRootPath, name);                 // wwwroot
}

ContentRootPath es la raíz del proyecto (el antiguo ~/), WebRootPath es wwwroot (la antigua raíz de archivos estáticos). Para la codificación HTML, Server.HtmlEncode se convierte en System.Net.WebUtility.HtmlEncode o, en DI, un HtmlEncoder inyectado.

Verifica: una solicitud que carga un archivo resuelve la misma ruta absoluta que esperas, tanto en Windows como en Linux (el Path.Combine lo mantiene portable).

Paso 5: Mueve Session, sabiendo que se comporta de forma distinta

La session de ASP.NET Core es opcional, basada en bytes, no se serializa automáticamente y no ofrece bloqueo por solicitud. Regístrala:

// .NET 11, ASP.NET Core 11, C# 14
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
// ...
app.UseSession(); // before endpoints

Luego cambia el indexador por los helpers tipados:

// .NET 11, ASP.NET Core 11, C# 14
httpContext.Session.SetString("user", "marius"); // was Session["user"] = "marius"
string? user = httpContext.Session.GetString("user");
httpContext.Session.SetInt32("count", 3);

Almacenar un objeto significa serializarlo tú mismo (por ejemplo con System.Text.Json) y llamar a SetString. No hay una session de objetos automática como tenía Framework. La guía de migración de session vale la pena leerla si dependías del bloqueo de session.

Verifica: establece un valor en una solicitud, léelo de vuelta en la siguiente; confirma que sobrevive entre solicitudes con la misma cookie de session.

Cuando una reescritura es demasiado grande: los adaptadores System.Web

Si HttpContext está entretejido a través de bibliotecas de clases a las que también llama una aplicación Framework aún no migrada, reescribir cada firma de una vez no es viable. Microsoft entrega los adaptadores System.Web exactamente para esto. Reimplementan la forma de System.Web.HttpContext sobre el contexto de ASP.NET Core, así que una biblioteca puede apuntar a netstandard2.0 y servir a ambos runtimes.

Los paquetes que verás:

En la aplicación ASP.NET Core lo activas:

// .NET 11, ASP.NET Core 11, C# 14
builder.Services.AddSystemWebAdapters();
// ...
app.UseSystemWebAdapters();

Una biblioteca que recibía System.Web.HttpContext sigue compilando después de que cambies la referencia a System.Web por el paquete adaptador. Para convertir entre las dos representaciones dentro de una solicitud usas las conversiones en caché, lo que te permite reescribir puntos de llamada concretos de forma incremental:

// .NET 11, ASP.NET Core 11, C# 14
// Microsoft.AspNetCore.Http.HttpContext -> System.Web.HttpContext
System.Web.HttpContext legacy = coreContext.AsSystemWeb();
// System.Web.HttpContext -> Microsoft.AspNetCore.Http.HttpContext
HttpContext core = legacy.AsAspNetCore();

Los adaptadores no son gratis. Agregan sobrecarga frente a las APIs nativas, no todos los miembros están soportados, y dos comportamientos necesitan activarse porque ASP.NET Core no los provee por defecto: un stream de solicitud con búsqueda y completamente almacenado en buffer (PreBufferRequestStream) y una respuesta almacenada en buffer (BufferResponseStream). Si una biblioteca lee el cuerpo dos veces o depende de Response.End(), actívalos en los endpoints relevantes:

// .NET 11, ASP.NET Core 11, C# 14
app.MapDefaultControllerRoute()
   .PreBufferRequestStream()
   .BufferResponseStream();

Verificación

Después de la migración, recorre esta lista:

Reversión

Esto es una migración de código, no una migración de datos, así que la reversión es un git revert de la rama. Lo único a vigilar es el formato del estado de session: la session de ASP.NET Core no es compatible a nivel de cable con la session de ASP.NET Framework, así que si volteaste el tráfico de producción y los usuarios tienen sesiones vivas, una reversión descarta esas sesiones y fuerza un nuevo inicio de sesión. Drena o acepta eso. Nada más aquí es de un solo sentido.

Trampas que vale la pena conocer antes de empezar

Si haces esto como parte de un movimiento de framework más amplio, esto encaja dentro de la mayor migración de .NET Framework 4.8 a .NET 11, y probablemente también estés reemplazando el modelo de hosting en el mismo paso cuando migres de IWebHostBuilder a WebApplication.CreateBuilder. Para los endpoints nuevos escritos durante la migración, vale la pena sopesar los trade-offs de APIs mínimas frente a controladores antes de portar la forma del controlador antiguo tal cual.

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver