Cache na memória no ASP.NET Core

Por Rick Anderson, John Luo e Steve Smith

O cache pode melhorar significativamente o desempenho e a escalabilidade de um aplicativo, reduzindo o trabalho necessário para gerar conteúdo. O cache funciona melhor com dados que mudam com pouca frequência e são caros de gerar. O cache faz uma cópia dos dados que podem ser retornados muito mais rapidamente do que da fonte. Os aplicativos devem ser escritos e testados para nunca depender de dados armazenados em cache.

ASP.NET Core suporta vários caches diferentes. O cache mais simples é baseado na IMemoryCache interface. IMemoryCache representa um cache armazenado na memória do servidor web. Os aplicativos em execução em uma fazenda de servidores (vários servidores) devem garantir que as sessões sejam persistentes ao usar o cache na memória. As sessões fixas garantem que todas as solicitações de um cliente vão para o mesmo servidor. Por exemplo, um aplicativo Web Azure usa Microsoft ARR (Application Request Routing) para rotear todas as solicitações para o mesmo servidor.

Sessões não persistentes em uma farm de servidores exigem um cache distribuído para evitar problemas de consistência de cache. Para alguns aplicativos, um cache distribuído pode dar suporte a uma expansão mais alta do que um cache na memória. O uso de um cache distribuído descarrega a memória cache para um processo externo.

O cache na memória pode armazenar qualquer objeto. A interface de cache distribuído é limitada a byte[]. Os itens de cache armazenados em cache na memória e distribuídos como pares chave-valor.

Usar System.Runtime.Caching/MemoryCache

System.Runtime.Caching / MemoryCache (pacote NuGet) pode ser usado com:

  • .NET Standard 2.0 ou posterior
  • Qualquer implementação .NET que dá suporte ao .NET Standard 2.0 ou posterior (como o ASP.NET Core 3.1 ou posterior)
  • .NET Framework 4.5 ou posterior

Microsoft. Extensions.Caching.Memory/IMemoryCache (descrito neste artigo) é recomendado em System.Runtime.Caching/MemoryCache porque oferece melhor integração com ASP.NET Core. Por exemplo, IMemoryCache funciona nativamente com a injeção de dependência do ASP.NET Core.

Use System.Runtime.Caching/MemoryCache como uma ponte de compatibilidade ao portar código do ASP.NET 4.x para o ASP.NET Core.

Examinar as diretrizes de cache na memória

As diretrizes a seguir se aplicam ao cache na memória:

  • O código sempre deve ter uma opção de fallback para buscar dados e não depender da disponibilidade de um valor armazenado em cache.

  • O cache usa memória, que é um recurso escasso. Limitar o crescimento do cache:

    • Não insira entrada externa no cache. Por exemplo, o uso de entrada arbitrária fornecida pelo usuário como uma chave de cache não é recomendado porque a entrada pode consumir uma quantidade imprevisível de memória.

    • Use expirações para limitar o crescimento do cache.

    • Use SetSize, Size e SizeLimit para limitar o tamanho do cache. O ASP.NET Core runtime não limita o tamanho do cache com base na pressão de memória. O desenvolvedor é responsável por limitar o tamanho do cache.

Criar uma instância de IMemoryCache

O cache na memória é um serviço a que um aplicativo faz referência usando a injeção de dependência.

Warning

Se o mesmo cache for usado por várias estruturas ou bibliotecas, ele será um cache compartilhado . Se você usar um cache de memória compartilhado da injeção de dependência e também usar SetSize, Sizee SizeLimit para limitar o tamanho do cache, o aplicativo poderá falhar.

Quando um limite de tamanho é definido em um cache, todas as entradas devem especificar um tamanho quando forem adicionadas. Essa abordagem pode levar a problemas porque os desenvolvedores podem não ter controle total sobre o que usa o cache compartilhado.

Para limitar o tamanho do cache com o método SetSize, a propriedade Size ou a propriedade SizeLimit, crie uma instância única de cache. Para obter mais informações e um exemplo, consulte Usar SetSize, Size e SizeLimit para limitar o tamanho do cache.

