.NET Core Redis Cache Manager

Aref Nozar
Hexaworks-Papers
Published in
5 min readMay 11, 2024
.NET Core Redis Cache Manager

Hey there! Let’s talk about managing caching, especially when you’re knee-deep in .NET Core projects with a microservices architecture. Picture this: you’re writing a web application, building microservices left and right, and suddenly you realize you need a cache distributor. Of course I chose redis for it. But alas, you’re faced with the tedious task of writing cache handlers for each service and entity separately. Enter frustration, stage left.

Now, we’re all familiar with DRY (Don’t Repeat Yourself). Yet, here we are, staring down the barrel of repetitive cache code. I decided to write a generic cache service and use it from now on :) . I’ve converted it to a NuGet package so you can obtain it either from the command line or the Nuget package manager:

dotnet add package CacheManager.Redis

CacheManager.Redis

Also, the source code and samples are available in this repository. Don’t forget to give a star if you find it helpful.

Ok let’s get our hand dirty. First i’m going to install theStackExchangeRedis package:

dotnet add package StackExchange.Redis

Creating the Interface

Create an interface named ‘IRedisDistributedCache’:

interface IRedisDitributedCache : IDistributedCache
{
}

As you can see it inherits from ‘IDistributedCache’ from the StackEschangeRedis Nuget package so we can add out custom functionality to it. For now we’ll just use it as a marker. With this approach we can have more than one redis instance in each service (though not necessarily recommended).

Implementing the DistributedCache

Create the ‘RedisDistribudedCache’ class:

internal class RedisDistributedCache : RedisCache, IRedisDitributedCache
{
public RedisDistributedCache(IOptions<RedisCacheOptions> optionsAccessor) : base(optionsAccessor)
{
}
}

We are inheriting from ‘RedisCache’, which we downloaded, and implementing the interface we created above. Note that you should create the constructor and pass options to the base class constructor.

Cache Manager Interface

Now, let’s define the ‘IRedisCacheManager’ interface:

public interface IRedisCache<TEntity> where TEntity : class
{
bool TryGet(string key, out TEntity? response);

Task<TEntity?> TryGetAsync(string key, CancellationToken cancellationToken = default);

void Set(string key, TEntity entity);

Task SetAsync(string key, TEntity entity, CancellationToken token = default);

void Refresh(string key);

Task RefreshAsync(string key, CancellationToken token = default);

void Remove(string key);

Task RemoveAsync(string key, CancellationToken token = default);
}

It’s quite aimilar to the ‘IDitributedCache’ interface but uses a generic type instead of bytes. Now let’s implement the ‘RedisCacheManager’ class:

internal class RedisCacheManager<TEntity> : 
IRedisCacheManager<TEntity> where TEntity : class
{
private readonly IRedisDitributedCache _cache;

public RedisCacheManager(IRedisDitributedCache cache)
=> _cache = cache;
}

Here we inject ‘IRedisDistributedCache’ into our class constructor so we can use it in our class.

Implementing Cache Manager

For the ‘TryGet’ method, we need to get the data ,convert it to the generic entity, and returnit back. If there is no data or it coludnt be converted we will return ‘false’ and ‘null’ as the response:

public bool TryGet(string key, out TEntity? response)
{
response = default;
var cachedResponse = _cache.Get(key);
if (cachedResponse == null) return false;

var responseString = Encoding.UTF8.GetString(cachedResponse);
try
{
response = JsonSerializer.Deserialize<TEntity>(responseString);
}
catch (Exception)
{
return false;
}
return true;
}

i’m using ‘System.Text.Json’ for serialization; you can use NewtonSoft or any other serializer you prefer.

For ‘TryGetAsync’ , we only need to return ‘null’ if there is no data or the conversion is not successful:

public async Task<TEntity?> TryGetAsync(string key, CancellationToken cancellationToken = default)
{
var cachedResponse = await _cache.GetAsync(key, cancellationToken);
if (cachedResponse is null) return null;

var responseString = Encoding.UTF8.GetString(cachedResponse);
try
{
return JsonSerializer.Deserialize<TEntity>(responseString);
}
catch (Exception)
{
return null;
}
}

