Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make StoreKeys round trippable #122

Merged
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "7.0.306",
"version": "7.0.400",
"rollForward": "latestPatch",
"allowPrerelease": false
}
Expand Down
30 changes: 26 additions & 4 deletions src/Marvin.Cache.Headers/Extensions/ServicesExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using Marvin.Cache.Headers;
using Marvin.Cache.Headers.Interfaces;
using Marvin.Cache.Headers.Serialization;
using Marvin.Cache.Headers.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand All @@ -25,6 +26,7 @@ public static class ServicesExtensions
/// <param name="dateParserFunc">Func to provide a custom <see cref="IDateParser" /></param>
/// <param name="validatorValueStoreFunc">Func to provide a custom <see cref="IValidatorValueStore" /></param>
/// <param name="storeKeyGeneratorFunc">Func to provide a custom <see cref="IStoreKeyGenerator" /></param>
/// <param name="storeKeySerializerFunc">Func to provide a custom <see cref="IStoreKeySerializer" /></param>
/// <param name="eTagGeneratorFunc">Func to provide a custom <see cref="IETagGenerator" /></param>
/// <param name="lastModifiedInjectorFunc">Func to provide a custom <see cref="ILastModifiedInjector" /></param>
public static IServiceCollection AddHttpCacheHeaders(
Expand All @@ -35,6 +37,7 @@ public static IServiceCollection AddHttpCacheHeaders(
Func<IServiceProvider, IDateParser> dateParserFunc = null,
Func<IServiceProvider, IValidatorValueStore> validatorValueStoreFunc = null,
Func<IServiceProvider, IStoreKeyGenerator> storeKeyGeneratorFunc = null,
Func<IServiceProvider, IStoreKeySerializer> storeKeySerializerFunc = null,
Func<IServiceProvider, IETagGenerator> eTagGeneratorFunc = null,
Func<IServiceProvider, ILastModifiedInjector> lastModifiedInjectorFunc = null)
{
Expand All @@ -50,23 +53,25 @@ public static IServiceCollection AddHttpCacheHeaders(
dateParserFunc,
validatorValueStoreFunc,
storeKeyGeneratorFunc,
storeKeySerializerFunc,
eTagGeneratorFunc,
lastModifiedInjectorFunc);

return services;
}

private static void AddModularParts(
IServiceCollection services,
private static void AddModularParts(IServiceCollection services,
Func<IServiceProvider, IDateParser> dateParserFunc,
Func<IServiceProvider, IValidatorValueStore> validatorValueStoreFunc,
Func<IServiceProvider, IStoreKeyGenerator> storeKeyGeneratorFunc,
Func<IServiceProvider, IStoreKeySerializer> storeKeySerializerFunc,
Func<IServiceProvider, IETagGenerator> eTagGeneratorFunc,
Func<IServiceProvider, ILastModifiedInjector> lastModifiedInjectorFunc)
{
AddDateParser(services, dateParserFunc);
AddValidatorValueStore(services, validatorValueStoreFunc);
AddStoreKeyGenerator(services, storeKeyGeneratorFunc);
AddStoreKeySerializer(services, storeKeySerializerFunc);
AddETagGenerator(services, eTagGeneratorFunc);
AddLastModifiedInjector(services, lastModifiedInjectorFunc);

Expand Down Expand Up @@ -124,7 +129,7 @@ private static void AddValidatorValueStore(

if (validatorValueStoreFunc == null)
{
validatorValueStoreFunc = _ => new InMemoryValidatorValueStore();
validatorValueStoreFunc = services => new InMemoryValidatorValueStore(services.GetRequiredService<IStoreKeySerializer>());
}

services.Add(ServiceDescriptor.Singleton(typeof(IValidatorValueStore), validatorValueStoreFunc));
Expand All @@ -146,7 +151,24 @@ private static void AddStoreKeyGenerator(

services.Add(ServiceDescriptor.Singleton(typeof(IStoreKeyGenerator), storeKeyGeneratorFunc));
}


private static void AddStoreKeySerializer(
IServiceCollection services,
Func<IServiceProvider, IStoreKeySerializer> storeKeySerializerFunc)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

if (storeKeySerializerFunc == null)
{
storeKeySerializerFunc = _ => new DefaultStoreKeySerializer();
}

services.Add(ServiceDescriptor.Singleton(typeof(IStoreKeySerializer), storeKeySerializerFunc));
}

private static void AddETagGenerator(
IServiceCollection services,
Func<IServiceProvider, IETagGenerator> eTagGeneratorFunc)
Expand Down
33 changes: 33 additions & 0 deletions src/Marvin.Cache.Headers/Interfaces/IStoreKeySerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Any comments, input: @KevinDockx
// Any issues, requests: https://github.com/KevinDockx/HttpCacheHeaders


using System;
using System.Text.Json;

namespace Marvin.Cache.Headers.Interfaces
{
/// <summary>
/// Contract for a key serializer, used to serialize a <see cref="StoreKey" />
/// </summary>
public interface IStoreKeySerializer
{
/// <summary>
/// Serialize a <see cref="StoreKey"/>.
/// </summary>
/// <param name="keyToSerialize">The <see cref="StoreKey"/> to be serialized.</param>
/// <returns>The <param name="keyToSerialize"/> serialized to a <see cref="string"/>.</returns>
///<exception cref="ArgumentNullException">thrown when the <paramref name="keyToSerialize"/> passed in is <c>null</c>.</exception>
string SerializeStoreKey(StoreKey keyToSerialize);

/// <summary>
/// Deserialize a <see cref="StoreKey"/> from a <see cref="string"/>.
/// </summary>
/// <param name="storeKeyJson">The Json representation of a <see cref="StoreKey"/> to be deserialized.</param>
/// <returns>The <param name="storeKeyJson"/> deserialized to a <see cref="StoreKey"/>.</returns>
///<exception cref="ArgumentNullException">thrown when the <paramref name="storeKeyJson"/> passed in is <c>null</c>.</exception>
///<exception cref="ArgumentException">thrown when the <paramref name="storeKeyJson"/> passed in is an empty string.</exception>
///<exception cref="JsonException">thrown when the <paramref name="storeKeyJson"/> passed in cannot be deserialized to a <see cref="StoreKey"/>.</exception>
StoreKey DeserializeStoreKey(string storeKeyJson);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Any comments, input: @KevinDockx
// Any issues, requests: https://github.com/KevinDockx/HttpCacheHeaders

using System;
using System.Text.Json;
using Marvin.Cache.Headers.Interfaces;

namespace Marvin.Cache.Headers.Serialization
{
/// <summary>
/// Serializes a <see cref="StoreKey"/> to JSON./// </summary>
public class DefaultStoreKeySerializer : IStoreKeySerializer
{
///<inheritDoc/>
public string SerializeStoreKey(StoreKey keyToSerialize)
{
ArgumentNullException.ThrowIfNull(keyToSerialize);
return JsonSerializer.Serialize(keyToSerialize);
}

///<inheritDoc/>
public StoreKey DeserializeStoreKey(string storeKeyJson)
{
if (storeKeyJson == null)
{
throw new ArgumentNullException(nameof(storeKeyJson));
}
else if (storeKeyJson.Length == 0)
{
throw new ArgumentException("The storeKeyJson parameter cannot be an empty string.", nameof(storeKeyJson));
}

return JsonSerializer.Deserialize<StoreKey>(storeKeyJson);
}
}
}
53 changes: 25 additions & 28 deletions src/Marvin.Cache.Headers/Stores/InMemoryValidatorValueStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ private readonly ConcurrentDictionary<string, ValidatorValue> _store
private readonly ConcurrentDictionary<string, StoreKey> _storeKeyStore
= new ConcurrentDictionary<string, StoreKey>();

public Task<ValidatorValue> GetAsync(StoreKey key) => GetAsync(key.ToString());
//Serializer for StoreKeys.
private readonly IStoreKeySerializer _storeKeySerializer;

public InMemoryValidatorValueStore(IStoreKeySerializer storeKeySerializer) => _storeKeySerializer =
storeKeySerializer ?? throw new ArgumentNullException(nameof(storeKeySerializer));

public Task<ValidatorValue> GetAsync(StoreKey key)
{
var keyJson = _storeKeySerializer.SerializeStoreKey(key);
return GetAsync(keyJson);
}

private Task<ValidatorValue> GetAsync(string key)
{
Expand All @@ -41,9 +51,10 @@ private Task<ValidatorValue> GetAsync(string key)
public Task SetAsync(StoreKey key, ValidatorValue eTag)
{
// store the validator value
_store[key.ToString()] = eTag;
var keyJson = _storeKeySerializer.SerializeStoreKey(key);
_store[keyJson] = eTag;
// save the key itself as well, with an easily searchable stringified key
_storeKeyStore[key.ToString()] = key;
_storeKeyStore[keyJson] = key;
return Task.FromResult(0);
}

Expand All @@ -54,7 +65,8 @@ public Task SetAsync(StoreKey key, ValidatorValue eTag)
/// <returns></returns>
public Task<bool> RemoveAsync(StoreKey key)
{
_storeKeyStore.TryRemove(key.ToString(), out _);
var keyJson = _storeKeySerializer.SerializeStoreKey(key);
_storeKeyStore.TryRemove(keyJson, out _);
return Task.FromResult(_store.TryRemove(key.ToString(), out _));
}

Expand All @@ -74,32 +86,17 @@ public async IAsyncEnumerable<StoreKey> FindStoreKeysByKeyPartAsync(string value
if (ignoreCase)
{
valueToMatch = valueToMatch.ToLowerInvariant();

foreach (var key in _storeKeyStore.Keys
.Where(k => k.ToLowerInvariant().Contains(valueToMatch)))
{
if (_storeKeyStore.TryGetValue(key, out StoreKey storeKey))
{
lstStoreKeysToReturn.Add(storeKey);
}
}
}
else
{
foreach (var key in _storeKeyStore.Keys
.Where(k => k.Contains(valueToMatch)))

foreach (var key in _store.Keys)
{
if (_storeKeyStore.TryGetValue(key, out StoreKey storeKey))
{
lstStoreKeysToReturn.Add(storeKey);
}
var deserializedKey =_storeKeySerializer.DeserializeStoreKey(key);
var deserializedKeyValues = String.Join(',', ignoreCase ? deserializedKey.Values.Select(x => x.ToLower()) : deserializedKey.Values);
if (deserializedKeyValues.Contains(valueToMatch))
{
yield return deserializedKey;
}
}
}

for (int i = 0; i < lstStoreKeysToReturn.Count; i++)
{
yield return lstStoreKeysToReturn[i];
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="Quibble.Xunit" Version="0.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -26,6 +27,7 @@
</ItemGroup>

<ItemGroup>
<Folder Include="Serialization\" />
<Folder Include="Properties\" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Text.Json;
using Marvin.Cache.Headers.Serialization;
using Quibble.Xunit;
using Xunit;

namespace Marvin.Cache.Headers.Test.Serialization;

public class DefaultStoreKeySerializerFacts
{
private readonly DefaultStoreKeySerializer _storeKeySerializer =new();

[Fact]
public void SerializeStoreKey_ThrowsArgumentNullException_WhenKeyToSerializeIsNull()
{
StoreKey keyToSerialize = null;
Assert.Throws<ArgumentNullException>(() =>_storeKeySerializer.SerializeStoreKey(keyToSerialize));
}

[Fact]
public void SerializeStoreKey_ReturnsTheKeyToSerializeAsJson_WhenStoreKeyIsNotNull()
{
var keyToSerialize = new StoreKey
{
{ "testKey", "TestValue" }
};
const string expectedStoreKeyJson = "{\"testKey\":\"TestValue\"}";

var serializedStoreKey = _storeKeySerializer.
SerializeStoreKey(keyToSerialize);

JsonAssert.Equal(expectedStoreKeyJson, serializedStoreKey);
}

[Fact]
public void DeserializeStoreKey_ThrowsArgumentNullException_WhenStoreKeyJsonIsNull()
{
string storeKeyJson = null;
Assert.Throws<ArgumentNullException>(() => _storeKeySerializer.DeserializeStoreKey(storeKeyJson));
}

[Fact]
public void DeserializeStoreKey_ThrowsArgumentException_WhenStoreKeyJsonIsAnEmptyString()
{
var storeKeyJson = String.Empty;
Assert.Throws<ArgumentException>(() => _storeKeySerializer.DeserializeStoreKey(storeKeyJson));
}
[Fact]
public void DeserializeStoreKey_ThrowsJsonException_WhenStoreKeyJsonIsInvalid()
{
const string storeKeyJson = "{";
Assert.Throws<JsonException>(() => _storeKeySerializer.DeserializeStoreKey(storeKeyJson));
}

[Fact]
public void DeserializeStoreKey_ReturnsTheStoreKeyJsonAsAStoreKey_WhenTheStoreKeyJsonIsValidJson()
{
var expectedStoreKey = new StoreKey
{
{ "testKey", "TestValue" }
};
const string storeKeyJson = "{\"testKey\":\"TestValue\"}";

var deserializedStoreKey = _storeKeySerializer.DeserializeStoreKey(storeKeyJson);

Assert.Equal(expectedStoreKey, deserializedStoreKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Marvin.Cache.Headers.Interfaces;
using Marvin.Cache.Headers.Stores;
using Moq;
using Xunit;

namespace Marvin.Cache.Headers.Test.Stores
{
public class InMemoryValidatorValueStoreFacts
{
[Fact]
public void Ctor_ThrowsArgumentNullException_WhenStoreKeySerializerIsNull()
{
IStoreKeySerializer storeKeySerializer = null;
Assert.Throws<ArgumentNullException>(() =>new InMemoryValidatorValueStore(storeKeySerializer));
}

[Fact]
public async Task GetAsync_Returns_Stored_ValidatorValue()
{
Expand All @@ -22,9 +32,15 @@ public async Task GetAsync_Returns_Stored_ValidatorValue()
{ "queryString", string.Empty },
{ "requestHeaderValues", string.Join("-", new List<string> {"text/plain", "gzip"})}
};

var target = new InMemoryValidatorValueStore();
await target.SetAsync(requestKey, new ValidatorValue(new ETag(ETagType.Strong, "test"), referenceTime));

var requestKeyJson =JsonSerializer.Serialize(requestKey);
var storeKeySerializer =new Mock<IStoreKeySerializer>();
storeKeySerializer.Setup(x =>x.SerializeStoreKey(requestKey)).Returns(requestKeyJson);
storeKeySerializer.Setup(x => x.DeserializeStoreKey(requestKeyJson)).Returns(requestKey);

var target = new InMemoryValidatorValueStore(storeKeySerializer.Object);

await target.SetAsync(requestKey, new ValidatorValue(new ETag(ETagType.Strong, "test"), referenceTime));

// act
var result = await target.GetAsync(requestKey);
Expand Down Expand Up @@ -53,8 +69,14 @@ public async Task GetAsync_DoesNotReturn_Unknown_ValidatorValue()
{ "queryString", string.Empty },
{ "requestHeaderValues", string.Join("-", new List<string> {"text/plain", "gzip"})}
};

var storeKeySerializer = new Mock<IStoreKeySerializer>();
var requestKeyJson =JsonSerializer.Serialize(requestKey);
var requestKey2Json = JsonSerializer.Serialize(requestKey2);
storeKeySerializer.Setup(x =>x.SerializeStoreKey(requestKey)).Returns(requestKeyJson);
storeKeySerializer.Setup(x => x.SerializeStoreKey(requestKey2)).Returns(requestKey2Json);

var target = new InMemoryValidatorValueStore();
var target = new InMemoryValidatorValueStore(storeKeySerializer.Object);
await target.SetAsync(requestKey, new ValidatorValue(new ETag(ETagType.Strong, "test"), referenceTime));

// act
Expand Down