← ASP.NET Core: aplicações, contratos e operação
06 / 10 · 60 MIN

HTTP de saída, cache e resiliência

Controla o ciclo de vida dos clientes HTTP, o tempo total e a partilha de respostas em cache sem presumir unicidade de efeitos ou frescura.

Cliente e handler têm vidas diferentes

CreateClient devolve um novo HttpClient, mas a factory pode reutilizar um handler com o seu pool de ligações. Libertar um cliente criado pela factory não destrói imediatamente o handler partilhado. Esta separação permite reutilizar recursos sem exigir uma única instância de cliente guardada por toda a aplicação.

Um typed client capturado por um singleton pode sobreviver à renovação pretendida dos handlers e dificultar a reação a mudanças DNS. Um singleton pode receber IHttpClientFactory e pedir clientes nomeados quando necessários. Outra estratégia documentada usa ligações com PooledConnectionLifetime configurado; não basta aumentar o timeout de um cliente antigo.

O scope do handler não é o pedido recebido

A factory cria um scope de DI próprio por handler. Esse scope é diferente do scope do pedido HTTP recebido e pode durar mais do que ele. Um serviço scoped injetado no handler não deve ser interpretado como a identidade atual de cada pedido que passa pela aplicação.

Guardar um utilizador ou token em estado do handler reutilizado pode fazer esse valor atravessar pedidos. Mantém os dados específicos da operação numa fronteira que os forneça para o envio correspondente, sem os conservar como estado partilhado. Nomear um cliente por serviço remoto não cria automaticamente isolamento de identidade por utilizador.

O prazo inclui todas as tentativas

O handler de resiliência distingue timeout total e timeout por tentativa. O total limita a execução incluindo repetições; o de tentativa limita uma chamada individual. Backoff e novas tentativas consomem o tempo observado pelo chamador. Três tentativas de dez segundos não cabem necessariamente num prazo global de vinte e cinco.

No exemplo, o cliente tem oito segundos de limite total, dois por tentativa e no máximo uma repetição. São escolhas ilustrativas que ainda precisam de caber no prazo da operação completa, com margem para o restante trabalho. Circuit breaker e controlo de concorrência tratam outras condições; não substituem a definição desse orçamento.

Uma repetição pode repetir o efeito

O handler standard pode repetir todos os métodos se a política não for alterada. Um POST que cria uma expedição pode ter sido confirmado no servidor antes de a resposta se perder. Timeout do cliente não prova rollback remoto. Mais tentativas podem aumentar efeitos, sem aumentar a certeza sobre qual tentativa os produziu.

DisableFor permite selecionar métodos sem retry e DisableForUnsafeHttpMethods desativa-o para o conjunto documentado de métodos não seguros. Seguro e idempotente são conceitos diferentes: a decisão deve considerar o contrato real. Perante resultado incerto, usa a identidade original e a política de consulta ou reconciliação disponível antes de assumir que uma nova operação é aceitável.

Disponibilizar e ativar output cache

AddOutputCache e UseOutputCache disponibilizam o mecanismo. O armazenamento ainda precisa de uma política base ou ativação nos endpoints, como CacheOutput. Na política predefinida, só respostas 200 a GET ou HEAD são guardadas, e ficam excluídos pedidos autenticados e respostas com Set-Cookie. Estas condições não devem ser atribuídas a qualquer política personalizada sem leitura.

O exemplo usa um endpoint público anónimo e valores fixos. Se a aplicação tiver autenticação e autorização, o middleware de cache deve respeitar a ordem documentada depois dessas etapas. Uma resposta em cache não deve contornar a decisão de acesso nem tornar públicos dados de uma identidade anterior.

Chaves, validade e invalidação

Uma entrada só deve ser partilhada quando as diferenças entre pedidos não alteram a representação armazenada. Se preço e etiqueta variam por currency e culture, omitir currency numa chave personalizada pode servir EUR a um pedido USD. Um TTL curto limita a duração do erro, mas não torna essa partilha correta.

A etiqueta catalog permite selecionar entradas para EvictByTagAsync quando os dados relevantes mudam. A alteração da base e a invalidação não se tornam automaticamente uma transação conjunta; há que observar falhas entre elas. O locking de preenchimento reduz stampedes, mas não invalida entradas nem garante frescura por si.

Duas leituras do exemplo

A primeira leitura é da configuração HTTP: supplier é um cliente nomeado com limites e política de retry explícitos. O domínio .example é ilustrativo e nenhum endpoint o chama neste programa. Esta configuração não demonstra conectividade, autenticação do fornecedor ou ausência de efeitos duplicados.

A segunda leitura segue /public-price: EUR/pt produz 100 e uma etiqueta portuguesa; USD/en produz 110 e uma etiqueta inglesa. A chave distingue ambos os parâmetros e a validade é trinta segundos. Os valores são fixos para ensino, não preços reais. As avaliações pedem escolher o resultado e diagnosticar mudanças de política, sem executar pedidos ou código.

// Program.cs: ASP.NET Core 10.0 Web SDK, net10.0, ImplicitUsings enabled.
// Microsoft.Extensions.Http.Resilience 10.x available.
// The named client is configured for illustration; no endpoint calls it here.
// Price values and labels are a fixed public teaching model.
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Http.Resilience;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient("supplier", client =>
    client.BaseAddress = new Uri("https://supplier.example/"))
    .AddStandardResilienceHandler(options =>
    {
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(8);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
        options.Retry.MaxRetryAttempts = 1;
        options.Retry.Delay = TimeSpan.FromMilliseconds(200);
        options.Retry.DisableForUnsafeHttpMethods();
    });
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/public-price", (string currency, string culture) =>
{
    if (currency is not ("EUR" or "USD") || culture is not ("pt" or "en"))
        return Results.BadRequest();
    var amount = currency == "EUR" ? 100 : 110;
    var label = culture == "pt" ? "Preço de exemplo" : "Example price";
    return Results.Ok(new { currency, culture, amount, label });
}).CacheOutput(policy => policy
    .SetVaryByQuery("currency", "culture")
    .Expire(TimeSpan.FromSeconds(30))
    .Tag("catalog"));
app.Run();
NA PRÁTICA

O modelo produz preços distintos para EUR e USD e etiquetas distintas para pt e en. A chave mantém ambas as dimensões. O cliente supplier tem um limite total de oito segundos e um limite de dois por tentativa; não é chamado pelo endpoint e a configuração não demonstra conectividade.

Armadilhas comuns

Capturar um typed client indefinidamente; guardar identidade em handlers reutilizados; repetir POST após timeout sem conhecer o efeito; confundir timeout de tentativa com prazo global; omitir moeda na chave; tratar locking como invalidação.

Tópicos relacionados: Persistência e concorrência · Autenticação e autorização · Saúde, observabilidade e diagnóstico

Leva esta ideia contigo

Reutiliza infraestrutura sem partilhar indevidamente identidade. Limita o conjunto de tentativas, confirma a semântica dos efeitos e só partilha uma entrada de cache entre pedidos com representações equivalentes.

Criar conta

Referência: Factory clients and handler lifetimes · ASP.NET Core 10.0 / .NET 10 / C# 14