Partilhar via


Middleware de cache de saída no ASP.NET Core

Por Tom Dykstra

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Esse artigo explica como configurar o middleware de cache de saída em um aplicativo ASP.NET Core. Para obter uma introdução ao cache de saída, confira Cache de saída.

O middleware de cache de saída pode ser usado em todos os tipos de aplicativos ASP.NET Core: API mínima, API Web com controladores, MVC e Páginas Razor. Exemplos de código são fornecidos para APIs mínimas e APIs baseadas em controlador. Os exemplos de API baseados em controlador mostram como usar atributos para configurar o cache. Esses atributos também podem ser usados ​​em aplicativos MVC e Razor Pages.

Os exemplos de código referem-se a uma classe Gravatar que gera uma imagem e fornece uma data e hora do tipo "gerado em". A classe é definida e usada apenas no aplicativo de exemplo. Sua finalidade é facilitar a visualização quando a saída armazenada em cache está sendo usada. Para obter mais informações, confira Como baixar um exemplo e Diretivas de pré-processador no código de exemplo.

Adicionar o middleware ao aplicativo

Adicione o middleware de cache de saída à coleção de serviços chamando AddOutputCache.

Adicione o middleware ao pipeline de processamento de solicitação chamando UseOutputCache.

Por exemplo:

builder.Services.AddOutputCache();
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseOutputCache();
app.UseAuthorization();

Chamar AddOutputCache e UseOutputCache não inicia o comportamento de cache, ele disponibiliza o cache. Para fazer as respostas do cache do aplicativo, o cache deve ser configurado conforme mostrado nas seções a seguir.

Observação

  • Em aplicativos que usam middleware CORS, UseOutputCache deve ser chamado após UseCors.
  • Em aplicativos de páginas Razor e aplicativos com controladores, UseOutputCache deve ser chamado após UseRouting.

Configurar um ponto de extremidade ou página

Para aplicativos da API mínima, configure um ponto de extremidade para fazer cache chamando CacheOutput ou aplicando o atributo [OutputCache], conforme mostrado nos exemplos a seguir:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

Para aplicativos com controladores, aplique o atributo [OutputCache] ao método de ação conforme mostrado aqui:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Para aplicativos de Páginas do Razor, aplique o atributo à classe de página Razor.

Configurar vários pontos de extremidade ou páginas

Crie políticas ao chamar AddOutputCache para especificar a configuração de cache que se aplica a vários pontos de extremidade. Uma política pode ser selecionada para pontos de extremidade específicos, enquanto uma política base fornece configuração de cache padrão para uma coleção de pontos de extremidade.

O código realçado a seguir configura o cache para todos os pontos de extremidade do aplicativo, com tempo de expiração de 10 segundos. Se um tempo de expiração não for especificado, o padrão será de um minuto.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

O código realçado a seguir cria duas políticas, cada uma especificando um tempo de expiração diferente. Os pontos de extremidade selecionados podem usar a expiração de 20 segundos e outros podem usar a expiração de 30 segundos.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Você pode selecionar uma política para um ponto de extremidade ao chamar o método CacheOutput ou usar o atributo [OutputCache].

Em um aplicativo de API mínimo, o código a seguir configura um endpoint com expiração de 20 segundos e outro com expiração de 30 segundos:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

Para aplicativos com controladores, aplique o atributo [OutputCache] ao método de ação para selecionar uma política:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Para aplicativos de páginas Razor, aplique o atributo à classe de página Razor.

Política de cache de saída padrão

Por padrão, o cache de saída segue estas regras:

  • Somente as respostas HTTP 200 são armazenadas em cache.
  • Somente as solicitações HTTP GET ou HEAD são armazenadas em cache.
  • As respostas que definem cookies não são armazenadas em cache.
  • As respostas a solicitações autenticadas não são armazenadas em cache.

O código a seguir aplica todas as regras de cache padrão a todos os pontos de extremidade de um aplicativo:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Substituir a política padrão

O código a seguir mostra como substituir as regras padrão. As linhas realçadas no código de política personalizada a seguir permitem o armazenamento em cache para métodos HTTP POST e respostas HTTP 301:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

Para usar essa política personalizada, crie uma política nomeada:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

E selecione a política nomeada para um ponto de extremidade. O código a seguir seleciona a política personalizada para um ponto de extremidade em um aplicativo de API mínimo:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

O código a seguir faz o mesmo para uma ação do controlador:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Substituição de política padrão alternativa

