Start Debugging

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

Uma migração prática do System.Web.HttpContext do ASP.NET Framework para o HttpContext do ASP.NET Core 11: HttpContext.Current, o mapa de propriedades, Server.MapPath, Session e o shim dos adaptadores System.Web para migrações incrementais.

A única linha que quebra mais migrações de ASP.NET Framework do que qualquer outra é HttpContext.Current. Ela não existe no ASP.NET Core. Não há um contexto ambiental estático para acessar a partir de uma classe arbitrária, o tipo HttpContext é um tipo diferente em um namespace diferente (Microsoft.AspNetCore.Http.HttpContext, não System.Web.HttpContext), e a maioria das propriedades das quais você dependia foram movidas, mudaram de forma ou sumiram. Este artigo mapeia a API antiga para a nova no .NET 11 / ASP.NET Core 11, e depois mostra os dois caminhos reais à frente: uma reescrita limpa para o código que você controla, e os adaptadores oficiais System.Web quando você tem uma pilha de bibliotecas compartilhadas que passam HttpContext de um lado para o outro e não podem ser reescritas de uma só vez.

Para um handler pequeno ou um único controller, a reescrita leva uma hora. Para um monólito onde HttpContext.Current está costurado por uma camada de negócio em um assembly separado, reserve dias e recorra aos adaptadores para que as bibliotecas continuem compilando contra ambos os frameworks enquanto você migra aplicação por aplicação. Nada da semântica HTTP muda; o que muda é como você alcança a requisição, que o ciclo de vida agora está estritamente vinculado à requisição, e que não há afinidade de thread em que se apoiar.

Por que esta migração não é um localizar-e-substituir

System.Web.HttpContext e Microsoft.AspNetCore.Http.HttpContext são objetos genuinamente diferentes, e as lacunas são comportamentais, não apenas cosméticas:

O próprio guia de migração do HttpContext da Microsoft enquadra isso como duas estratégias, e a escolha conduz tudo o que vem abaixo: reescrita completa, ou adaptadores System.Web para uma migração incremental.

O que quebra

ÁreaASP.NET FrameworkASP.NET Core 11Severidade
Contexto ambientalHttpContext.CurrentIHttpContextAccessor (registrar com AddHttpContextAccessor)alta
Ciclo de vida do contextoUtilizável após a requisição às vezesObjectDisposedException após a requisição terminaralta
Segurança de threadsRequisição com afinidade de threadSem afinidade de thread através de awaitalta
Escrever na respostaResponse.Write(s)await Response.WriteAsync(s)média
Ler formulário / corpoRequest.Form, Request.InputStream (sync)await Request.ReadFormAsync(), Request.Body (leitura única)média
Cabeçalhos / cookies de respostaDefinir a qualquer momentoDefinir antes de a resposta começar (ou via OnStarting)média
Caminhos físicosServer.MapPath("~/x")IWebHostEnvironment.ContentRootPath / WebRootPath + Path.Combinemédia
SessionSession["k"], auto-serializada, com lockHttpContext.Session.GetString/SetString, baseada em bytes, sem lockmédia
Codificação HTMLServer.HtmlEncodeSystem.Net.WebUtility.HtmlEncode / HtmlEncoderbaixa
URL da requisiçãoRequest.Url, Request.RawUrlRequest.Scheme/Host/Path/QueryString ou GetDisplayUrl()baixa

Lista de verificação prévia

Passos da migração

Passo 1: Registre o acessador e pare de recorrer ao HttpContext.Current

Substitua o acesso ambiental por injeção explícita. No 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();

Um serviço que antes lia HttpContext.Current agora recebe IHttpContextAccessor:

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

Não armazene accessor.HttpContext em um campo. Leia-o no ponto de uso toda vez, porque o campo capturaria um contexto de uma requisição e o entregaria a outra, ou a nenhuma. Dentro de um controller ou de uma API mínima você já tem HttpContext como propriedade ou parâmetro, então prefira passá-lo explicitamente e pule o acessador por completo.

Verifique: a solução compila sem referências a System.Web nos projetos reescritos, e uma requisição que exercita CurrentUserService retorna o id de usuário esperado.

Passo 2: Traduza as propriedades da requisição

A maioria dos membros de Request foram movidos em vez de sumir. O mapeamento que cobre os casos comuns:

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