Solicite a IMemoryCache instância no construtor:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

O código a seguir usa o TryGetValue método para verificar se uma hora está no cache. Caso um horário não esteja armazenado em cache, uma nova entrada será criada e adicionada ao cache com o método Set.

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

No código anterior, a entrada de cache é configurada com uma expiração deslizante de 3 segundos. Se a entrada de cache não for acessada por mais de 3 segundos, a entrada será removida do cache. Cada vez que a entrada de cache é acessada, ela permanece no cache por mais 3 segundos. A CacheKeys classe faz parte do exemplo de download.

A hora atual e a hora armazenada em cache são exibidas:

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

O código a seguir usa o Set método de extensão para armazenar dados em cache por um tempo relativo sem MemoryCacheEntryOptions:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

No código anterior, a entrada de cache é configurada com uma expiração relativa de um dia. A entrada de cache é removida do cache após um dia, mesmo que a entrada seja acessada durante o período de tempo limite.

O código a seguir usa os métodos GetOrCreate e GetOrCreateAsync para armazenar dados em cache.

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

O código a seguir chama o Get método para buscar o tempo armazenado em cache:

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

O código a seguir obtém ou cria um item armazenado em cache com expiração absoluta:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Um conjunto de itens armazenados em cache com apenas uma expiração deslizante corre o risco de nunca expirar. Se o item armazenado em cache for acessado repetidamente dentro do intervalo de expiração deslizante, o item nunca expirará. Combinar uma expiração deslizante com uma expiração absoluta garante que o item expire. A expiração absoluta define um limite superior em quanto tempo o item pode ser armazenado em cache. Ele ainda permite que o item expire anteriormente, se o item não for solicitado dentro do intervalo de expiração deslizante. Se o intervalo de expiração deslizante ou o tempo de expiração absoluto passar, o item será removido do cache.

O código a seguir obtém ou cria um item armazenado em cache com expiração deslizante e absoluta:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

O código anterior garante que os dados não sejam armazenados em cache por mais tempo do que o tempo absoluto.

GetOrCreate, GetOrCreateAsynce Get são métodos de extensão na CacheExtensions classe. Esses métodos estendem a capacidade do IMemoryCache.

Criar opções de entrada de cache de memória (MemoryCacheEntryOptions) para uma entrada

O exemplo a seguir demonstra como criar MemoryCacheEntryOptions para uma entrada. O código conclui as seguintes tarefas:

  • Define a prioridade do cache como CacheItemPriority.NeverRemove.

  • Configura uma função PostEvictionDelegate para ser chamada depois que a entrada for removida do cache. O callback executa em um thread diferente daquele que executa a remoção do item do cache.

public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

Limitar o tamanho do cache com SetSize, Size e SizeLimit

Opcionalmente, uma MemoryCache instância pode especificar e impor um limite de tamanho. O limite de tamanho do cache não tem uma unidade de medida definida porque o cache não tem nenhum mecanismo para medir o tamanho das entradas. Se o limite de tamanho do cache estiver definido, todas as entradas deverão especificar o tamanho. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão da memória. Cabe ao desenvolvedor limitar o tamanho do cache. O tamanho especificado é em unidades que o desenvolvedor escolhe.

Por exemplo:

  • Se o aplicativo Web armazena em cache principalmente cadeias de caracteres, cada tamanho de entrada de cache pode ser o comprimento da cadeia de caracteres.
  • O aplicativo pode especificar o tamanho de todas as entradas como 1 e o limite de tamanho é a contagem de entradas.

Se a SizeLimit propriedade não estiver definida, o cache aumentará sem limite. O runtime do ASP.NET Core não corta o cache quando a memória do sistema está baixa. Os aplicativos devem ser arquitetados para:

  • Limite o crescimento do cache.
  • Chame o método Compact ou Remove quando a memória disponível for limitada.