Como alternativa, use injeção de dependência (DI) para inicializar uma instância, com as alterações a seguir na classe de política personalizada:

  • Um construtor público em vez de um construtor privado.
  • Elimine a propriedade Instance na classe de política personalizada.

Por exemplo:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

O restante da classe é o mesmo mostrado anteriormente. Adicione a política personalizada, conforme mostrado no exemplo a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

O código anterior usa DI para criar a instância da classe de política personalizada. Todos os argumentos públicos no construtor são resolvidos.

Ao usar uma política personalizada como política base, não chame OutputCache() (sem argumentos) nem use o atributo [OutputCache] em qualquer ponto de extremidade ao qual a política base deva se aplicar. Chamar OutputCache() ou usar o atributo adiciona a política padrão ao endpoint.

Especificar a chave de cache

Por padrão, todas as partes do URL são incluídas como a chave para uma entrada de cache, ou seja, esquema, host, porta, caminho e cadeia de caracteres de consulta. No entanto, você pode querer controlar explicitamente a chave de cache. Por exemplo, suponha que você tenha um ponto de extremidade que retorne uma resposta exclusiva apenas para cada valor exclusivo da cadeia de caracteres de consulta culture. A variação em outras partes do URL, como outras cadeias de caracteres de consulta, não deve resultar em entradas de cache diferentes. Você pode especificar tais regras em uma política, conforme mostrado no código realçado a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Você pode então selecionar a política VaryByQuery para um ponto de extremidade: Em um aplicativo de API mínimo, o código a seguir seleciona a política VaryByQuery para um ponto de extremidade que retorna uma resposta exclusiva apenas para cada valor exclusivo da cadeia de caracteres de consulta culture:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

O código a seguir faz o mesmo para uma ação do controlador:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Aqui estão algumas das opções para controlar a chave de cache:

  • SetVaryByQuery - Especifique um ou mais nomes de cadeia de caracteres de consulta para adicionar à chave de cache.

  • SetVaryByHeader- Especifique um ou mais cabeçalhos HTTP para adicionar à chave de cache.

  • VaryByValue - Especifique um valor para adicionar à chave de cache. O exemplo a seguir usa um valor que indica se o tempo atual do servidor em segundos é par ou ímpar. Uma nova resposta é gerada apenas quando o número de segundos passa de ímpar para par ou de par para ímpar.

    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => builder
            .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
            .Tag("tag-blog"));
        options.AddBasePolicy(builder => builder.Tag("tag-all"));
        options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
        options.AddPolicy("NoCache", builder => builder.NoCache());
        options.AddPolicy("NoLock", builder => builder.SetLocking(false));
        options.AddPolicy("VaryByValue", builder => 
            builder.VaryByValue((context) =>
                new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    });
    

Use OutputCacheOptions.UseCaseSensitivePaths para especificar que a parte do caminho da chave diferencia maiúsculas de minúsculas. O padrão não diferencia maiúsculas de minúsculas.

Para mais opções, confira a classe OutputCachePolicyBuilder.

Revalidação de cache

A revalidação de cache significa que o servidor pode retornar um código de status HTTP 304 Not Modified em vez do corpo da resposta completo. Esse código de status informa ao cliente que a resposta à solicitação permanece inalterada em relação ao que o cliente recebeu anteriormente.

O código a seguir ilustra o uso de um cabeçalho Etag para habilitar a revalidação de cache. Se o cliente enviar um cabeçalho If-None-Match com o valor etag de uma resposta anterior e a entrada de cache for recente, o servidor retornará 304 Not Modified em vez da resposta completa. Veja como definir o valor etag em uma política, em um aplicativo API Minimal:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

