Como usar Redis em uma solução .Net

lukesilva

👨‍💻 Lucas Silva

Posted on November 29, 2021

Como usar Redis em uma solução .Net

Olá!

Faz algum tempo que não escrevo por aqui mas senti saudades disso. Desde que escrevi meu último artigo grande (uma introdução sobre flutter), comecei a trabalhar com .Net e C#. É possível ver algumas semelhanças em ambas linguagens e isso facilitou bastante meu aprendizado nela.

Bem, tenho desenvolvido algumas APIs e trabalhado com alguns problemas envolvendo implantações de aplicações, bem coisa de backend mesmo. Um dos primeiros problemas que precisei resolver foi implementar uma forma de uso de cache utilizando Redis em uma API.

Apenas para esclarecimento, cache é um local de armazenamento de dados temporários que servirão para ajudar aplicações web, mobile etc. Implementar cache na sua solução pode trazer diversos benefícios como economizar banda, reduzir tempo de resposta, realizar menos chamadas ao banco em alguns casos específicos etc. Entretanto também pode causar inúmeros problemas. Um deles é o de os dados não estarem mais atualizados em soluções que esses dados mudam frequentemente.

Criei um código para esse artigo que pode ser encontrado nesse repositório do GitHub. Nele, criei consumi a PokéApi, uma API com dados de quase todos os pokémons lançados até hoje. Segui a política de fair use dela que diz "Locally cache resources whenever you request them." (armazene recursos em cache quando os consultar) e salvei todas as requisições em cache por um tempo limitado.

A estrutura de pastas do projeto ficou da seguinte forma:za

ExemploRedis/
├─ Controllers/
│  ├─ PokemonController.cs
├─ Extensions/
│  ├─ DistributedCacheExtension.cs
├─ Services/
│  ├─ Interfaces/
│  │  ├─ ICacheService.cs
│  │  ├─ IPokemonService.cs
│  ├─ PokemonCacheService.cs
│  ├─ PokemonService.cs
├─ Pokemon.cs
Enter fullscreen mode Exit fullscreen mode

O arquivo Pokemon.cs contém as informações de Pokémons da API. Para a simplicidade do exemplo, adicionei apenas 3 propriedades:

public class Pokemon
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Weight { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Adicionei o pacote Microsoft.Extensions.Caching.Redis NuGet para trabalhar com o Redis. Com ele, criei a extensão Extensions/DistributedCacheExtension.cs para adicionar o serviço à API:

public static IServiceCollection AddDistributedCache(
    this IServiceCollection services,
    IConfiguration configuration)
{
    services.AddDistributedRedisCache(options =>
    {
        options.Configuration = 
            configuration.GetConnectionString("Redis");
        options.InstanceName = 
            configuration["Redis:InstanceName"];
    });
    return services;
}
Enter fullscreen mode Exit fullscreen mode

As options da configuração são autoexplicativas: string de conexão ao serviço e nome da instância.
Adicionei essa extensão no arquivo Startup.cs, no método ConfigureServices, com a seguinte linha de código:

services.AddDistributedCache(Configuration);
Enter fullscreen mode Exit fullscreen mode

Com o Redis configurado, desenvolvi um serviço que me auxiliaria a buscar e adicionar dados a ele visando evitar repetição de código.

public interface ICacheService<T>
{
    Task<T> Get(int id);
    Task Set(T content);
}
Enter fullscreen mode Exit fullscreen mode

O código acima é da interface do serviço. Decidi usar um tipo genérico nela para que ela seja reaproveitada para qualquer objeto que eu precisar. O serviço que a implementa ficou assim:

public class PokemonCacheService : ICacheService<Pokemon>
{
    private readonly IDistributedCache _distributedCache;
    private readonly DistributedCacheEntryOptions _options;
    private const string Prefix = "pokemon_";

    public PokemonCacheService(IDistributedCache distributedCache)
    {
        _distributedCache = distributedCache;
        _options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = 
                TimeSpan.FromSeconds(120),
            SlidingExpiration = TimeSpan.FromSeconds(60)
        };
    }

    public async Task<Pokemon> Get(int id)
    {
        var key = Prefix + id;
        var cache = await _distributedCache.GetStringAsync(key);
        if (cache is null)
        {
            return null;
        }
        var pokemon = JsonConvert.DeserializeObject<Pokemon> 
            (cache);
        return pokemon;
    }

    public async Task Set(Pokemon content)
    {
        var key = Prefix + content.Id;
        var pokemonString = JsonConvert.SerializeObject(content);
        await _distributedCache.SetStringAsync(key, pokemonString, 
            _options);
    }
}
Enter fullscreen mode Exit fullscreen mode

Passando por todos os pontos:

private readonly IDistributedCache _distributedCache;
private readonly DistributedCacheEntryOptions _options;
private const string Prefix = "pokemon_";

public PokemonCacheService(IDistributedCache distributedCache)
{
    _distributedCache = distributedCache;
    _options = new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow =   
            TimeSpan.FromSeconds(120),
        SlidingExpiration = TimeSpan.FromSeconds(60)
    };
}
Enter fullscreen mode Exit fullscreen mode