O código a seguir cria uma instância de tamanho fixo e sem unidade de medida acessível por injeção de dependência:

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

A propriedade SizeLimit não possui unidades. As entradas armazenadas em cache devem especificar o tamanho em todas as unidades que considerarem mais apropriadas quando o limite de tamanho do cache for definido. Todos os usuários de uma instância de cache devem usar o mesmo sistema de unidades. Uma entrada não será armazenada em cache se a soma dos tamanhos de entrada em cache exceder o valor especificado por SizeLimit. Se nenhum limite de tamanho de cache for definido, o tamanho do cache definido na entrada será ignorado.

O código a seguir registra a MyMemoryCache instância com o contêiner de injeção de dependência:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache é criado como um cache de memória independente para componentes que estão cientes desse cache com tamanho limitado e sabem como definir o tamanho da entrada de cache adequadamente.

O tamanho da entrada de cache pode ser definido usando o SetSize método de extensão ou a Size propriedade:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

No código anterior, as duas linhas realçadas obtêm o mesmo resultado de definir o tamanho da entrada de cache. O SetSize método é fornecido para conveniência ao encadear chamadas para new MemoryCacheOptions().

Remover itens de cache com MemoryCache.Compact

O MemoryCache.Compact método tenta remover a porcentagem especificada do cache na seguinte ordem:

  • Todos os itens expirados
  • Itens por prioridade, em que os itens de prioridade mais baixos são removidos primeiro
  • Objetos menos usados recentemente
  • Itens com a expiração absoluta mais antiga
  • Itens com término dinâmico mais antigo

Os itens fixados com prioridade NeverRemovenunca são removidos. O código a seguir remove um item de cache e chama o Compact método para remover 25% de entradas armazenadas em cache:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

Para obter mais informações, consulte a fonte compacta no GitHub.

Remover uma entrada de cache com dependências expiradas

O exemplo a seguir mostra como expirar uma entrada de cache se uma entrada dependente expirar. Um CancellationChangeToken é adicionado ao item em cache. Quando o Cancel método é chamado no CancellationTokenSource objeto, ambas as entradas de cache são removidas:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

Ao usar um CancellationTokenSource objeto, você pode remover várias entradas de cache como um grupo. Com o padrão using no código anterior, as entradas de cache criadas dentro do escopo using herdam gatilhos e configurações de expiração.

Revisar notas sobre caching em memória

As seguintes notas se aplicam ao cache na memória:

  • A expiração não acontece em segundo plano.

    Não há temporizador que verifique ativamente o cache em busca de itens expirados. Qualquer atividade no cache (via Get, TryGetValue, Setou Remove) pode disparar uma verificação em segundo plano para itens expirados. Um temporizador definido no CancellationTokenSource objeto (usando o método CancelAfter) também remove a entrada e aciona uma verificação para itens expirados.

    O exemplo a seguir usa o construtor sobrecarregado CancellationTokenSource(TimeSpan) para o token registrado. Quando esse token é acionado, ele remove a entrada imediatamente e dispara os retornos de chamada de remoção:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • Quando você usa um retorno de chamada para repovoar um item de cache:

    • Várias solicitações podem descobrir que o valor da chave armazenada em cache está vazio porque o retorno de chamada não foi concluído.
    • Essa abordagem pode resultar em vários threads repopulando o item armazenado em cache.
  • Quando uma entrada de cache (a principal) cria outra entrada (a derivada), a derivada copia os tokens de expiração da entrada principal e as configurações de expiração baseadas em tempo. O filho não expira por remoção manual ou atualização da entrada pai.

  • Use a propriedade PostEvictionCallbacks para especificar quais retornos de chamada devem ser disparados depois que uma entrada de cache é removida do cache.

  • Para a maioria dos aplicativos, IMemoryCache está ativado. Por exemplo, chamar AddMvc, AddControllersWithViews, AddRazorPagese AddMvcCore().AddRazorViewEnginemuitos outros Add{Service} métodos no arquivo Program.cs habilita IMemoryCache.

    Para aplicativos que não chamam um dos métodos mencionados Add{Service} , talvez seja necessário chamar o AddMemoryCache método no arquivo Program.cs .