E veja como definir o valor da etag em uma API baseada em controlador:

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
    public async Task GetAsync()
    {
        var etag = $"\"{Guid.NewGuid():n}\"";
        HttpContext.Response.Headers.ETag = etag;
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Outra maneira de fazer a revalidação de cache é comparar a data de criação da entrada de cache com a data solicitada pelo cliente. Quando o cabeçalho de solicitação If-Modified-Since é fornecido, o cache de saída retorna 304 se a entrada em cache for mais antiga e não tiver expirado.

A revalidação de cache é automática em resposta a esses cabeçalhos enviados pelo cliente. Nenhuma configuração especial é necessária no servidor para habilitar esse comportamento, além de habilitar o cache de saída.

Usar tags para remover entradas de cache

Você pode usar tags para identificar um grupo de pontos de extremidade e remover todas as entradas de cache do grupo. Por exemplo, o código de API mínimo a seguir cria um par de endpoints cujos URLs começam com "blog" e os marca como "tag-blog":

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

O código a seguir mostra como atribuir marcas a um ponto de extremidade em uma API baseada em controlador:

[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Uma maneira alternativa de atribuir marcas para pontos de extremidade com rotas que começam com blog é definir uma política base que se aplique a todos os pontos de extremidade com essa rota. O código a seguir mostra como fazer isso:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Outra alternativa para aplicativos de API mínimos é chamar MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

Nos exemplos de atribuição de tags anteriores, ambos os pontos de extremidade são identificados pela tag tag-blog. Você pode remover as entradas de cache para esses pontos de extremidade com uma única instrução que faz referência a essa tag:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

Com esse código, uma solicitação HTTP POST enviada para https://localhost:<port>/purge/tag-blog remove as entradas de cache desses pontos de extremidade.

Você pode querer uma maneira de remover todas as entradas de cache para todos os pontos de extremidade. Para fazer isso, crie uma política base para todos os pontos de extremidade como o código a seguir faz:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Essa política básica permite que você use a tag "tag-all" para remover tudo no cache.

Desabilitar o bloqueio de recursos

Por padrão, o bloqueio de recursos é habilitado para mitigar o risco de estouro de cache e rebanho trovejante. Para obter mais informações, confira Cache de Saída.

Para desabilitar o bloqueio de recursos, chame SetLocking(false) ao criar uma política, conforme mostrado no exemplo a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

O exemplo a seguir seleciona a política sem bloqueio para um ponto de extremidade em um aplicativo de API mínimo:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

Em uma API baseada em controlador, use o atributo para selecionar a política:

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Limites

As propriedades a seguir OutputCacheOptions permitem configurar limites que se aplicam a todos os pontos de extremidade:

  • SizeLimit - Tamanho máximo de armazenamento em cache. Quando esse limite é atingido, nenhuma nova resposta é armazenada em cache até que as entradas mais antigas sejam removidas. O valor padrão é de 100 MB.
  • MaximumBodySize – se o corpo da resposta exceder esse limite, ele não será armazenado em cache. O valor padrão é de 64 MB.
  • DefaultExpirationTimeSpan - A duração do tempo de expiração que se aplica quando não especificada por uma política. O valor padrão é de 60 segundos.

Armazenamento em cache

IOutputCacheStore é usado para armazenamento. Por padrão, é usado com MemoryCache. As respostas armazenadas em cache são armazenadas em processo, portanto, cada servidor tem um cache separado que é perdido sempre que o processo do servidor é reiniciado.

Cache Redis

Uma alternativa é usar o cache Redis. O cache Redis fornece consistência entre nós de servidor por meio de um cache compartilhado que sobrevive a processos individuais do servidor. Para usar o Redis como cache de saída:

  • Instale o pacote NuGet Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

  • Chame builder.Services.AddStackExchangeRedisOutputCache (não AddStackExchangeRedisCache) e forneça uma cadeia de conexão que aponte para um servidor Redis.

    Por exemplo:

    builder.Services.AddStackExchangeRedisOutputCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("MyRedisConStr");
        options.InstanceName = "SampleInstance";
    });
    
    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => 
            builder.Expire(TimeSpan.FromSeconds(10)));
    });
    
    • options.Configuration – uma cadeia de conexão para um servidor Redis local ou para uma oferta hospedada, como o Cache do Azure para Redis. Por exemplo, <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False para o cache do Azure para Redis.
    • options.InstanceName – opcional, especifica uma partição lógica para o cache.

    As opções de configuração são idênticas às opções de cache distribuído baseadas em Redis.

Não recomendamos o uso de IDistributedCache com cache de saída. IDistributedCache não possui recursos atômicos, necessários para marcação. Recomendamos que você use o suporte interno para o Redis ou crie implementações personalizadas do IOutputCacheStore usando dependências diretas no mecanismo de armazenamento subjacente.

Confira também

Esse artigo explica como configurar o middleware de cache de saída em um aplicativo ASP.NET Core. Para obter uma introdução ao cache de saída, confira Cache de saída.

O middleware de cache de saída pode ser usado em todos os tipos de aplicativos ASP.NET Core: API mínima, API Web com controladores, MVC e Páginas Razor. O aplicativo de exemplo é uma API Mínima, mas todos os recursos de cache ilustrados por ele também têm suporte nos outros tipos de aplicativo.

Adicionar o middleware ao aplicativo

