devbackend.tech

Back-end .NET e modernização de sistemas legados

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

  1. Strangler Fig com YARP: o ASP.NET Core na frente do legado
  2. Testes de caracterização
  3. Minimal APIs com resultados tipados
  4. Integrações resilientes com HttpClientFactory
  5. Chamando um LLM a partir de um serviço .NET

1. Strangler Fig com YARP: o ASP.NET Core na frente do legado

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.

Pontos de atenção

2. Testes de caracterização

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.

3. Minimal APIs com resultados tipados

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);

4. Integrações resilientes com HttpClientFactory

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.

5. Chamando um LLM a partir de um serviço .NET

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

Boas práticas


Sobre

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