Usar uma atualização de cache em segundo plano

Use um serviço em segundo plano , como a IHostedService interface, para atualizar o cache. O serviço em segundo plano pode recomputar as entradas e atribuí-las ao cache somente depois que elas estiverem prontas.

Exibir ou baixar código de exemplo (como baixar)

Noções básicas de cache

O cache pode melhorar significativamente o desempenho e a escalabilidade de um aplicativo, reduzindo o trabalho necessário para gerar conteúdo. O cache funciona melhor com dados que mudam com pouca frequência e são caros de gerar. O cache faz uma cópia dos dados que podem ser retornados muito mais rapidamente do que da fonte. Os aplicativos devem ser escritos e testados para nunca depender de dados armazenados em cache.

ASP.NET Core suporta vários caches diferentes. O cache mais simples é baseado no IMemoryCache. IMemoryCache representa um cache armazenado na memória do servidor web. Os aplicativos em execução em uma fazenda de servidores (vários servidores) devem garantir que as sessões sejam persistentes ao usar o cache na memória. As sessões fixas garantem que as solicitações subsequentes de um cliente vão para o mesmo servidor. Por exemplo, os aplicativos Web do Azure usam o ARR ( Roteamento de Solicitação de Aplicativo ) para rotear todas as solicitações subsequentes para o mesmo servidor.

Sessões não fixas em um web farm exigem um cache distribuído para evitar problemas de consistência de cache. Para alguns aplicativos, um cache distribuído pode dar suporte a uma expansão mais alta do que um cache na memória. O uso de um cache distribuído descarrega a memória cache para um processo externo.

O cache na memória pode armazenar qualquer objeto. A interface de cache distribuído é limitada a byte[]. Os itens de cache armazenados em cache na memória e distribuídos como pares chave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching / MemoryCache (pacote NuGet) pode ser usado com:

  • .NET Standard 2.0 ou posterior.
  • Qualquer implementação do .NET direcionada ao .NET Standard 2.0 ou posterior. Por exemplo, ASP.NET Core 3.1 ou posterior.
  • .NET Framework 4.5 ou posterior.

Microsoft.Extensions.Caching.Memory/IMemoryCache (descrito neste artigo) é recomendado sobre System.Runtime.Caching/MemoryCache porque é melhor integrado ao ASP.NET Core. Por exemplo, IMemoryCache funciona nativamente com a injeção de dependência do ASP.NET Core.

Use System.Runtime.Caching/MemoryCache como uma ponte de compatibilidade ao portar código do ASP.NET 4.x para o ASP.NET Core.

Diretrizes de cache

  • O código deve sempre ter uma opção de fallback para buscar dados e não depender da disponibilidade de um valor armazenado em cache.
  • O cache usa um recurso escasso, a memória. Limitar o crescimento do cache:
    • Não use entrada externa como chaves de cache.
    • Use expirações para limitar o crescimento do cache.
    • Use SetSize, Size e SizeLimit para limitar o tamanho do cache. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão da memória. Cabe ao desenvolvedor limitar o tamanho do cache.

Usar IMemoryCache

Warning

Usar uma memória cache compartilhada de Injeção de Dependência e chamar SetSize, Size, ou SizeLimit para limitar o tamanho do cache pode fazer com que o aplicativo falhe. Quando um limite de tamanho é definido em um cache, todas as entradas devem especificar um tamanho ao serem adicionadas. Isso pode levar a problemas, pois os desenvolvedores podem não ter controle total sobre o que usa o cache compartilhado. Ao usar SetSize, Size, ou SizeLimit para limitar o cache, crie um singleton de cache. Para obter mais informações e um exemplo, consulte Usar SetSize, Size e SizeLimit para limitar o tamanho do cache. Um cache compartilhado é compartilhado por outras estruturas ou bibliotecas.

