Implementando cache de memória no ASP.NET Core

A performance de um site pode ser um fator determinante para o seu sucesso ou fracasso. Mesmo com a velocidade das conexões crescendo a cada ano, também cresce o número de usuários que fazem uso apenas de dispositivos móveis, onde essas velocidades são significativamente menores. Então toda a aplicação web que deseja se destacar precisa fornecer um bom desempenho e uma das formas de obter isso é implementando cache de memória.

Noções básicas de cache

O cache pode melhorar significativamente a performance e a escalabilidade de uma aplicação, reduzindo o processo necessário para gerar conteúdo. Desta forma, ele funciona melhor com conteúdos que não são muito alterados e tem um custo alto de geração.

O cache cria uma cópia do conteúdo que pode ser retornada muito mais rápida que a fonte original, como demonstra o diagrama abaixo:

Demostra o fluxo do cache.

É importante que a aplicação seja criada de uma forma que não dependa apenas do cache.

Cache no ASP.NET Core

O ASP.NET Core fornece suporte à alguns tipos de cache. O mais simples é o cache de memória, onde o conteúdo é salvo na memória do servidor. Neste tipo de cache, caso a aplicação faça uso de mais de um servidor (for uma aplicação distribuída), é importante que o balanceamento de carga esteja definido para que as requisições subsequentes de um usuário sempre sejam encaminhadas para o mesmo servidor. Caso contrário, a aplicação não conseguirá aproveitar o cache de memória.

Outro tipo de cache suportado pelo ASP.NET Core é o cache distribuído. Geralmente gerenciado por uma aplicação externa, este tipo de cache pode ser compartilhado entre várias instâncias da aplicação. Tornando o tipo de cache ideal para aplicações distribuídas.

Em ambos os casos, o cache trabalha com o armazenamento de dados seguinte do padrão chave-valor.

Neste artigo focaremos apenas no cache de memória.

Implementando cache de memória no ASP.NET Core

No ASP.NET Core, o cache de memória é representado pela interface IMemoryCache, que já é “injetada” nas dependências da aplicação automaticamente pelo framework. Assim, podemos ter acesso a ele no construtor da classe:

public class ProductsController : Controller
{
    private readonly IMemoryCache _cache;

    public ProductsController(IMemoryCache cache)
    {
        _cache = cache;
    }
}  

Com acesso ao cache, pode ser utilizado o método TryGetValue para tentar obter uma informação:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";
    List<Product> products;
    if (!_cache.TryGetValue<List<Product>>(cacheKey, out products))
    {
        products = await _context.Products.ToListAsync();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10));

        _cache.Set(cacheKey, products, cacheOptions);
    }
    return View(products);
}

Note que se a informação não constar no cache, ela é salva:

_cache.Set(cacheKey, products, cacheOptions);

Durante o salvamento de um dado, pode ser definida algumas opções, no caso acima é definido o “SlidingExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
           .SetSlidingExpiration(TimeSpan.FromSeconds(10));

Que indica o tempo que o cache pode ficar inativo, antes de ser removido. É importante prestar atenção neste ponto, pois caso o usuário acesse a aplicação dentro deste tempo, ele é renovado, o que pode fazer com que este item nunca seja removido do cache, mostrando para o usuário algo defasado.

Para este caso, uma alternativa é também definir o “AbsoluteExpiration”:

var cacheOptions = new MemoryCacheEntryOptions()
    .SetSlidingExpiration(TimeSpan.FromSeconds(10))
    .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

Que define o tempo máximo que um item pode ser mantido no cache.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

Uma alternativa para o TryGetValue é o uso do GetOrCreate:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Que tentará obter o item do cache e caso não exista, será adicionado nele.

Por fim, ainda pode ser utilizado o método Get que apenas retorna o item do cache, ou nulo, se ele não for encontrado:

public IActionResult Index()
{
    var cacheKey = "Products";

    var products = _cache.Get<List<Product>>(cacheKey);

    return View(products);
}

Limite do cache de memória

O ASP.NET Core não controla o tamanho do cache de memória, mesmo se não houver espaço na memória, ele tentará salvar a informação nela. Desta forma, para evitar este tipo de situação, podemos definir um limite para este cache.

Como a implementação padrão de IMemoryCache não implementa isso, caso queira definir este limite, é necessário fornecer uma implementação:

public class CustomMemoryCache 
{
    public MemoryCache Cache { get; set; }
    public CustomMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 2048
        });
    }
}

Ela pode ser adicionada nas dependências da aplicação:

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

E obtidas no construtor das classes:

public class ProductsController : Controller
{
    private readonly MemoryCache _cache;

    public ProductsController(CustomMemoryCache cache)
    {
        _cache = cache;
    }
}  

Entretanto, como o ASP.NET Core não faz o controle do tamanho do cache, ao definir um, sempre que um item for salvo nele, é necessário definir o tamanho deste item:

public async Task<IActionResult> Index()
{
    var cacheKey = "Products";

    var products = await _cache.GetOrCreateAsync<List<Product>>(cacheKey, async entry => {

        entry.SlidingExpiration = TimeSpan.FromSeconds(10);
        entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(30);
        entry.Size = 10;
        return await _context.Products.ToListAsync();
    });

    return View(products);
}

Este tamanho é arbitrário e não define uma unidade de medida, pode representar o tamanho do item em bytes, o número de caracteres de uma string, a quantidade de itens de uma coleção, etc. Por isso é importante que a aplicação utilize apenas uma unidade de medida. Ao atingir o limite, o cache não salvará mais nenhuma informação.

Conclusão

A implementação de cache de memória no ASP.NET Core é um processo simples que pode fornecer muitos benefícios. Desta forma, caso a sua aplicação necessite exibir muitos dados, que sofram poucas alternações é altamente recomendado que ela implemente cache.

Você pode ver a aplicação apresentada neste artigo no meu Github!

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200