.NET ASP.NET Core Performance Cache Redis Web API

Output Cache no .NET: cache de respostas HTTP para APIs de alta performance

Guia completo do Output Cache nativo do ASP.NET Core: políticas por endpoint, vary by parameters, invalidação por tag.

N
Neryx Digital Architects
14 de janeiro de 2026
11 min de leitura
190 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

Output Cache é diferente de IDistributedCache ou IMemoryCache: em vez de cachear dados brutos, ele cacheia a resposta HTTP completa — status code, headers e body. Uma requisição que levaria 200ms e 5 queries no banco retorna em menos de 1ms do cache. No .NET 7+ esse recurso é nativo, sem pacotes externos.

A diferença entre os tipos de cache no .NET

Tipo O que cacheia Granularidade Ideal para
IMemoryCache Objetos .NET Qualquer coisa Dados de domínio, configurações
IDistributedCache Bytes serializados Chave/valor Sessões, dados entre pods
Output Cache Resposta HTTP completa Por endpoint + parâmetros APIs públicas, listagens, catálogos
Response Cache (antigo) Headers Cache-Control Por URL CDN, browser cache

Setup básico

// Program.cs
builder.Services.AddOutputCache(options =>
{
    // Política padrão: 60 segundos, sem vary
    options.AddBasePolicy(builder =>
        builder.Cache().Expire(TimeSpan.FromSeconds(60)));

    // Política nomeada para catálogos (5 minutos)
    options.AddPolicy("catalog", builder =>
        builder
            .Cache()
            .Expire(TimeSpan.FromMinutes(5))
            .SetVaryByQuery("category", "page", "pageSize", "sort")
            .Tag("catalog-tag"));

    // Política para dados de usuário (varia por usuário)
    options.AddPolicy("user-data", builder =>
        builder
            .Cache()
            .Expire(TimeSpan.FromMinutes(2))
            .SetVaryByHeader("Authorization")
            .SetVaryByRouteValue("userId")
            .Tag("user-data-tag"));

    // Política para dados públicos estáticos (longa duração)
    options.AddPolicy("static-content", builder =>
        builder
            .Cache()
            .Expire(TimeSpan.FromHours(1))
            .Tag("static-tag"));

    // Sem cache (para endpoints que nunca devem ser cacheados)
    options.AddPolicy("no-cache", builder =>
        builder.NoCache());
});

app.UseOutputCache(); // DEVE vir antes de UseAuthentication e MapControllers

Aplicando cache nos endpoints

// Minimal APIs
app.MapGet("/api/products", async (
    [FromQuery] string? category,
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 20,
    IProductService service => service) =>
{
    var result = await service.GetAllAsync(category, page, pageSize);
    return Results.Ok(result);
})
.CacheOutput("catalog")  // Aplica política nomeada
.WithTags("Products");

// Vary by route value — cada produto tem seu próprio cache
app.MapGet("/api/products/{id:guid}", async (Guid id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product is null ? Results.NotFound() : Results.Ok(product);
})
.CacheOutput(policy => policy
    .Cache()
    .Expire(TimeSpan.FromMinutes(5))
    .SetVaryByRouteValue("id")
    .Tag($"product-tag"));

// Controllers
[ApiController]
[Route("api/[controller]")]
public class CatalogController : ControllerBase
{
    [HttpGet]
    [OutputCache(PolicyName = "catalog")]
    public async Task<IActionResult> GetAll(
        [FromQuery] string? category,
        [FromQuery] int page = 1)
    {
        // Só executa se não estiver no cache
        var result = await _service.GetAllAsync(category, page);
        return Ok(result);
    }

    [HttpGet("{id:guid}")]
    [OutputCache(Duration = 300, VaryByRouteValueNames = ["id"])]
    public async Task<IActionResult> GetById(Guid id)
    {
        var product = await _service.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }

    // Endpoints de escrita NUNCA devem ser cacheados
    [HttpPost]
    [OutputCache(NoStore = true)]
    public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
    {
        // ...
    }
}

Invalidação por tag: o recurso mais poderoso

// Invalidar cache quando dados mudam
public class ProductService : IProductService
{
    private readonly IProductRepository _repository;
    private readonly IOutputCacheStore _cache;

    public ProductService(IProductRepository repository, IOutputCacheStore cache)
    {
        _repository = repository;
        _cache = cache;
    }

    public async Task<Guid> CreateAsync(CreateProductRequest request, CancellationToken ct)
    {
        var product = await _repository.AddAsync(new Product(request), ct);

        // Invalida TODAS as respostas cacheadas com a tag "catalog-tag"
        // Isso limpa listagens de produtos (que incluem o novo produto)
        await _cache.EvictByTagAsync("catalog-tag", ct);

        return product.Id;
    }

    public async Task UpdateAsync(Guid id, UpdateProductRequest request, CancellationToken ct)
    {
        await _repository.UpdateAsync(id, request, ct);

        // Invalida especificamente este produto E as listagens
        await _cache.EvictByTagAsync($"product:{id}", ct);
        await _cache.EvictByTagAsync("catalog-tag", ct);
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct)
    {
        await _repository.DeleteAsync(id, ct);

        // Invalida produto específico e listagens
        await _cache.EvictByTagAsync($"product:{id}", ct);
        await _cache.EvictByTagAsync("catalog-tag", ct);
    }
}

