DEV Community

Cover image for How to use Redis in a .Net solution
🐙 Lukão 🐙
🐙 Lukão 🐙

Posted on

How to use Redis in a .Net solution

Hello ! 👋

It's been a long time since I've last written in here.

Well, since the last time, I've started to work a lot on .Net and infrastructure / DevOps problems. One of the first and most common problem in my daily routine is implementing a way of caching API results using Redis.

Just for didactic purposes, a cache is a temporary data storage place. This data will be used by any kind of application and implementing it can bring a lot of benefits like bandwidth saving, faster response times, fewer database hits and so, but it can do a lot of damage if not carefully implemented.

The code I've created for this post can be found in this GitHub repository. You will find in there an API which uses PokéAPI -- a gigantic pokémon database. Its fair use policy says "Locally cache resources whenever you request them." and this is why it is the perfect API for this project.

The final folder structure is as follows:

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

Pokemon.cs has the properties PokéApi returns. For simplicity, I've added just 3 properties:

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

I've added Microsoft.Extensions.Caching.Redis NuGet package to use Redis. With it, I've created Extension/DistributedCacheExtension.cs to add Redis service:

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

The config options are self-explanatory: connection string and instance name.
Then I've added it to Startup.cs as follows:

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

After configuring Redis, I've developed a service that would help me get and send data for it (and avoid code repetition). Interface:

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

Service:

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

A step-by-step analysis:

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

The first 2 fields, _distributedCache, and _options are linked to Redis configuration. IDistributedCache is the interface responsible to access Redis by dependency injection. DistributedCacheEntryOptions is the class responsible for adding AbsoluteExpirationRelativeToNow and SlidingExpiration, which respectively means the total time a data will be stored and total time it will be stored without being accessed (never greater than absolute time). Prefix is the string used to create a key to access pokémon stored.
Get method:

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

The key is used to find data using GetStringAsync(key) method from the IDistributedCache interface. I return it if it is null (could be an exception or another way of validating it). Else, the resulting string is deserialized.
Set method:

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

The key is used for SetStringAsync() method as the serialized pokémon object. _options (the ones about expiration times) are also used here.

With everything ready, I've created the service which consumes PokéApi. Interface:

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

The service:

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

It is a very simple service: It has a HttpClient and the GetPokemon(int id) method which calls PokéApi and returns a pokémon. It was injected like this:

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

And for the last part, I've created a controller:

[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

And this is it. Don't forget to get the full code here. There's a docker-compose.yml file to help you use Redis.

Discussion (0)