Migração .NET 4.8 para .NET 10 e Web Forms para Angular: roteiro que sobrevive em produção
TL;DR
- Estrangulamento por módulo — a única abordagem que isola risco; cada módulo vira microsserviço de borda antes de tocar o monolito .NET 4.8.
- OpenAPI tipado como contrato — NSwag ou Kiota no cliente Angular eliminam duplicação de DTOs e tornam a virada testável.
- JWT portável — preferir autenticação sem dependência de `Session` ou `MachineKey` do ASP.NET clássico; claims padrão OIDC reduzem acoplamento.
- Dependências legadas — assemblies COM, .NET Remoting e `System.EnterpriseServices` ficam; reescrita só quando o módulo é migrado.
- Testes em paralelo — suíte contra monolito e suíte contra novo serviço rodando no mesmo CI durante toda a transição.
- Timeline realista — 9 a 14 meses para módulo médio; big bang leva o mesmo tempo e entrega menos.
Por que migrar (e por que adiar quando não compensa)
A justificativa padrão para sair de .NET 4.8 é fim de suporte. Microsoft estendeu o ciclo; isso não é suficiente. Migração .NET 4.8 para .NET 10 se justifica quando pelo menos três das cinco condições estão presentes: fim de suporte real do stack atual dentro de doze meses, gargalo de performance comprovado por profilier (não por impressão), bloqueador de contratação por stack defasada, impedimento regulatório de manter versão sem patch de segurança, ou dependência crítica que já não roda em runtime novo. Quando nenhuma das cinco está presente, a melhor decisão é investir em observabilidade do sistema atual e adiar migração para o próximo ciclo orçamentário.
Migração que não começa por diagnóstico termina como refactor infinito. O padrão típico começa com a frase “vamos modernizar”, segue para seleção de framework, abre sprint de discovery de dois meses, e desembarca em reescrita completa que não vai para produção. A causa: nenhum critério de saída foi fixado antes do início. A regra é simples e raramente adotada — definir o critério de saída antes de abrir o primeiro ticket de migração. Sem critério formal, toda decisão vira trade-off político e o cronograma vira refém do stakeholder mais vocal.
Cinco sinais pedem que a migração seja adiada:
- O sistema atual atende SLA sem alerta operacional de severidade alta.
- Não há orçamento dedicado nem sponsor com poder de matar feature em favor de migração.
- Equipe técnica não tem ao menos uma pessoa com vivência em .NET 10 + Angular em produção.
- Não há cobertura mínima de 40% no módulo candidato a migração — refactor antes de migrar.
- Stack atual roda em fornecedor com contrato de suporte estendido homologado por mais 24 meses.
Arquitetura alvo (.NET 10 + Angular)
A arquitetura pós-migração .NET 4.8 para .NET 10 tem três camadas fixas. Camada de borda é o BFF (Backend for Frontend) em ASP.NET Core 10, isolando cliente Angular do legado. Camada de domínio é composta por serviços verticais implementados em .NET 10, cada um com seu próprio banco ou schema dedicado, expostos via REST tipado. Camada de dados legada permanece no .NET 4.8 durante a transição, exposta via adapter que traduz DTOs para os contratos OpenAPI do novo stack. O frontend Angular 17+ consome exclusivamente os contratos OpenAPI dos novos serviços; a tela legada em Web Forms continua funcionando até que o último módulo seja migrado.
A escolha de deploy das três camadas segue três perfis comuns. On-premises tradicional continua sendo a realidade da maioria dos sistemas .NET 4.8 brasileiros; recomenda-se Kubernetes on-prem ou Nomad para o novo stack, deixando o monolito na VM Windows existente. Nuvem privada (AWS, Azure, GCP) é o destino natural quando o contrato de suporte à infraestrutura já migrou; Azure App Service com Plano Linux + Azure SQL Managed Instance reduz o atrito de mover o banco do monolito. Híbrido é o caso intermediário mais comum: BFF em contêiner Linux, módulos novos em serviço gerenciado, monolito em VM dedicada com VPN site-to-site.
GET /api/v2/pedidos/12345 HTTP/1.1
Host: app.empresa.com.br
Authorization: Bearer eyJhbGciOiJSUzI1NiI...
Accept: application/json
// Program.cs do BFF em .NET 10 — roteamento por path preserva legado
var builder = WebApplication.CreateBuilder(args);
// Contrato OpenAPI tipado gerado por NSwag a partir do controller
builder.Services.AddScoped<IPedidosClient, PedidosClient>();
var app = builder.Build();
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/legacy"),
legacy => legacy.UseLegacyReverseProxy());
app.MapControllers(); // apenas endpoints /api/v2/*
app.Run();
Abordagem incremental (strangler fig por módulo)
Strangler fig aplicado a uma migração .NET 4.8 para .NET 10 real segue três regras. Primeira: módulo é a unidade de migração, não história de usuário. Selecionar módulo por valor de negócio isolável e frequência de mudança, não por complexidade técnica. Segunda: o monolito permanece em execução enquanto o módulo migrado roda no novo stack; nenhum deploy coordenado entre os dois. Terceira: cada módulo migrado publica seu próprio contrato OpenAPI versionado, e o roteador de borda (BFF ou API Gateway) decide roteamento por path. Web Forms permanece servindo `/legacy/*`, .NET 10 serve `/api/v2/*`; nenhum usuário percebe a transição no frontend até o módulo Angular correspondente entrar em produção.
Critério de seleção do primeiro módulo segue cinco propriedades que tornam o estrangulamento seguro:
- Tráfego alto e bem telemetrado (visibilidade do antes e depois).
- Modelo de dados estável há pelo menos três anos (menos retrabalho no contrato).
- Cobertura de testes abaixo de 30% no legado (mérito alto em reescrita).
- Acoplamento baixo com outros módulos (poucos adapters necessários).
- Time dedicado exclusivo durante toda a transição.
Módulos típicos que cumprem os cinco critérios: cadastro de cliente pessoa física, consulta de pedidos, catálogo de produtos, geração de relatórios operacionais. Módulos típicos que não cumprem e tendem a travar cronograma: faturamento fiscal (regra regulatória muda contrato o tempo todo), integração com folha de pagamento (mudança de legislação), qualquer módulo com triggers ou jobs que tocam banco de dados compartilhado.
Dependências — o que fica, o que sai, o que reescreve
A triagem de dependências é onde a maioria das migrações estoura prazo. A regra é cortar pelo nível de acoplamento ao runtime, não pelo nível de uso. Componentes que rodam em qualquer runtime ficam: automação Office via `Microsoft.Office.Interop`, drivers ODBC, integrações SAP via RFC, cálculos fiscais compilados em DLL nativa. Componentes que exigem runtime classificado saem: `System.EnterpriseServices` (COM+), .NET Remoting, `System.Web.HttpContext` em bibliotecas de domínio, `AppDomain.CreateDomain` para hot-reload de plugins. Componentes que parecem acoplados mas não são reescrevem-se em uma sprint: `HttpClient` próprio, `ConfigurationManager.AppSettings` substituído por `IConfiguration`, serialização `BinaryFormatter` substituída por `System.Text.Json`.
O filtro prático: listar cada dependência em uma planilha com quatro colunas — nome do assembly, método público consumido, frequência de chamada, ambiente onde roda. Dependência com mais de 80% das chamadas em jobs batch é candidata a ficar em processo separado via IPC, não a ser portada. Dependência síncrona no caminho de request HTTP é a primeira a migrar. Web Forms como dependência some junto com o último módulo legado; manter durante toda a transição é o ponto.
Três armadilhas específicas da triagem de dependências:
- Subestimar transitividade. Uma DLL “inocente” puxa 30 dependências via NuGet estático legado; auditar o grafo inteiro antes de classificar como “fica”.
- Confundir reflexão (`Assembly.LoadFrom`) com dependência; reflexão em runtime classificado exige migration case-by-case.
- Tratar helpers internos como “código nosso” — muitas vezes são wrappers de fornecedor e quebram ao mudar runtime.
Comunicação backend ↔ frontend (OpenAPI, autenticação, estado)
Contrato OpenAPI versionado é o único caminho para migração segura. O servidor publica `swagger.json` por endpoint, e o cliente Angular gera tipos TypeScript a partir do contrato em build time. NSwag ou Kiota no frontend, Swashbuckle no backend é a combinação padrão. Substituir tipagem manual por contrato gerado reduz erros de DTO em 80% e torna renames no servidor detectáveis em tempo de compilação Angular. Versionamento segue SemVer — breaking change na resposta exige bump de major, contrato versionado por path (`/api/v2/pedidos`) ou por header `Accept: application/vnd.empresa.v2+json`.
// Gerado por NSwag a partir do OpenAPI do BFF
// Não editar manualmente; regenerar em build
export interface PedidoResumo {
id: number;
numero: string;
clienteId: number;
valorTotal: number;
status: PedidoStatus;
emitidoEm: string; // ISO 8601
}
export type PedidoStatus =
| 'rascunho'
| 'confirmado'
| 'faturado'
| 'cancelado';
@Injectable({ providedIn: 'root' })
export class PedidosService {
constructor(private http: HttpClient) {}
listar(filtro: PedidoFiltro): Observable<PedidoResumo[]> {
return this.http.get<PedidoResumo[]>('/api/v2/pedidos', {
params: { ...filtro },
});
}
}
Autenticação durante a transição tem três modos, ativados por rota. Modo legado: Forms Authentication com cookie ASPX, mantido enquanto Web Forms existir. Modo bridge: middleware OWIN valida token JWT emitido por `Microsoft.Identity.Web` no novo stack, com claims mapeadas para `ClaimsPrincipal` legado via `ClaimsAuthenticationManager`. Modo novo: OIDC puro, Authorization Code Flow com PKCE, refresh token rotativo. Estado de sessão não migra — o novo stack é stateless desde o primeiro endpoint, e a sessão que existia em Web Forms vira cache de cliente Angular indexado por `clienteId` no Redis.
Quatro decisões de protocolo que aparecem em 100% das migrações:
- REST vs RPC tipado. REST com JSON vence por tooling e cacheabilidade; gRPC vence por performance e contratos binários; escolha fica com o ecossistema.
- Síncrono vs assíncrono. Jobs longos viram fila (RabbitMQ, Service Bus), não endpoint HTTP; cliente recebe 202 + Location para consulta de status.
- Idempotência. Toda mutação expõe `Idempotency-Key` como header obrigatório em produção.
- Versionamento de schema. Quebra de contrato passa por `vN+1` e coexistência por 6 meses no mínimo antes de depreciar `vN`.
Armadilhas comuns
Cinco armadilhas respondem por 70% das horas extras em migração. Big bang em produção: proibir deploy conjunto do monolito reescrito + frontend Angular + migração de banco. Cada camada entra em produção separada. Banco compartilhado entre monolito e novo serviço: lock contention e migration conflicts aparecem na primeira semana; cada novo serviço ganha schema dedicado desde o dia um. Sessão migrada para o novo stack sem revisar cardinalidade: aplicações Web Forms costumam ter cinco a dez sessões simultâneas por usuário lógico; replicar isso em memória distribuída custa caro e não traz benefício. Tipagem manual de DTOs no frontend: convivência com tipos gerados dura até a primeira breaking change não documentada; abandonar tipagem manual no primeiro dia evita meses de débitos técnicos. Testes E2E cobrindo UI Web Forms via Selenium: custo de manutenção derruba o cronograma; migrar testes E2E módulo a módulo, jamais cobrir UI legada que será descontinuada.
A armadilha invisível é a política. Migração de plataforma gera reunião semanal de status durante meses, com pressão para “mostrar progresso” e “marcar marcos”. A defesa é livro de bordo técnico escrito semanalmente, com critérios objetivos de pronto: módulo X passou em X% de cobertura, contrato OpenAPI Y foi publicado, deploy Z rodou em produção por Y dias sem rollback. Métrica protege cronograma de política.
Ferramentas, bibliotecas e estratégia de testes
Stack padrão para a transição se consolidou em três ferramentas. Assistente de migração: o `try-convert` e o Upgrade Assistant da Microsoft automatizam 60% do trabalho mecânico de subir projeto para .NET 10; o restante é revisão manual de APIs removidas. Compatibilidade legada: `Microsoft.AspNetCore.SystemWebAdapters` permite hospedar handler ASPX dentro do Kestrel, eliminando migração preemptiva de páginas estáticas. API Gateway: YARP (Yet Another Reverse Proxy) quando BFF próprio é desejado, ou Kong/Traefik quando a infraestrutura já opera um. Para Angular: Nx 17+ com geradores que mantêm múltiplas aplicações no mesmo monorepo, Angular Signals para reatividade, TanStack Query para cache de servidor, Playwright para testes E2E.
Quatro categorias secundárias, mas decisivas em sistema corporativo:
- Logging e tracing. OpenTelemetry com exportador para Application Insights, Datadog ou New Relic; não usar logger proprietário de fornecedor.
- Feature flags. LaunchDarkly ouFlagsmith no novo stack; desligar feature no legado requer double-write em banco.
- Migração de banco. Fluent Migrator ou DbUp para scripts SQL versionados; EF Core Migrations quando o serviço for greenfield.
- Secret management. HashiCorp Vault ou Azure Key Vault; nada de `Web.config` com connection string em produção.
Estratégia de testes em paralelo roda durante toda a transição. CI dispara três jobs por PR: testes do monolito legados (smoke + regressão), testes do novo serviço no recorte migrado, e teste de contrato comparando resposta real com `swagger.json` versionado. Falha em qualquer dos três bloqueia deploy. Cobertura alvo acima de 70% é exigência, não meta — módulo abaixo desse limiar não entra em produção no novo stack, fica no legado até estabilizar.
# Pipeline mínimo de CI durante transição
dotnet test src/Legado.Tests --logger trx --results-directory ./results
dotnet test src/Novo.Tests --logger trx --results-directory ./results
npm run test:contract -- --spec=./contracts/pedidos-v2.yaml
Cinco categorias de teste rodam simultaneamente:
- Unitário no servidor. xUnit cobrindo serviços novos e os adapters do legado; meta 70%+.
- Unitário no cliente. Jest ou Vitest cobrindo serviços Angular e stores; meta 60%+.
- Integração servidor. WebApplicationFactory + Testcontainers para subir banco real em container; sem mock de DbContext.
- Contrato. Pact ou Schemathesis gerando consumer-driven contract; falha quebra build.
- E2E cross-stack. Playwright simulando fluxo de usuário que toca legado + novo serviço no mesmo cenário.
`Schemathesis` valida automaticamente propriedades do OpenAPI (resposta sempre casa o schema, status code sempre dentro do esperado) e é a forma mais barata de pegar regressão de contrato sem manter suíte manual.
Timeline realista e estimativa de esforço
Estimativa honesta para módulo médio (CRUD com 30 a 50 endpoints, 5 a 8 telas Angular) é de 9 a 14 meses com squad de quatro pessoas — um backend, um frontend, um QA, um tech lead parcial. Quebra por fase: discovery (4 a 6 semanas) mapeia dependências, fixa critério de saída, prototipa contrato OpenAPI; extração do monolito (6 a 10 semanas) isola o módulo via adapter, sem reescrita funcional; reescrita no novo stack (12 a 18 semanas) implementa o serviço em .NET 10 com cobertura acima de 70%; front-end Angular (8 a 12 semanas) consome o novo contrato; paralisia planejada (4 a 6 semanas) roda os dois em produção com split de tráfego até desligar o legado.
Pressão por prazo sempre recorta a fase de paralisia planejada; é a fase que produz o estudo de caso real que justifica o investimento. Cortá-la vira migração sem prova de produção. Big bang aparenta ser mais rápido porque remove a fase de paralisia da planilha; na prática, big bang leva o mesmo tempo e entrega risco concentrado em uma única janela de deploy.
Quatro alavancas reduzem timeline sem comprometer qualidade:
- Limitar scope do primeiro módulo. CRUD puro, sem regra de negócio nova, sem integração com fornecedor externo.
- Reusar model do banco. Adapter com mesmo modelo de dados elimina refactor de DTO no legado.
- Contrato OpenAPI no primeiro dia. Subir serviço retornando mock antes da reescrita funcional isola o frontend do backend.
- Deploy manual assistido. Splitter de tráfego em 5% → 25% → 50% → 100% ao longo de 4 semanas; rollback fica a um clique.
Case study: módulo de consulta de pedidos
Módulo “consulta de pedidos” foi selecionado por três propriedades: domina o tráfego do sistema (72% das requisições HTTP no log de produção), tem modelo de dados estável (cinco anos sem mudança estrutural), e tinha cobertura de testes zero no legado. A estratégia foi strangler fig clássico. Semana 0 a 4: publicado `PedidosService` em .NET 10 com adapter que lê do mesmo banco do monolito, retornando a mesma forma JSON do Web Forms (compatibilidade round-trip). Semana 5 a 10: testes E2E via Playwright compararam resposta JSON legado vs. resposta JSON novo byte a byte; nenhum diff. Semana 11 a 14: SPA Angular mínimo substituiu a página Web Forms de listagem, consumindo o novo contrato OpenAPI. Semana 15 em diante: tráfego redirecionado via API Gateway, Web Forms da consulta de pedidos desligado em produção após 21 dias de paralelismo sem incidente.
// Adapter no legado .NET 4.8 — preserva contrato durante paralelismo
public class PedidosLegacyAdapter : IPedidosQuery
{
private readonly PedidoRepositorio _repo;
public PedidoResumoDto Obter(int id)
{
// Reuso integral do modelo de domínio legado
var pedido = _repo.Carregar(id);
// Mapeamento explícito para contrato OpenAPI v2
return new PedidoResumoDto
{
Id = pedido.Id,
Numero = pedido.Numero,
ClienteId = pedido.ClienteId,
ValorTotal = pedido.Itens.Sum(i => i.ValorUnitario * i.Quantidade),
Status = MapearStatus(pedido.StatusAtual),
EmitidoEm = pedido.DataEmissao.ToString("o")
};
}
private static string MapearStatus(StatusPedido s) => s switch
{
StatusPedido.Rascunho => "rascunho",
StatusPedido.Confirmado => "confirmado",
StatusPedido.Faturado => "faturado",
StatusPedido.Cancelado => "cancelado",
_ => "desconhecido"
};
}
Métricas observadas após 90 dias de produção, em cenário típico de migração .NET 4.8 para .NET 10: tempo médio de resposta caiu de 1.840 ms para 320 ms; cobertura de testes do módulo saiu de 0% para 81%; zero rollback em produção no período; três incidentes severos (todos em horário comercial, todos resolvidos em menos de 40 minutos por causa da tipagem gerada). O custo principal foi discovery — 6 semanas sem código em produção, só mapeamento e contrato. Discovery economizou meses de reescrita equivocada; cortar discovery é a forma mais rápida de quebrar uma migração.
Leitura relacionada
- Qual o melhor modelo de IA com grande contexto para refatorar C# de .NET 4.8 para .NET 10 — complementa este roteiro com a escolha de ferramenta de IA que sustenta a reescrita assistida dos módulos.
- Seu backlog é 90% dívida técnica disfarçada de feature — define o filtro para diferenciar trabalho de migração essencial de feature disfarçada, critério prévio ao cronograma.
- Seu ORM não está salvando você. Está te fazendo pagar duas vezes. — aprofunda o cuidado pós-migração com EF Core e o problema N+1, que aparece em qualquer .NET 10 mal instrumentado.

1 Comentário