O cache na memória é um serviço referenciado de um aplicativo usando a Injeção de Dependência. Solicite a instância IMemoryCache no construtor:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

O seguinte código usa TryGetValue para verificar se um horário está no cache. Se uma hora não for armazenada em cache, uma nova entrada será criada e adicionada ao cache com Set. A CacheKeys classe faz parte do exemplo de download.

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

A hora atual e a hora armazenada em cache são exibidas:

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

O código a seguir usa o Set método de extensão para armazenar dados em cache por um tempo relativo sem criar o MemoryCacheEntryOptions objeto:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

O valor armazenado DateTime em cache permanece no cache enquanto houver solicitações dentro do período de tempo limite.

O código a seguir usa GetOrCreate e GetOrCreateAsync para armazenar dados em cache.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

O código a seguir chama Get para buscar o tempo armazenado em cache:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

O código a seguir obtém ou cria um item armazenado em cache com expiração absoluta:

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Um conjunto de itens armazenados em cache com apenas uma expiração deslizante corre o risco de nunca expirar. Se o item armazenado em cache for acessado repetidamente dentro do intervalo de expiração deslizante, o item nunca expirará. Combine uma expiração deslizante com uma expiração absoluta para garantir que o item expire. A expiração absoluta define um limite superior de quanto tempo o item pode ser armazenado em cache enquanto ainda permite que o item expire mais cedo se não for solicitado dentro do intervalo de expiração deslizante. Se o intervalo de expiração deslizante ou o tempo de expiração absoluto passar, o item será removido do cache.

O código a seguir obtém ou cria um item armazenado em cache com expiração deslizante e absoluta:

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

O código anterior garante que os dados não serão armazenados em cache por mais tempo do que o tempo absoluto.

GetOrCreate, GetOrCreateAsynce Get são métodos de extensão na CacheExtensions classe. Esses métodos estendem a capacidade do IMemoryCache.

MemoryCacheEntryOptions

O exemplo a seguir,

  • Define um tempo de expiração deslizante. As solicitações que acessam esse item armazenado em cache redefinirão o relógio de expiração deslizante.
  • Define a prioridade do cache como CacheItemPriority.NeverRemove.
  • Define um PostEvictionDelegate que será chamado depois que a entrada for removida do cache. O callback é executado em um thread diferente do código que remove o item do cache.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Usar SetSize, Size e SizeLimit para limitar o tamanho do cache

Opcionalmente, uma MemoryCache instância pode especificar e impor um limite de tamanho. O limite de tamanho do cache não tem uma unidade de medida definida porque o cache não tem nenhum mecanismo para medir o tamanho das entradas. Se o limite de tamanho do cache estiver definido, todas as entradas deverão especificar o tamanho. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão da memória. Cabe ao desenvolvedor limitar o tamanho do cache. O tamanho especificado é em unidades que o desenvolvedor escolhe.

Por exemplo:

  • Se o aplicativo Web estivesse armazenando principalmente cadeias de caracteres em cache, cada tamanho de entrada de cache poderia ser o comprimento da cadeia de caracteres.
  • O aplicativo pode especificar o tamanho de todas as entradas como 1 e o limite de tamanho é a contagem de entradas.

Se SizeLimit não estiver definido, o cache crescerá sem limite. O runtime do ASP.NET Core não corta o cache quando a memória do sistema está baixa. Os aplicativos devem ser arquitetados para:

  • Limite o crescimento do cache.
  • Chame Compact ou Remove quando a memória disponível for limitada:

O código a seguir cria um tamanho fixo MemoryCache sem unidade de medida, acessível por injeção de dependência:

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit não tem unidades. As entradas armazenadas em cache devem especificar o tamanho em quaisquer unidades que considerem mais apropriadas se o limite de tamanho do cache tiver sido definido. Todos os usuários de uma instância de cache devem usar o mesmo sistema de unidades. Uma entrada não será armazenada em cache se a soma dos tamanhos de entrada armazenados em cache exceder o valor especificado por SizeLimit. Se nenhum limite de tamanho de cache for definido, o tamanho do cache definido na entrada será ignorado.

