Skip to content

Commit

Permalink
Merge pull request KevinDockx#128 from SeanFarrow/feature/sf/112-add-…
Browse files Browse the repository at this point in the history
…distributed-store

Provide the ability to use Redis as a store for the cache
  • Loading branch information
KevinDockx authored Oct 4, 2023
2 parents 471cc04 + ca06d67 commit a9f3af1
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.6.90" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ public async IAsyncEnumerable<string> FindStoreKeysByKeyPartAsync(string valueTo
}

RedisValue valueToMatchWithRedisPattern = ignoreCase ? $"pattern: {valueToMatch.ToLower()}" : $"pattern: {valueToMatch}";
List<string> foundKeys =new List<string>();

foreach (var server in servers)
{
var keys = server.KeysAsync(_redisDistributedCacheKeyRetrieverOptions.Database, valueToMatchWithRedisPattern);

await foreach (var key in keys)
{
yield return key;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ public class DistributedCacheValidatorValueStore : IValidatorValueStore
{
private readonly IDistributedCache _distributedCache;
private readonly IRetrieveDistributedCacheKeys _distributedCacheKeyRetriever;
private readonly IStoreKeySerializer _storeKeySerializer;

public DistributedCacheValidatorValueStore(IDistributedCache distributedCache, IRetrieveDistributedCacheKeys distributedCacheKeyRetriever)
public DistributedCacheValidatorValueStore(IDistributedCache distributedCache, IRetrieveDistributedCacheKeys distributedCacheKeyRetriever, IStoreKeySerializer storeKeySerializer =null)
{
_distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache));
_distributedCacheKeyRetriever = distributedCacheKeyRetriever ?? throw new ArgumentNullException(nameof(distributedCacheKeyRetriever));
_storeKeySerializer = storeKeySerializer ?? throw new ArgumentNullException(nameof(storeKeySerializer));
}

public async Task<ValidatorValue> GetAsync(StoreKey key)
Expand All @@ -30,11 +32,12 @@ public async Task<ValidatorValue> GetAsync(StoreKey key)
throw new ArgumentNullException(nameof(key));
}

var result = await _distributedCache.GetAsync(key.ToString(), CancellationToken.None);
var serializedKey = _storeKeySerializer.SerializeStoreKey(key);
var result = await _distributedCache.GetAsync(serializedKey, CancellationToken.None);
return result == null ? null : CreateValidatorValue(result);
}

private ValidatorValue CreateValidatorValue(byte[] validatorValueBytes)
private static ValidatorValue CreateValidatorValue(byte[] validatorValueBytes)
{
var validatorValueUtf8String = Encoding.UTF8.GetString(validatorValueBytes);
var validatorValueETagTypeString = validatorValueUtf8String[..validatorValueUtf8String.IndexOf(" ", StringComparison.InvariantCulture)];
Expand All @@ -59,10 +62,11 @@ public Task SetAsync(StoreKey key, ValidatorValue validatorValue)
{
throw new ArgumentNullException(nameof(validatorValue));
}


var keyJson = _storeKeySerializer.SerializeStoreKey(key);
var eTagString = $"{validatorValue.ETag.ETagType} Value=\"{validatorValue.ETag.Value}\" LastModified={validatorValue.LastModified.ToString(CultureInfo.InvariantCulture)}";
var eTagBytes = Encoding.UTF8.GetBytes(eTagString);
return _distributedCache.SetAsync(key.ToString(), eTagBytes);
return _distributedCache.SetAsync(keyJson, eTagBytes);
}

public async Task<bool> RemoveAsync(StoreKey key)
Expand All @@ -72,8 +76,14 @@ public async Task<bool> RemoveAsync(StoreKey key)
throw new ArgumentNullException(nameof(key));
}

var keyString = key.ToString();
await _distributedCache.RemoveAsync(keyString);
var keyJson = _storeKeySerializer.SerializeStoreKey(key);
var cacheEntry = await _distributedCache.GetAsync(keyJson, CancellationToken.None);
if (cacheEntry is null)
{
return false;
}

await _distributedCache.RemoveAsync(keyJson);
return true;
}

