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 = trueexplicitamente - 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.