Adicione o middleware de cache de saída à coleção de serviços chamando AddOutputCache.

Adicione o middleware ao pipeline de processamento de solicitação chamando UseOutputCache.

Observação

  • Em aplicativos que usam middleware CORS, UseOutputCache deve ser chamado após UseCors.
  • Em aplicativos de páginas Razor e aplicativos com controladores, UseOutputCache deve ser chamado após UseRouting.
  • Chamar AddOutputCache e UseOutputCache não inicia o comportamento de cache, ele disponibiliza o cache. O cache de dados de resposta deve ser configurado conforme mostrado nas seções a seguir.

Configurar um ponto de extremidade ou página

Para aplicativos da API mínima, configure um ponto de extremidade para fazer cache chamando CacheOutput ou aplicando o atributo [OutputCache], conforme mostrado nos exemplos a seguir:

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

Para aplicativos com controladores, aplique o atributo [OutputCache] ao método de ação. Para aplicativos de páginas Razor, aplique o atributo à classe de página Razor.

Configurar vários pontos de extremidade ou páginas

Crie políticas ao chamar AddOutputCache para especificar a configuração de cache que se aplica a vários pontos de extremidade. Uma política pode ser selecionada para pontos de extremidade específicos, enquanto uma política base fornece configuração de cache padrão para uma coleção de pontos de extremidade.

O código realçado a seguir configura o cache para todos os pontos de extremidade do aplicativo, com tempo de expiração de 10 segundos. Se um tempo de expiração não for especificado, o padrão será de um minuto.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

O código realçado a seguir cria duas políticas, cada uma especificando um tempo de expiração diferente. Os pontos de extremidade selecionados podem usar a expiração de 20 segundos e outros podem usar a expiração de 30 segundos.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

Você pode selecionar uma política para um ponto de extremidade ao chamar o método CacheOutput ou usar o atributo [OutputCache]:

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

Para aplicativos com controladores, aplique o atributo [OutputCache] ao método de ação. Para aplicativos de páginas Razor, aplique o atributo à classe de página Razor.

Política de cache de saída padrão

Por padrão, o cache de saída segue estas regras:

  • Somente as respostas HTTP 200 são armazenadas em cache.
  • Somente as solicitações HTTP GET ou HEAD são armazenadas em cache.
  • As respostas que definem cookies não são armazenadas em cache.
  • As respostas a solicitações autenticadas não são armazenadas em cache.

O código a seguir aplica todas as regras de cache padrão a todos os pontos de extremidade de um aplicativo:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

Substituir a política padrão

O código a seguir mostra como substituir as regras padrão. As linhas realçadas no código de política personalizada a seguir permitem o armazenamento em cache para métodos HTTP POST e respostas HTTP 301:

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeResponseAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        var response = context.HttpContext.Response;

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

Para usar essa política personalizada, crie uma política nomeada:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

E selecione a política nomeada para um ponto de extremidade:

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

Substituição de política padrão alternativa

Como alternativa, use injeção de dependência (DI) para inicializar uma instância, com as alterações a seguir na classe de política personalizada:

  • Um construtor público em vez de um construtor privado.
  • Elimine a propriedade Instance na classe de política personalizada.

Por exemplo:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

O restante da classe é o mesmo mostrado anteriormente. Adicione a política personalizada, conforme mostrado no exemplo a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

O código anterior usa DI para criar a instância da classe de política personalizada. Todos os argumentos públicos no construtor são resolvidos.

Ao usar uma política personalizada como política base, não chame OutputCache() (sem argumentos) nenhum ponto de extremidade ao qual a política base deva ser aplicada. Chamar OutputCache() adiciona a política padrão ao ponto de extremidade.

Especificar a chave de cache

Por padrão, todas as partes do URL são incluídas como a chave para uma entrada de cache, ou seja, esquema, host, porta, caminho e cadeia de caracteres de consulta. No entanto, você pode querer controlar explicitamente a chave de cache. Por exemplo, suponha que você tenha um ponto de extremidade que retorne uma resposta exclusiva apenas para cada valor exclusivo da cadeia de caracteres de consulta culture. A variação em outras partes do URL, como outras cadeias de caracteres de consulta, não deve resultar em entradas de cache diferentes. Você pode especificar tais regras em uma política, conforme mostrado no código realçado a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Você pode então selecionar a política VaryByQuery para um ponto de extremidade:

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