// Endpoint com tag dinâmica por ID
app.MapGet("/api/products/{id:guid}", async (
    Guid id,
    IProductService service,
    HttpContext context) =>
{
    var product = await service.GetByIdAsync(id);
    return product is null ? Results.NotFound() : Results.Ok(product);
})
.CacheOutput(policy => policy
    .Cache()
    .Expire(TimeSpan.FromMinutes(5))
    .SetVaryByRouteValue("id")
    .Tag(context => [$"product:{context.HttpContext.Request.RouteValues["id"]}"]));

Output Cache com Redis para múltiplos pods

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
// Por padrão, Output Cache usa memória in-process (não funciona bem com múltiplos pods)
// Redis distribui o cache entre todas as instâncias

builder.Services.AddStackExchangeRedisOutputCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "outputcache:"; // Prefixo das chaves no Redis
});

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("catalog", builder =>
        builder
            .Cache()
            .Expire(TimeSpan.FromMinutes(5))
            .SetVaryByQuery("category", "page")
            .Tag("catalog-tag"));
});

Customizando a chave de cache

// Incluir o tenant na chave de cache (multi-tenancy)
public class TenantCacheKeyProvider : IOutputCacheKeyProvider
{
    public ValueTask<string> GetCacheKeyAsync(
        OutputCacheContext context,
        CancellationToken cancellationToken)
    {
        var tenantId = context.HttpContext.Items["TenantId"]?.ToString()
            ?? context.HttpContext.User.FindFirst("tenant_id")?.Value
            ?? "default";

        // Chave = método + path + query + tenantId
        var baseKey = $"{context.HttpContext.Request.Method}:{context.HttpContext.Request.Path}:{tenantId}";

        if (context.HttpContext.Request.QueryString.HasValue)
            baseKey += context.HttpContext.Request.QueryString.Value;

        return ValueTask.FromResult(baseKey);
    }
}

// Registrar
builder.Services.AddSingleton<IOutputCacheKeyProvider, TenantCacheKeyProvider>();

Política condicional: não cachear usuários premium

// Política que pula o cache para usuários com role premium
// (dados personalizados não devem ser compartilhados entre usuários)
options.AddPolicy("conditional-cache", builder =>
    builder.AddPolicy<ConditionalCachePolicy>());

public class ConditionalCachePolicy : IOutputCachePolicy
{
    public ValueTask CacheRequestAsync(
        OutputCacheContext context,
        CancellationToken cancellationToken)
    {
        // Nunca cacheia para usuários premium (dados personalizados)
        if (context.HttpContext.User.HasClaim("plan", "premium"))
        {
            context.EnableOutputCaching = false;
            return ValueTask.CompletedTask;
        }

        // Nunca cacheia POST/PUT/DELETE
        var method = context.HttpContext.Request.Method;
        if (HttpMethods.IsPost(method) ||
            HttpMethods.IsPut(method) ||
            HttpMethods.IsDelete(method))
        {
            context.EnableOutputCaching = false;
            return ValueTask.CompletedTask;
        }

        context.EnableOutputCaching = true;
        context.ResponseExpirationTimeSpan = TimeSpan.FromMinutes(5);
        return ValueTask.CompletedTask;
    }

    public ValueTask ServeFromCacheAsync(
        OutputCacheContext context,
        CancellationToken cancellationToken)
        => ValueTask.CompletedTask;

    public ValueTask ServeResponseAsync(
        OutputCacheContext context,
        CancellationToken cancellationToken)
    {
        // Só armazena respostas 200 OK
        context.AllowCacheLookup = context.HttpContext.Response.StatusCode == 200;
        context.AllowCacheStorage = context.HttpContext.Response.StatusCode == 200;
        return ValueTask.CompletedTask;
    }
}

Monitorando o cache com logs

// appsettings.Development.json — ver hits/misses do cache
{
  "Logging": {
    "LogLevel": {
      "Microsoft.AspNetCore.OutputCaching": "Information"
    }
  }
}

// No log você verá:
// info: OutputCaching — Cache miss for path /api/products
// info: OutputCaching — Cache hit for path /api/products?page=1

Estratégia de tags — referência rápida

// Nomenclatura recomendada para tags
"catalog-tag"              // Todas as listagens do catálogo
$"product:{id}"            // Produto específico
$"user:{userId}"           // Dados de um usuário específico
$"tenant:{tenantId}"       // Todos os dados de um tenant
"static-content"           // Conteúdo que raramente muda
$"category:{categorySlug}" // Por categoria

// Hierarquia de invalidação:
// Ao deletar um produto:
await _cache.EvictByTagAsync($"product:{id}");     // Produto específico
await _cache.EvictByTagAsync("catalog-tag");        // Listagens que o incluem
await _cache.EvictByTagAsync($"category:{catId}"); // Listagem da sua categoria

O que nunca cachear

  • Endpoints de escrita (POST, PUT, DELETE) — use NoStore = true explicitamente
  • Respostas com dados de usuário específico sem vary by user (vaza dados entre usuários)
  • Dados financeiros em tempo real (saldo, cotações)
  • Tokens de autenticação e endpoints de auth
  • Notificações e mensagens que precisam ser entregues imediatamente

Output Cache pode reduzir a carga no banco de dados em 80%+ em APIs com alto tráfego de leitura. Se você está enfrentando problemas de performance em suas APIs .NET, fale com a Neryx.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.