Os dois primeiros campos, _distributedCache e _options estão relacionados diretamente à configuração do Redis. IDistributedCache é a interface que utilizei para acessar o Redis através de injeção de dependência. Já DistributedCacheEntryOptions é a classe responsável por configurar opções como AbsoluteExpirationRelativeToNow e SlidingExpiration, que referem-se ao tempo total que um dado ficará armazenado e ao tempo que ele pode ficar armazenado sem ser consultado (nunca maior que o tempo absoluto), respectivamente. Prefix refere-se ao prefixo que utilizo para armazenar os pokemons. Utilizo ele + o id do pokemon para armazenar os dados com chaves únicas -- o que facilita a consulta posterior.

Sobre o método Get:

public async Task<Pokemon> Get(int id)
{
    var key = Prefix + id;
    var cache = await _distributedCache.GetStringAsync(key);
    if (cache is null)
    {
        return null;
    }
    var pokemon = JsonConvert.DeserializeObject<Pokemon>(cache);
    return pokemon;
}
Enter fullscreen mode Exit fullscreen mode

Nele, crio a chave como prefixo + id do pokemon e busco o dado no cache utilizando o método GetStringAsync(key) da interface IDistributedCache. Se ele for nulo, retorno nulo (ou poderia notificar um erro ou lançar uma exceção). Caso o contrário, deserializo a string obtida em pokemon e retorno o valor.

Sobre o método Set:

public async Task Set(Pokemon content)
{
    var key = Prefix + content.Id;
    var pokemonString = JsonConvert.SerializeObject(content);
    await _distributedCache.SetStringAsync(key, pokemonString, 
        _options);
}
Enter fullscreen mode Exit fullscreen mode

Nele, crio a chave da mesma forma, com o prefixo + o id do pokemon informado, então serializo o objeto em string e armazeno no Redis utilizando o método SetStringAsync() da interface IDistributedCache. Esse método recebe a chave que será utilizada para armazenamento (a mesma a ser consultada depois), o dado que será armazenado e as opções de armazenamento (lembra que configurei a expiração dos dados no construtor ? Pois bem, é aqui que informo essa configuração).

Com o serviço de cache pronto, desenvolvi o serviço de consulta à PokéApi. Interface:

public interface IPokemonService
{
    Task<Pokemon> GetPokemon(int id);
}
Enter fullscreen mode Exit fullscreen mode

E o serviço que a implementa:

public class PokemonService : IPokemonService
{
    private readonly HttpClient _httpClient;

    public PokemonService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new 
            Uri("https://pokeapi.co/api/v2/");
    }

    public async Task<Pokemon> GetPokemon(int id)
    {
        var response = await 
            _httpClient.GetAsync($"pokemon/{id}");
        var content = await response.Content.ReadAsStringAsync();
        var pokemon = JsonConvert.DeserializeObject<Pokemon> 
            (content);
        return pokemon;
    }
}
Enter fullscreen mode Exit fullscreen mode

O serviço é bem simples: possui um campo HttpClient que foi injetado e o método GetPokemon(int id), que faz a chamada à API e retorna um pokémon. Adicionei o HttpClient ao startup utilizando a seguinte linha de código no arquivo Startup.cs:

    services.AddHttpClient<IPokemonService, PokemonService>();
Enter fullscreen mode Exit fullscreen mode

É importante notar que esse método adiciona tanto o client como o serviço a ser injetado por dependência.

Com tudo pronto, criei um controller para servir esses dados:

[ApiController]
[Route("api/[controller]")]
public class PokemonController : ControllerBase
{
    private readonly IPokemonService _pokemonService;
    private readonly ICacheService<Pokemon> _pokemonCacheService;

    public PokemonController(IPokemonService pokemonService, 
        ICacheService<Pokemon> pokemonCacheService)
    {
        _pokemonService = pokemonService;
        _pokemonCacheService = pokemonCacheService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        Pokemon pokemon = await _pokemonCacheService.Get(id);
        if (pokemon is null)
        {
            pokemon = await _pokemonService.GetPokemon(id);
            await _pokemonCacheService.Set(pokemon);
        }
        return Ok(pokemon);
    }
}
Enter fullscreen mode Exit fullscreen mode

Nele, injeto tanto o PokemonService quanto o CacheService pelo construtor. Como essa Api faz apenas a pesquisa de pokemons, o controller possui apenas 1 método: Get. Ele recebe um id na rota, então verifica se há no cache um pokemon com esse id. Se houver, ele retorna o pokémon. Caso não haja, ele chama o service que consultará a API e então devolve o pokémon.

E esse é o fim do tutorial. Lembre-se de baixar o repositório no GitHub caso queira estudá-lo melhor. Caso queira, deixei um arquivo docker-compose.yml para facilitar o uso do Redis.

💖 💪 🙅 🚩
lukesilva
👨‍💻 Lucas Silva

Posted on November 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related