Ler o formulário ou o corpo é assíncrono e o corpo é um stream somente-avanço que você pode ler uma vez:

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

Verifique: acesse um endpoint que leia query, formulário e um cabeçalho; confira que os valores batem com o que a aplicação Framework retornava para a mesma requisição.

Passo 3: Traduza a resposta, e respeite quando os cabeçalhos podem ser definidos

Escrever é assíncrono, e os cabeçalhos e cookies devem ser definidos antes de o corpo começar 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);

Se você está em middleware e precisa definir cabeçalhos logo antes de a resposta ser enviada, use o callback em vez de defini-los 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);

Verifique: inspecione os cabeçalhos de resposta com curl -i; confirme que o cabeçalho está presente e que você não recebe uma exceção response has already started sob carga.

Passo 4: Substitua Server.MapPath por IWebHostEnvironment

Server.MapPath("~/App_Data/x.json") não tem equivalente. Injete IWebHostEnvironment e combine os caminhos você mesmo:

// .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 é a raiz do projeto (o antigo ~/), WebRootPath é wwwroot (a antiga raiz de arquivos estáticos). Para a codificação HTML, Server.HtmlEncode vira System.Net.WebUtility.HtmlEncode ou, em DI, um HtmlEncoder injetado.

Verifique: uma requisição que carrega um arquivo resolve o mesmo caminho absoluto que você espera, tanto no Windows quanto no Linux (o Path.Combine o mantém portável).

Passo 5: Mova Session, sabendo que ela se comporta de forma diferente

A session do ASP.NET Core é opcional, baseada em bytes, não é serializada automaticamente e não oferece lock por requisição. Registre-a:

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

Depois troque o indexador pelos 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);

Armazenar um objeto significa serializá-lo você mesmo (por exemplo com System.Text.Json) e chamar SetString. Não há uma session de objetos automática como o Framework tinha. O guia de migração de session vale a leitura se você dependia do lock de session.

Verifique: defina um valor em uma requisição, leia-o de volta na próxima; confirme que ele sobrevive entre requisições com o mesmo cookie de session.

Quando uma reescrita é grande demais: os adaptadores System.Web

Se HttpContext está entrelaçado por bibliotecas de classes que uma aplicação Framework ainda não migrada também chama, reescrever cada assinatura de uma vez não é viável. A Microsoft entrega os adaptadores System.Web exatamente para isso. Eles reimplementam a forma de System.Web.HttpContext sobre o contexto do ASP.NET Core, então uma biblioteca pode mirar netstandard2.0 e servir ambos os runtimes.

Os pacotes que você verá:

Na aplicação ASP.NET Core você opta por ativá-los:

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

Uma biblioteca que recebia System.Web.HttpContext continua compilando depois de você trocar a referência a System.Web pelo pacote adaptador. Para converter entre as duas representações dentro de uma requisição você usa as conversões em cache, o que permite reescrever pontos de chamada específicos 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();

Os adaptadores não são de graça. Eles adicionam overhead frente às APIs nativas, nem todos os membros são suportados, e dois comportamentos precisam ser ativados porque o ASP.NET Core não os fornece por padrão: um stream de requisição com busca e totalmente bufferizado (PreBufferRequestStream) e uma resposta bufferizada (BufferResponseStream). Se uma biblioteca lê o corpo duas vezes ou depende de Response.End(), ative-os nos endpoints relevantes:

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

Verificação

Após a migração, percorra esta lista:

Rollback

Isto é uma migração de código, não uma migração de dados, então o rollback é um git revert da branch. A única coisa a observar é o formato do estado de session: a session do ASP.NET Core não é compatível no nível de fio com a session do ASP.NET Framework, então se você virou o tráfego de produção e os usuários têm sessões vivas, um rollback descarta essas sessões e força um novo login. Drene ou aceite isso. Nada mais aqui é de mão única.

Armadilhas que vale a pena conhecer antes de começar

Se você faz isso como parte de uma migração de framework mais ampla, isto se encaixa dentro da maior migração de .NET Framework 4.8 para .NET 11, e você provavelmente também estará substituindo o modelo de hosting no mesmo passo quando migrar de IWebHostBuilder para WebApplication.CreateBuilder. Para os endpoints novos escritos durante a migração, vale a pena pesar os trade-offs de APIs mínimas versus controllers antes de portar a forma do controller antigo ao pé da letra.

Fontes

Comments

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

< Voltar