Actually, we can move the try-catch to another static method named ‘TryDeserialize’:

public static bool TryDeserialize<TType>(this string value, out TType? response)
{
response = default;
try
{
response = JsonSerializer.Deserialize<TType>(value);
return true;
}
catch (Exception e)
{
return false;
}
}

Our methods should now look like this:

public bool TryGet(string key, out TEntity? response)
{
response = default;
var cachedResponse = _cache.Get(key);
if (cachedResponse == null) return false;

var responseString = Encoding.UTF8.GetString(cachedResponse);
return responseString.TryDeserialize(out response);
}

public async Task<TEntity?> TryGetAsync(string key, CancellationToken cancellationToken = default)
{
var cachedResponse = await _cache.GetAsync(key, cancellationToken);
if (cachedResponse is null) return null;

var responseString = Encoding.UTF8.GetString(cachedResponse);
var isSerialized = responseString.TryDeserialize<TEntity>(out var response);
return isSerialized
? response
: null;
}

for the set methods we need to convert the entity to bytes and store it in Redis using ‘IRedisDistributedCache’:

        public void Set(string key, TEntity entity)
{
var serialized = JsonSerializer.Serialize(entity);
var bytes = Encoding.UTF8.GetBytes(serialized);
_cache.Set(key, bytes);
}

public Task SetAsync(string key, TEntity entity, CancellationToken cancellationToken = default)
{
var serialized = JsonSerializer.Serialize(entity);
var bytes = Encoding.UTF8.GetBytes(serialized);
return _cache.SetAsync(key, bytes, cancellationToken);
}

Other methods implementations involve calling ‘IRedisDistributedCache’ related methods. you can add any logic you want here.

public void Refresh(string key) => _cache.Refresh(key);

public Task RefreshAsync(string key, CancellationToken cancellationToken = default) =>
_cache.RefreshAsync(key, cancellationToken);

public void Remove(string key) => _cache.Remove(key);

public Task RemoveAsync(string key, CancellationToken cancellationToken = default) =>
_cache.RemoveAsync(key, cancellationToken);

Setting Up Services

Let’s write a method to add our dependencies in the services. I’ve created a static class named ‘Setup’ and included our service definitions in there:

public static IServiceCollection AddRedisCacheManager(this IServiceCollection services, string connectionString)
{
services.AddSingleton<IRedisDitributedCache>(x =>
{
var options = x.GetRequiredService<IOptions<RedisCacheOptions>>();
options.Value.Configuration = connectionString;
return new RedisDistributedCache(options);
});
services.AddScoped(typeof(IRedisCacheManager<>), typeof(RedisCacheManager<>));

return services;
}

Use this method in your startup code to add the necessary services to the .NET service container:

builder.Services.AddCacheService(builder.Configuration.GetConnectionString("RedisConnection"));

we also need to have ‘RedisConnectionString’ in appsettings or environment variables.

To use the code we wrote we can easily inject the IRedisCacheManager in any class and use the method we need.

Example Usage

public class Book {
public int Id {get;set;}
public string Name {get;set;}
}

public class MainController : ControllerBase
{
private readonly IRedisCacheManager<Book> _cache;

public MainController(ICacheService<Book> cache)
=> _cache = cache;

[HttpGet("Book")]
public async Task<ActionResult<Book>> AdminUser(CancellationToken cancellationToken)
{
if (_cache.TryGet("bookKey", out var cachedBook) && cachedBook is not null)
return Ok(cachedBook);

var book = new Book{
Id =10,
Name=".net in action",
}
await _cache.SetAsync("bookKey", book, cancellationToken);
return Ok(book);
}
}

I’ve added more functionality like default expiration behaviour and other options to ease working with Redis cache with a type instead of bytes and etc.

git repository:

Nuget:

https://www.nuget.org/packages/CacheManager.Redis

--

--