Aqui estão algumas das opções para controlar a chave de cache:

  • SetVaryByQuery - Especifique um ou mais nomes de cadeia de caracteres de consulta para adicionar à chave de cache.

  • SetVaryByHeader- Especifique um ou mais cabeçalhos HTTP para adicionar à chave de cache.

  • VaryByValue - Especifique um valor para adicionar à chave de cache. O exemplo a seguir usa um valor que indica se o tempo atual do servidor em segundos é par ou ímpar. Uma nova resposta é gerada apenas quando o número de segundos passa de ímpar para par ou de par para ímpar.

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

Use OutputCacheOptions.UseCaseSensitivePaths para especificar que a parte do caminho da chave diferencia maiúsculas de minúsculas. O padrão não diferencia maiúsculas de minúsculas.

Para mais opções, confira a classe OutputCachePolicyBuilder.

Revalidação de cache

A revalidação de cache significa que o servidor pode retornar um código de status HTTP 304 Not Modified em vez do corpo da resposta completo. Esse código de status informa ao cliente que a resposta à solicitação permanece inalterada em relação ao que o cliente recebeu anteriormente.

O código a seguir ilustra o uso de um cabeçalho Etag para habilitar a revalidação de cache. Se o cliente enviar um cabeçalho If-None-Match com o valor etag de uma resposta anterior e a entrada de cache for recente, o servidor retornará 304 Não Modificado em vez da resposta completa:

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

Outra maneira de fazer a revalidação de cache é comparar a data de criação da entrada de cache com a data solicitada pelo cliente. Quando o cabeçalho de solicitação If-Modified-Since é fornecido, o cache de saída retorna 304 se a entrada em cache for mais antiga e não tiver expirado.

A revalidação de cache é automática em resposta a esses cabeçalhos enviados pelo cliente. Nenhuma configuração especial é necessária no servidor para habilitar esse comportamento, além de habilitar o cache de saída.

Usar tags para remover entradas de cache

Você pode usar tags para identificar um grupo de pontos de extremidade e remover todas as entradas de cache do grupo. Por exemplo, o código a seguir cria um par de pontos de extremidade cujos URLs começam com "blog" e tags como "tag-blog":

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

Uma maneira alternativa de atribuir tags para o mesmo par de pontos de extremidade é definir uma política de base que se aplique aos pontos de extremidade que começam com blog:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Outra alternativa é ligar para MapGroup:

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

Nos exemplos de atribuição de tags anteriores, ambos os pontos de extremidade são identificados pela tag tag-blog. Você pode remover as entradas de cache para esses pontos de extremidade com uma única instrução que faz referência a essa tag:

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

Com esse código, uma solicitação HTTP POST enviada para https://localhost:<port>/purge/tag-blog removerá as entradas de cache desses pontos de extremidade.

Você pode querer uma maneira de remover todas as entradas de cache para todos os pontos de extremidade. Para fazer isso, crie uma política base para todos os pontos de extremidade como o código a seguir faz:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

Essa política básica permite que você use a tag "tag-all" para remover tudo no cache.

Desabilitar o bloqueio de recursos

Por padrão, o bloqueio de recursos é habilitado para mitigar o risco de estouro de cache e rebanho trovejante. Para obter mais informações, confira Cache de Saída.

Para desabilitar o bloqueio de recursos, chame SetLocking(false) ao criar uma política, conforme mostrado no exemplo a seguir:

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

O exemplo a seguir seleciona a política sem bloqueio para um ponto de extremidade:

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

limites

As propriedades a seguir OutputCacheOptions permitem configurar limites que se aplicam a todos os pontos de extremidade:

  • SizeLimit - Tamanho máximo de armazenamento em cache. Quando esse limite for atingido, nenhuma nova resposta será armazenada em cache até que as entradas mais antigas sejam removidas. O valor padrão é de 100 MB.
  • MaximumBodySize - Se o corpo da resposta exceder esse limite, ele não será armazenado em cache. O valor padrão é de 64 MB.
  • DefaultExpirationTimeSpan - A duração do tempo de expiração que se aplica quando não especificada por uma política. O valor padrão é de 60 segundos.

Armazenamento em cache

IOutputCacheStore é usado para armazenamento. Por padrão, é usado com MemoryCache. Não recomendamos o uso de IDistributedCache com cache de saída. IDistributedCache não possui recursos atômicos, necessários para marcação. Recomendamos que você crie implementações IOutputCacheStore personalizadas usando dependências diretas no mecanismo de armazenamento subjacente, como o Redis. Ou use o suporte interno para o cache Redis no .NET 8..

Confira também