Por anos fui a pessoa responsável por manter um aplicativo ASP.NET WebForms vivo. Ele sustentava todo o negócio: representantes de vendas, seus pedidos, comissões, as empresas que representavam. Rodava exclusivamente no Windows, dependia de um conjunto de controles DevExpress de 2011 e imprimia relatórios por um mecanismo que se recusava a compilar em qualquer máquina que não fosse uma específica. Cada mudança parecia desarmar uma bomba. Esta é a história de como o reescrevi como uma API .NET 8 e um front-end React, e por que tirá-lo do Windows acabou sendo a parte que mais mudou tudo.
TL;DR: Reconstruí um sistema legado ASP.NET WebForms como uma API REST .NET 8 e uma React SPA, mantive o banco de dados MySQL existente intacto e migrei tudo para Linux.
Stack: ASP.NET Core 8, EF Core 8, Pomelo MySQL, JWT, QuestPDF, React 18, TypeScript, Vite, Tailwind, TanStack Query, Linux, Docker
Nível: Intermediário
Tempo de leitura: ~9 min

O que realmente me travava eram dois problemas usando um único casaco. O primeiro era o próprio WebForms: o estado vivia no ViewState, cada clique era um round trip completo ao servidor, e a marcação, o C# por trás dela e as chamadas ao banco de dados compartilhavam a mesma página. Não havia onde segurar. O segundo era que tudo estava acorrentado ao Windows: precisava do IIS para rodar, de uma máquina Windows para compilar os relatórios, e até minha máquina de desenvolvimento precisava ser Windows para abri-lo. Portanto, o custo nunca era apenas “parece antigo.” O custo era que eu não conseguia colocar um segundo cliente, um teste automatizado ou um servidor Linux barato em nenhum lugar perto dele sem que o framework todo em formato Windows viesse junto.
O momento em que isso realmente me atingiu foi numa mudança de relatório. Um representante queria uma coluna extra em um PDF de pedido — um trabalho de cinco minutos em qualquer stack saudável. Para entregar, precisei inicializar uma VM Windows que tinha os assemblies exatos do ReportViewer, porque nada mais no mundo conseguia compilar aquele .rdlc. Enquanto estava lá, rolei a tela por arquivos com nomes como Pedido-MICRO4341.cs, fósseis de cada “vou arrumar isso depois” que já tinha me dito. Foi nessa tarde que decidi: o banco de dados poderia ficar, e tudo o que estava em cima dele ia embora.
Mantendo o banco de dados, substituindo tudo acima
A única regra que me dei foi que o schema MySQL não se moveria. Ele é compartilhado com o sistema que ainda serve representantes reais em produção, então uma migração que “melhorasse” uma coluna era uma migração que poderia derrubar o negócio às 9h da manhã. Escolhi o EF Core para a nova camada de dados, mas virei o fluxo de trabalho usual de cabeça para baixo: sem migrations, sem model-first. O banco de dados é a fonte da verdade, e minhas entidades C# se dobram para se adequar ao que o schema legado já diz, idiossincrasias e tudo. O antigo RepDLL tinha uma classe, um DAO e um BO para cada entidade, cada um escrevendo SQL à mão por uma facade MySQL. Tudo isso colapsou em entidades simples e um DbContext.
Até fixei a versão do servidor manualmente em vez de deixar o Pomelo detectá-la automaticamente, porque a detecção automática abre uma conexão extra na inicialização só para perguntar ao MySQL qual versão é, e no hosting compartilhado onde isso roda, cada conexão desperdiçada conta contra um limite fixo. Detalhe pequeno, mas são esses que você aprende do jeito difícil.
Por que .NET Core, e o que o Linux realmente mudou
Essa é a decisão com a qual mais me alegro. O app antigo era .NET Framework 4.8, o que significa Windows e nada mais. O .NET 8 roda o mesmo C# no Linux, e esse único fato reverberou em tudo. Parei de precisar de uma licença Windows Server só para hospedar um aplicativo web. Finalmente pude construir uma imagem Docker e ter um deploy byte a byte idêntico no meu laptop e no servidor, em vez de “funciona na única VM que ninguém tem permissão de tocar.” O runtime em si é dramaticamente mais rápido do que o Framework já foi, e o ASP.NET Core vem com seu próprio servidor web, o Kestrel (sim, batizado de acordo com o falcão), então o app escuta requisições diretamente sem o IIS na frente. O detalhe que ainda me faz sorrir: estou escrevendo este post na mesma máquina Linux onde a API agora roda. A stack antiga não me deixaria desenvolver lá de forma alguma.
Transformando páginas em uma API
Cada .aspx que costumava fazer postback para si mesmo se tornou um controller fino que delega para um service. BOPedido virou PedidoService, registrado uma vez no container, e PedidosController apenas o expõe via HTTP e retorna JSON. Injeção de dependência faz parte do framework agora, então parei de conectar objetos manualmente como o código antigo fazia. A parte com a qual mais tomei cuidado foi a multitenancy, porque um representante nunca pode ver os pedidos de outro.
PerfilContexto lê o claim id_representante do token e filtra todas as queries por ele. O app antigo dependia do estado de sessão para o mesmo trabalho, o que é exatamente o tipo de regra implícita que quebra silenciosamente no momento em que um segundo cliente aparece.
Uma API para web e mobile
O sistema legado tinha um serviço SOAP separado parafusado nele só para que o app mobile pudesse acessar os mesmos dados, o que significava que as mesmas regras de negócio viviam em duas bases de código e divergiam com o tempo. Todo aquele serviço foi embora. O app React e o app mobile agora chamam os mesmos endpoints REST com o mesmo JWT, então há um conjunto de regras e um lugar para mudá-las. A autenticação também ficou stateless: a API entrega um token, o cliente o devolve em cada requisição e não há sessão no servidor para manter.
Relatórios que rodam em qualquer lugar
O mecanismo de relatórios que iniciou toda essa jornada foi a primeira coisa que arranchei fora. Aqueles arquivos .rdlc viraram geradores QuestPDF — classes C# simples como PedidoPdfGenerator que descrevem o layout em código. Rodam em qualquer lugar que o .NET 8 rode, o que agora significa um container Linux, então a mudança de coluna de cinco minutos finalmente é uma mudança de cinco minutos. Exportações de planilhas passam pelo ClosedXML da mesma forma. Sem VM, sem assemblies sagrados, sem cerimônia.
Um front-end que dá vontade de abrir
A grid DevExpress e a dança de postback foram substituídas por um front-end React em TypeScript — uma pasta por domínio, cada uma conversando com um módulo de API tipado. O TanStack Query cuida do cache e do refetching, então ordenar uma coluna não recarrega a página inteira. Os gráficos que costumavam renderizar no servidor viraram componentes Recharts alimentados por um endpoint /graficos simples, então o browser os desenha e o servidor só manda números. O Tailwind substituiu uma pilha de CSS que ninguém queria manter. Nada disso precisa de licença — o que, depois de anos de DevExpress, pareceu estranho de um jeito bom.
Onde chegamos
Mantive o banco de dados MySQL exatamente onde estava e reconstruí tudo acima. O sistema que antes exigia Windows, IIS, um conjunto de controles licenciado e uma VM de relatórios sagrada agora roda como uma API .NET 8 e um app React, em um container, no Linux, servido pelo Kestrel, com uma única API REST alimentando tanto a web quanto o cliente mobile. É mais rápido, consigo testá-lo, posso fazer o deploy do mesmo jeito toda vez, e posso trabalhar nele no mesmo laptop Linux em que estou digitando isso. Os arquivos Pedido-MICRO4341.cs não fizeram a viagem.
O que ainda quero corrigir
Não vou fingir que está pronto. Algumas coisas ainda estão na minha lista, e prefiro nomeá-las do que embelezar o projeto.
- As senhas: o login ainda compara senhas como texto puro, direto do banco de dados legado. O BCrypt.Net já está no projeto me esperando, então hash de verdade é a próxima coisa que devo a esse sistema.
- Os segredos: credenciais de produção e a chave de assinatura JWT ainda ficam no appsettings.json. Elas pertencem a variáveis de ambiente e precisam ser rotacionadas, porque qualquer coisa commitada no git é efetivamente pública para quem tem acesso ao repositório.
- Testes: agora que a lógica fica atrás de services e um boundary HTTP, finalmente tenho a costura que o WebForms nunca me deu. Quero uma suite de integração real antes da próxima grande mudança, não depois dela.
- O pipeline: o container roda, mas o build e o deploy ainda são feitos manualmente. CI é o próximo passo óbvio agora que nada é exclusivo do Windows.
Dúvidas ou feedback? Me encontre no LinkedIn ou GitHub.