Expand All @@ -91,7 +101,8 @@ public async IAsyncEnumerable<StoreKey> FindStoreKeysByKeyPartAsync(string value
var foundKeys = _distributedCacheKeyRetriever.FindStoreKeysByKeyPartAsync(valueToMatch, ignoreCase);
await foreach (var foundKey in foundKeys.ConfigureAwait(false))
{
yield return new StoreKey();
var k = _storeKeySerializer.DeserializeStoreKey(foundKey);
yield return k;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,34 +54,30 @@ public void Constructs_A_RedisDistributedCacheKeyRetriever_When_All_The_Passed_I
}

[Fact]
public void FindStoreKeysByKeyPartAsync_Throws_An_Argument_Null_Exception_When_The_valueToMatch_Passed_in_Is_null()
public async Task FindStoreKeysByKeyPartAsync_Throws_An_Argument_Null_Exception_When_The_valueToMatch_Passed_in_Is_null()
{
var connectionMultiplexer = new Mock<IConnectionMultiplexer>();
var redisDistributedCacheKeyRetrieverOptions = new Mock<IOptions<RedisDistributedCacheKeyRetrieverOptions>>();
redisDistributedCacheKeyRetrieverOptions.SetupGet(x => x.Value).Returns(new RedisDistributedCacheKeyRetrieverOptions());
var redisDistributedCacheKeyRetriever = new RedisDistributedCacheKeyRetriever(connectionMultiplexer.Object, redisDistributedCacheKeyRetrieverOptions.Object);
string? valueToMatch = null;
var result = redisDistributedCacheKeyRetriever.FindStoreKeysByKeyPartAsync(valueToMatch);
var enumerator = result.GetAsyncEnumerator();
Assert.ThrowsAsync<ArgumentNullException>(async () => await enumerator.MoveNextAsync());
}
var exception = await CaptureTheExceptionIfOneIsThrownFromAnIAsyncEnumerable(() => redisDistributedCacheKeyRetriever.FindStoreKeysByKeyPartAsync(valueToMatch));
Assert.IsType<ArgumentNullException>(exception);
}

[Fact]
public void FindStoreKeysByKeyPartAsync_Throws_An_Argument_Exception_When_The_valueToMatch_Passed_in_Is_an_empty_string()
public async Task FindStoreKeysByKeyPartAsync_Throws_An_Argument_Exception_When_The_valueToMatch_Passed_in_Is_an_empty_string()
{
var connectionMultiplexer = new Mock<IConnectionMultiplexer>();
var redisDistributedCacheKeyRetrieverOptions = new Mock<IOptions<RedisDistributedCacheKeyRetrieverOptions>>();
redisDistributedCacheKeyRetrieverOptions.SetupGet(x => x.Value).Returns(new RedisDistributedCacheKeyRetrieverOptions());
var redisDistributedCacheKeyRetriever = new RedisDistributedCacheKeyRetriever(connectionMultiplexer.Object, redisDistributedCacheKeyRetrieverOptions.Object);
var valueToMatch = String.Empty;
var result = redisDistributedCacheKeyRetriever.FindStoreKeysByKeyPartAsync(valueToMatch);
var enumerator = result.GetAsyncEnumerator();
Assert.ThrowsAsync<ArgumentException>(async () =>await enumerator.MoveNextAsync());
}
var exception = await CaptureTheExceptionIfOneIsThrownFromAnIAsyncEnumerable(() => redisDistributedCacheKeyRetriever.FindStoreKeysByKeyPartAsync(valueToMatch));
Assert.IsType<ArgumentException>(exception);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
[Theory, CombinatorialData]
public async Task FindStoreKeysByKeyPartAsync_Returns_An_Empty_Collection_Of_Keys_When_No_Servers_Are_available(bool onlyUseReplicas)
{
var connectionMultiplexer = new Mock<IConnectionMultiplexer>();
Expand Down Expand Up @@ -140,11 +136,9 @@ public async Task FindStoreKeysByKeyPartAsync_Returns_An_Empty_Collection_Of_Key
}

[Theory, CombinatorialData]
public async Task
FindStoreKeysByKeyPartAsync_Returns_A_Collection_Of_Keys_When_At_Least_One_Server_Is_Available_And_At_Least_One_Key_Exists_On_Any_Of_The_Available_Servers_That_Match_The_Past_in_Value_To_Match_In_The_Database_Specified_In_The_Options_Passed_to_The_Constructor(
bool onlyUseReplicas, bool ignoreCase, [CombinatorialRange(1, 2)] int numberOfServers)
public async Task FindStoreKeysByKeyPartAsync_Returns_A_Collection_Of_Keys_When_At_Least_One_Server_Is_Available_And_At_Least_One_Key_Exists_On_Any_Of_The_Available_Servers_That_Match_The_Past_in_Value_To_Match_In_The_Database_Specified_In_The_Options_Passed_to_The_Constructor(bool onlyUseReplicas, bool ignoreCase, [CombinatorialRange(1, 2)] int numberOfServers)
{
var rand = new Random();
var rand = Random.Shared;
var valueToMatch = "TestKey";
var valueToMatchWithPattern = GetValueToMatchWithPattern(valueToMatch, ignoreCase);
var keyPostFixes = Enumerable.Range(0, numberOfServers * 2);
Expand Down Expand Up @@ -195,4 +189,20 @@ private static Mock<IServer> SetupAServer(bool isReplica, int database, RedisVal
}

private static RedisValue GetValueToMatchWithPattern(string valueToMatch, bool ignoreCase) => ignoreCase ? $"pattern: {valueToMatch.ToLower()}" : $"pattern: {valueToMatch}";

private static async Task<Exception> CaptureTheExceptionIfOneIsThrownFromAnIAsyncEnumerable<T>(Func<IAsyncEnumerable<T>> sequenceGenerator)
{
try
{
await foreach (var _ in sequenceGenerator())
{
}
}
catch (Exception e)
{
return e;
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Interactive.Async" Version="6.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading

0 comments on commit a9f3af1

Please sign in to comment.