devbackend.tech
Práticas e exemplos em C# para levar aplicações WebForms, MVC e bancos legados para ASP.NET Core, sem parar a operação.
Sumário
Reescrever um sistema legado inteiro antes de colocar algo em produção é a forma mais comum de uma migração fracassar. O padrão Strangler Fig inverte a lógica: uma nova aplicação ASP.NET Core passa a receber todo o tráfego, responde às rotas que já foram migradas e encaminha o restante para o sistema antigo.
Com o YARP (Yet Another Reverse Proxy), isso cabe em poucas linhas.
Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapControllers(); // rotas já migradas respondem aqui
app.MapReverseProxy(); // todo o resto segue para o sistema legado
app.Run();
appsettings.json
{
"ReverseProxy": {
"Routes": {
"legado": {
"ClusterId": "legado",
"Match": { "Path": "{**catch-all}" }
}
},
"Clusters": {
"legado": {
"Destinations": {
"webforms": { "Address": "https://legado.interno/" }
}
}
}
}
}
A rota {**catch-all} tem a menor precedência no roteamento do ASP.NET Core. Quando um controller novo atende a um caminho, ele vence; quando não atende, a requisição vai para o legado. Migrar uma tela passa a ser: implementar o endpoint novo, publicar e observar.
Em sistemas legados, a especificação real é o próprio comportamento em produção. Antes de reescrever uma regra de negócio, capture entradas e saídas reais do sistema antigo e transforme cada caso em um teste. O novo código só substitui o antigo quando reproduz esses resultados.
CalculoTarifaCaracterizacaoTests.cs
public class CalculoTarifaCaracterizacaoTests
{
// Cada arquivo JSON contém a entrada e a saída registradas no sistema legado.
public static IEnumerable<object[]> Casos =>
Directory.GetFiles("Casos", "*.json")
.Select(arquivo => new object[] { Path.GetFileName(arquivo) });
[Theory]
[MemberData(nameof(Casos))]
public void Novo_calculo_reproduz_o_resultado_do_legado(string arquivo)
{
var caso = CasoRegistrado.Carregar(Path.Combine("Casos", arquivo));
var resultado = new CalculadoraTarifa().Calcular(caso.Entrada);
Assert.Equal(caso.SaidaLegado.Total, resultado.Total);
Assert.Equal(caso.SaidaLegado.Itens.Count, resultado.Itens.Count);
}
}
Regra prática: quando o novo código diverge do legado, a divergência precisa ser explicada antes de ser corrigida. Às vezes o "bug" do sistema antigo é uma regra de negócio que ninguém documentou.
Minimal APIs reduzem a cerimônia sem abrir mão de contratos claros. Com TypedResults e Results<...>, os possíveis retornos do endpoint ficam explícitos na assinatura, o que melhora a leitura, os testes e a documentação OpenAPI.
PedidosEndpoints.cs
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
public static class PedidosEndpoints
{
public static void MapPedidos(this IEndpointRouteBuilder app)
{
var grupo = app.MapGroup("/api/pedidos");
grupo.MapGet("/{id:int}", ObterPorId);
}
private static async Task<Results<Ok<PedidoDto>, NotFound>> ObterPorId(
int id, AppDbContext db, CancellationToken ct)
{
var pedido = await db.Pedidos
.AsNoTracking()
.Where(p => p.Id == id)
.Select(p => new PedidoDto(p.Id, p.Cliente, p.Total))
.FirstOrDefaultAsync(ct);
return pedido is null
? TypedResults.NotFound()
: TypedResults.Ok(pedido);
}
}
public sealed record PedidoDto(int Id, string Cliente, decimal Total);
AsNoTracking() em consultas somente leitura evita o custo do change tracker.Select busca apenas as colunas necessárias e não expõe a entidade.CancellationToken interrompe a consulta quando o cliente desiste da requisição.Sistemas legados costumam depender de ERPs e serviços internos que oscilam. O pacote Microsoft.Extensions.Http.Resilience adiciona um pipeline padrão com retry com backoff, circuit breaker e timeouts.
Program.cs
builder.Services
.AddHttpClient<ErpClient>(client =>
{
client.BaseAddress = new Uri("https://erp.interno/");
})
.AddStandardResilienceHandler();
ErpClient.cs
using System.Net.Http.Json;
public sealed class ErpClient(HttpClient http)
{
public Task<SaldoDto?> ObterSaldoAsync(string codigo, CancellationToken ct) =>
http.GetFromJsonAsync<SaldoDto>($"saldos/{Uri.EscapeDataString(codigo)}", ct);
}
public sealed record SaldoDto(string Codigo, decimal Saldo, DateTime AtualizadoEm);
Cuidado com retry: repetir chamadas só é seguro para operações idempotentes. Para operações que gravam dados, use uma chave de idempotência ou desative o retry naquele cliente.
Integrar um modelo de linguagem a um sistema corporativo não exige um framework novo. Um HttpClient tipado, configuração via IOptions e o mesmo pipeline de resiliência resolvem a maioria dos casos, como resumir documentos, classificar chamados ou extrair dados de textos livres.
ClaudeClient.cs
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
public sealed class ClaudeOptions
{
public string ApiKey { get; init; } = string.Empty;
public string Model { get; init; } = string.Empty;
}
public sealed class ClaudeClient(HttpClient http, IOptions<ClaudeOptions> options)
{
public async Task<string> ResumirAsync(string texto, CancellationToken ct)
{
var config = options.Value;
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages")
{
Content = JsonContent.Create(new
{
model = config.Model,
max_tokens = 1024,
messages = new[]
{
new { role = "user", content = $"Resuma em até 5 tópicos:\n\n{texto}" }
}
})
};
request.Headers.Add("x-api-key", config.ApiKey);
request.Headers.Add("anthropic-version", "2023-06-01");
using var response = await http.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(ct);
using var json = await JsonDocument.ParseAsync(stream, cancellationToken: ct);
return json.RootElement
.GetProperty("content")[0]
.GetProperty("text")
.GetString() ?? string.Empty;
}
}
Program.cs
builder.Services.Configure<ClaudeOptions>(builder.Configuration.GetSection("Claude"));
builder.Services.AddHttpClient<ClaudeClient>().AddStandardResilienceHandler();
Edynilson Batista Santos é desenvolvedor .NET sênior, com cerca de 24 anos de experiência em desenvolvimento de software. Atua com modernização de sistemas legados, migração de aplicações WebForms e MVC para .NET moderno e desenvolvimento back-end, com interesse crescente em integração de IA em sistemas corporativos.
© 2026 devbackend.tech