DEV Community

Danilo Itagyba
Danilo Itagyba

Posted on

Blindando Aplicações no .NET 10: Um Guia Completo sobre Polly e Resiliência

Resiliência à Prova de Falhas no .NET 10 com Polly

No desenvolvimento de software moderno, especialmente em arquiteturas de microsserviços e nuvem, a estabilidade não é definida pela ausência de falhas, mas pela capacidade do sistema de resistir e se recuperar delas.

Com a chegada do .NET 10 (e seguindo a evolução desde o .NET 8), a forma de implementar resiliência mudou drasticamente. As antigas Policies síncronas e pesadas deram lugar aos Resilience Pipelines: leves, performáticos e nativos para código assíncrono.

Este artigo explora como utilizar o poder do Polly em conjunto com o Microsoft.Extensions.Http.Resilience para criar aplicações robustas.

1. O Novo Paradigma: Resilience Pipelines

Diferente das versões anteriores do Polly (v7 e inferiores), a nova arquitetura (v8+) foca em:

  • Zero-Allocation (ou quase): Redução drástica no consumo de memória.
  • Pipeline Pattern: Estratégias são encadeadas de forma lógica e fluente.
  • Integração Nativa: Telemetria, Logging e Injeção de Dependência já "saem da caixa".

Instalação

Para projetos .NET modernos, você deve instalar os seguintes pacotes via NuGet:

dotnet add package Polly.Core
dotnet add package Microsoft.Extensions.Http.Resilience

Enter fullscreen mode Exit fullscreen mode

Nota: O pacote Microsoft.Extensions.Http.Resilience é a recomendação oficial da Microsoft para aplicações web e APIs.


2. Estratégias Fundamentais de Resiliência

Abaixo, detalhamos as principais estratégias (strategies) utilizando a sintaxe moderna do ResiliencePipelineBuilder.

A. Retry (Tentativa) com Jitter

A estratégia mais básica. Se falhar, tente novamente. No entanto, o Retry simples é perigoso (pode causar efeito "Thundering Herd"). A solução é o Backoff Exponencial com Jitter (ruído aleatório), que agora é o padrão no Polly moderno.

using Polly;
using Polly.Retry;

ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        // Trata exceções de rede e códigos 5xx
        ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(2),
        BackoffType = DelayBackoffType.Exponential, // Inclui Jitter automaticamente
        OnRetry = static args =>
        {
            Console.WriteLine($"Tentativa {args.AttemptNumber} falhou. Retentando...");
            return ValueTask.CompletedTask;
        }
    })
    .Build();

Enter fullscreen mode Exit fullscreen mode

B. Circuit Breaker (Disjuntor)

Protege o sistema de destino. Se um serviço está fora do ar, continuar tentando só piora a situação. O Circuit Breaker "abre o circuito" após um número de falhas, falhando imediatamente novas requisições para dar tempo ao serviço de se recuperar.

var circuitOptions = new CircuitBreakerStrategyOptions
{
    FailureRatio = 0.5,       // Se 50% das requisições falharem...
    SamplingDuration = TimeSpan.FromSeconds(30), // ...nos últimos 30s...
    MinimumThroughput = 7,    // ...com no mínimo 7 requisições...
    BreakDuration = TimeSpan.FromSeconds(60)     // ...abra o circuito por 1 min.
};

var pipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker(circuitOptions)
    .Build();

Enter fullscreen mode Exit fullscreen mode

C. Fallback (Plano de Contingência)

Quando tudo falha (Retry esgotado, Circuito aberto), o Fallback define o que fazer para não estourar erro na cara do usuário. Pode ser retornar um dado do cache ou um valor padrão.

var pipeline = new ResiliencePipelineBuilder()
    .AddFallback(new FallbackStrategyOptions<UserDto>
    {
        FallbackAction = _ => Outcome.FromResult(new UserDto { Name = "Usuário Padrão" }),
        OnFallback = static _ => 
        {
            Console.WriteLine("Fallback acionado! Retornando dados provisórios.");
            return ValueTask.CompletedTask;
        }
    })
    .Build();

Enter fullscreen mode Exit fullscreen mode

D. Hedging (Especulação)

Útil para baixa latência. O Hedging dispara requisições paralelas (ou com pequeno atraso). A primeira que retornar com sucesso é usada; as outras são canceladas.

Exemplo: Você chama uma API que às vezes demora 5s, mas normalmente leva 100ms. O Hedging dispara uma segunda chamada se a primeira demorar mais que 200ms.

var pipeline = new ResiliencePipelineBuilder()
    .AddHedging(new HedgingStrategyOptions
    {
        MaxHedgedAttempts = 2,
        Delay = TimeSpan.FromMilliseconds(500), // Dispara 2ª tentativa se 1ª demorar 500ms
    })
    .Build();

