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:
É 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.
Curso C# (C Sharp) - Introdução ao ASP.NET Core
Conhecer o cursoUma 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!