O código a seguir MyMemoryCache registra com o contêiner de injeção de dependência.

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache é criado como um cache de memória independente para componentes que estão cientes desse cache de tamanho limitado e sabem como definir o tamanho da entrada de cache adequadamente.

O código a seguir usa MyMemoryCache:

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

O tamanho da entrada de cache pode ser definido por Size ou pelos SetSize métodos de extensão:

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact Tenta remover a porcentagem especificada do cache na seguinte ordem:

  • Todos os itens expirados.
  • Itens por prioridade. Os itens de prioridade mais baixa são removidos primeiro.
  • Objetos usados menos recentemente.
  • Itens com a expiração absoluta mais precoce.
  • Itens com a expiração deslizante mais precoce.

Os itens fixados com prioridade NeverRemove nunca são removidos. O código a seguir remove um item de cache e chama Compact:

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

Para obter mais informações, consulte a fonte compacta no GitHub.

Dependências de cache

O exemplo a seguir mostra como expirar uma entrada de cache se uma entrada dependente expirar. Um CancellationChangeToken é adicionado ao item em cache. Quando Cancel é chamado no CancellationTokenSource, ambas as entradas de cache são removidas.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

O uso de um CancellationTokenSource permite que várias entradas de cache sejam removidas como um grupo. Com o using padrão no código acima, as entradas de cache criadas dentro do using bloco herdarão gatilhos e configurações de expiração.

Observações adicionais

  • A expiração não acontece em segundo plano. Não há temporizador que verifique ativamente o cache em busca de itens expirados. Qualquer atividade no cache (Get, Set, Remove) pode disparar uma verificação em segundo plano para itens expirados. Um temporizador no CancellationTokenSource (CancelAfter) também remove a entrada e aciona uma verificação de itens expirados. O exemplo a seguir usa CancellationTokenSource(TimeSpan) para o token registrado. Quando esse token é acionado, ele remove a entrada imediatamente e dispara os retornos de chamada de remoção:

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • Ao usar um retorno de chamada para repovoar um item de cache:

    • Várias solicitações podem encontrar o valor da chave em cache vazio porque o callback ainda não foi concluído.
    • Isso pode resultar em vários threads preenchendo novamente o item armazenado em cache.
  • Quando uma entrada de cache é usada para criar outra, o filho copia os tokens de expiração da entrada pai e as configurações de expiração baseadas em tempo. O filho não expira por remoção manual ou atualização da entrada pai.

  • Use PostEvictionCallbacks para definir as callbacks que serão disparadas depois que a entrada do cache for removida do cache. No código de exemplo, CancellationTokenSource.Dispose() é chamado para liberar os recursos não gerenciados usados pelo CancellationTokenSource. No entanto, o CancellationTokenSource não é descartado imediatamente porque ainda está sendo usado pela entrada de cache. O CancellationToken é passado para MemoryCacheEntryOptions para criar uma entrada de cache que expira após um determinado tempo. Portanto, Dispose não deve ser chamado até que a entrada de cache seja removida ou expire. O código de exemplo chama o RegisterPostEvictionCallback método para registrar um retorno de chamada que será invocado quando a entrada de cache for removida e descarta o CancellationTokenSource nesse retorno de chamada.

  • Para a maioria dos aplicativos, IMemoryCache está ativado. Por exemplo, chamar AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine, e muitos outros Add{Service} métodos em ConfigureServices, habilita IMemoryCache. Para aplicativos que não estão chamando um dos métodos mencionados anteriormente Add{Service}, pode ser necessário chamar AddMemoryCache em ConfigureServices.

Atualização do cache em segundo plano

Use um serviço em segundo plano , como IHostedService para atualizar o cache. O serviço em segundo plano pode recalcular as entradas e atribuí-las ao cache somente quando estiverem prontas.

Recursos adicionais