The problem
We've multiple applications that share the same Redis cluster.
We are using IDistributedCache with Redis as the backing store.
Adding an item to the cache looks like this:
var cacheKey = "my-key";
await _distributedCache.SetStringAsync(cacheKey, "Redis rules");
Sweet.
There's one problem though...remember that Redis is a key-value store?
What would happen if another application uses the same key? Yeah, that's right, it will overwrite our key. That's not good. Let's fix it.
Redis has support for using multiple databases,
but we're not using that feature since it's (somewhat) difficult to keep track of which application uses which database since the database are named using index based naming (1, 2, 3...)
Redis solution
If you just want to fix the specific Redis implementation, all you need to do is setting the InstanceName when configuring Redis.
Here I'm setting the InstanceName to my-app.
public static IServiceCollection AddRedisDistributedCache(this IServiceCollection services)
{
return services.AddStackExchangeRedisCache(options =>
{
options.ConfigurationOptions = RedisOptions.Options;
options.InstanceName = "my-app";
});
}
public static class RedisOptions
{
static RedisOptions()
{
Options = new ConfigurationOptions
{
ClientName = "my-app",
EndPoints =
{
new DnsEndPoint("localhost", 6379)
},
Password = "redisftw"
};
}
public static ConfigurationOptions Options { get; }
}
Let's add a test that verifies that we can write/read to/from Redis.
[Fact]
public async Task CanSaveAndReadKey()
{
var services = new ServiceCollection();
services.AddRedisDistributedCache();
var sut = services.BuildServiceProvider().GetRequiredService<IDistributedCache>();
await sut.SetStringAsync("my-key", "josef");
var result = await sut.GetStringAsync("my-key");
result.ShouldBe("josef");
}
[Fact]
public async Task KeyIsPrefixedWithInstanceName()
{
var services = new ServiceCollection();
services.AddRedisDistributedCache();
var sut = services.BuildServiceProvider().GetRequiredService<IDistributedCache>();
await sut.SetStringAsync("other-key", "Any");
using var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(RedisOptions.Options);
var database = connectionMultiplexer.GetDatabase();
var result = await database.KeyExistsAsync("my-appother-key");
result.ShouldBeTrue();
}
As you can see, we don't need to care about the prefix when writing (or reading) the values.
Let's have a look in the redis database to make sure that the keys has been prefixed correctly.
Great.
Generic solution
The above solution works great, but it's only working for the Redis implementation.
Say that we, for whatever reason, want to use another implementation of IDistributedCache and still want our prefix logic to "just work"?
Luckily, it's easy to create a generic solution by using the decorator pattern.
We need to do the following:
- Implement the IDistributedCache interface
- Decorate the actual implementation (in our case, it will be the Redis implementation from the Nuget package).
- Register our implementation in the DI container.
Here's our implementation that takes care of adding the prefix.
public class KeyPrefixedDistributedCache : IDistributedCache
{
private readonly IDistributedCache _distributedCache;
private readonly string _prefix;
public KeyPrefixedDistributedCache(IDistributedCache distributedCache, string prefix)
{
_distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache));
_prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
}
public byte[]? Get(string key)
{
var prefixedKey = PrefixKey(key);
return _distributedCache.Get(prefixedKey);
}
public Task<byte[]?> GetAsync(string key, CancellationToken token = new())
{
var prefixedKey = PrefixKey(key);
return _distributedCache.GetAsync(prefixedKey, token);
}
public void Refresh(string key)
{
var prefixedKey = PrefixKey(key);
_distributedCache.Refresh(prefixedKey);
}
public Task RefreshAsync(string key, CancellationToken token = new())
{
var prefixedKey = PrefixKey(key);
return _distributedCache.RefreshAsync(prefixedKey, token);
}
public void Remove(string key)
{
var prefixedKey = PrefixKey(key);
_distributedCache.Remove(prefixedKey);
}
public Task RemoveAsync(string key, CancellationToken token = new())
{
var prefixedKey = PrefixKey(key);
return _distributedCache.RemoveAsync(prefixedKey, token);
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
var prefixedKey = PrefixKey(key);
_distributedCache.Set(prefixedKey, value, options);
}
public Task SetAsync(
string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = new())
{
var prefixedKey = PrefixKey(key);
return _distributedCache.SetAsync(prefixedKey, value, options, token);
}
private string PrefixKey(string key)
{
return $"{_prefix}-{key}";
}
}
Only thing left to do is creating a new extension method for decorating and registering our implementation...
public static IServiceCollection AddPrefixedDistributedCache(this IServiceCollection services)
{
public static IServiceCollection AddPrefixedDistributedCache(this IServiceCollection services)
{
return services.AddSingleton<IDistributedCache>(x =>
{
var redisCacheOptions = new RedisCacheOptions { ConfigurationOptions = RedisOptions.Options };
var redisCache = new RedisCache(redisCacheOptions);
var keyPrefixedRedisCache = new KeyPrefixedDistributedCache(redisCache, "my-prefix");
return keyPrefixedRedisCache;
});
}
}
...and some tests that verifies that everything works as intended.
[Fact]
public async Task CanSaveAndReadKey()
{
var services = new ServiceCollection();
services.AddPrefixedDistributedCache();
var sut = services.BuildServiceProvider().GetRequiredService<IDistributedCache>();
await sut.SetStringAsync("my-key", "josef");
var result = await sut.GetStringAsync("my-key");
result.ShouldBe("josef");
}
[Fact]
public async Task KeyIsPrefixed()
{
var services = new ServiceCollection();
services.AddPrefixedDistributedCache();
var sut = services.BuildServiceProvider().GetRequiredService<IDistributedCache>();
await sut.SetStringAsync("other-key", "Any");
using var connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(RedisOptions.Options);
var database = connectionMultiplexer.GetDatabase();
var result = await database.KeyExistsAsync("my-prefix-other-key");
result.ShouldBeTrue();
}
Let's have a look in the redis database as well, it should contain two new keys, my-prefix-my-key and my-prefix-other-key.
Awesome.
All code can be found on GitHub.