Enter fullscreen mode Exit fullscreen mode

3. A Implementação "Gold Standard" no .NET 10

Em aplicações reais (.NET Core API, Worker Services, Blazor), você raramente constrói pipelines manualmente. Você utiliza a Injeção de Dependência para decorar o HttpClient.

A Microsoft introduziu o AddStandardResilienceHandler, que aplica uma combinação pré-configurada e otimizada de:

  1. Rate Limiter
  2. Total Timeout
  3. Retry
  4. Circuit Breaker
  5. Attempt Timeout

Configuração no Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

// 1. Registro do HttpClient nomeado
builder.Services.AddHttpClient("CatalogoApi", client =>
{
    client.BaseAddress = new Uri("https://api.catalogo.com");
    client.Timeout = TimeSpan.FromSeconds(30); // Timeout do socket
})
// 2. Adicionando a Resiliência Padrão
.AddStandardResilienceHandler(options => 
{
    // Podemos customizar os padrões se necessário
    options.Retry.MaxRetryAttempts = 5;
    options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(15);
    options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
});

var app = builder.Build();

Enter fullscreen mode Exit fullscreen mode

Consumindo o Serviço Resiliente

Sua classe de serviço não precisa saber que o Polly existe. A resiliência é transparente.

public class CatalogoService(IHttpClientFactory factory, ILogger<CatalogoService> logger)
{
    private readonly HttpClient _client = factory.CreateClient("CatalogoApi");

    public async Task<Produto?> GetProdutoAsync(int id, CancellationToken ct)
    {
        // Se a rede falhar, o Polly intercepta, tenta de novo, 
        // ou abre o circuito automaticamente aqui.
        try 
        {
            return await _client.GetFromJsonAsync<Produto>($"/produtos/{id}", ct);
        }
        catch (Exception ex)
        {
            // Logar apenas se todas as estratégias de resiliência falharem
            logger.LogError(ex, "Não foi possível obter o produto após múltiplas tentativas.");
            throw;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

4. Cenários Avançados: Pipeline Registry

Às vezes, você precisa reutilizar a mesma estratégia de resiliência em vários lugares que não são necessariamente chamadas HTTP (ex: acesso a arquivos, conexões Redis). Para isso, usamos o ResiliencePipelineProvider.

Registro

builder.Services.AddResiliencePipeline("meu-pipeline-banco", builder =>
{
    builder
        .AddRetry(new RetryStrategyOptions { MaxRetryAttempts = 3 })
        .AddTimeout(TimeSpan.FromSeconds(5));
});

Enter fullscreen mode Exit fullscreen mode

Uso Injetado

public class BancoService(ResiliencePipelineProvider<string> pipelineProvider)
{
    public async Task SalvarDadosAsync(string dados, CancellationToken ct)
    {
        // Recupera o pipeline pelo nome
        var pipeline = pipelineProvider.GetPipeline("meu-pipeline-banco");

        // Executa qualquer código arbitrário com resiliência
        await pipeline.ExecuteAsync(async token => 
        {
            await _database.ExecuteCommandAsync(dados, token);
        }, ct);
    }
}

Enter fullscreen mode Exit fullscreen mode

5. Checklist de Melhores Práticas

  1. Idempotência é Chave: Nunca use Retry em métodos que não sejam idempotentes (ex: POST para criar pedido) a menos que sua API suporte verificação de duplicidade. Se a primeira requisição criou o pedido mas a resposta falhou na rede, o Retry pode criar um pedido duplicado.
  2. Timeouts em Camadas:
  3. Attempt Timeout: Tempo para cada tentativa individual (curto).
  4. Total Timeout: Tempo máximo para todo o processo (longo).
  5. Socket Timeout: Configuração do HttpClient.

  6. Circuit Breaker Distribuído: Lembre-se que o Circuit Breaker é local (na memória da instância). Se você tem 10 réplicas da sua API no Kubernetes, cada uma terá seu próprio estado de circuito.

  7. Não trate tudo: Não use Retry para erros de negócio (400 Bad Request, 403 Forbidden). Resiliência é para falhas transitórias (503 Service Unavailable, 408 Request Timeout, falhas de rede).

Conclusão

No .NET 10, a biblioteca Polly deixou de ser apenas um utilitário de terceiros para se tornar parte integrante da stack de rede da Microsoft. Ao utilizar Microsoft.Extensions.Http.Resilience e os novos Pipelines, você garante que sua aplicação seja capaz de suportar as instabilidades inerentes à nuvem com performance máxima e código limpo.

Top comments (0)