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
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; }
}
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;
}
The config options are self-explanatory: connection string and instance name.
Then I've added it to Startup.cs as follows:
services.AddDistributedCache(Configuration);
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);
}
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);
}
}
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)
};
}
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;
}
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);
}
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);
}
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;
}
}
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();
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);
}
}
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.
Top comments (0)