Skip to content

Add optional Redis key prefix to essentially allow for namespacing of the Redis keys #8416

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@ public static class HotChocolateRedisPersistedOperationsRequestExecutorBuilderEx
/// <param name="queryExpiration">
/// A timeout after which an operation document is removed from the Redis cache.
/// </param>
/// <param name="cacheKeyPrefix">
/// An optional prefix for the cache keys used to store operation documents.
/// </param>
public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
this IRequestExecutorBuilder builder,
Func<IServiceProvider, IDatabase> databaseFactory,
TimeSpan? queryExpiration = null)
TimeSpan? queryExpiration = null,
string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(databaseFactory);

return builder.ConfigureSchemaServices(
s => s.AddRedisOperationDocumentStorage(
sp => databaseFactory(sp.GetCombinedServices()),
queryExpiration));
queryExpiration,
cacheKeyPrefix));
}

/// <summary>
Expand All @@ -48,18 +53,23 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
/// <param name="queryExpiration">
/// A timeout after which an operation document is removed from the Redis cache.
/// </param>
/// <param name="cacheKeyPrefix">
/// An optional prefix for the cache keys used to store operation documents.
/// </param>
public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
this IRequestExecutorBuilder builder,
Func<IServiceProvider, IConnectionMultiplexer> multiplexerFactory,
TimeSpan? queryExpiration = null)
TimeSpan? queryExpiration = null,
string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(multiplexerFactory);

return builder.ConfigureSchemaServices(
s => s.AddRedisOperationDocumentStorage(
sp => multiplexerFactory(sp.GetCombinedServices()).GetDatabase(),
queryExpiration));
queryExpiration,
cacheKeyPrefix));
}

/// <summary>
Expand All @@ -73,14 +83,19 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
/// <param name="queryExpiration">
/// A timeout after which an operation document is removed from the Redis cache.
/// </param>
/// <param name="cacheKeyPrefix">
/// An optional prefix for the cache keys used to store operation documents.
/// </param>
public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
this IRequestExecutorBuilder builder,
TimeSpan? queryExpiration = null)
TimeSpan? queryExpiration = null,
string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.AddRedisOperationDocumentStorage(
sp => sp.GetRequiredService<IConnectionMultiplexer>(),
queryExpiration);
queryExpiration,
cacheKeyPrefix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ public static class HotChocolateRedisPersistedOperationsServiceCollectionExtensi
/// <param name="queryExpiration">
/// A timeout after which an operation document is removed from the Redis cache.
/// </param>
/// <param name="cacheKeyPrefix">
/// An optional prefix for the cache keys used to store operation documents.
/// This can be useful to avoid key collisions when multiple applications share the same Redis instance.
/// </param>
public static IServiceCollection AddRedisOperationDocumentStorage(
this IServiceCollection services,
Func<IServiceProvider, IDatabase> databaseFactory,
TimeSpan? queryExpiration = null)
TimeSpan? queryExpiration = null,
string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(databaseFactory);

return services
.RemoveService<IOperationDocumentStorage>()
.AddSingleton<IOperationDocumentStorage>(
sp => new RedisOperationDocumentStorage(databaseFactory(sp), queryExpiration));
sp => new RedisOperationDocumentStorage(databaseFactory(sp), queryExpiration, cacheKeyPrefix));
}

private static IServiceCollection RemoveService<TService>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class RedisOperationDocumentStorage : IOperationDocumentStorage
{
private readonly IDatabase _database;
private readonly TimeSpan? _expiration;
private readonly string? _cacheKeyPrefix;

/// <summary>
/// Initializes a new instance of the class.
Expand All @@ -19,10 +20,14 @@ public class RedisOperationDocumentStorage : IOperationDocumentStorage
/// <param name="expiration">
/// A time span after which an operation document will be removed from the cache.
/// </param>
public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration = null)
/// <param name="cacheKeyPrefix">
/// An optional prefix for the cache keys used to store operation documents.
/// </param>
public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration = null, string? cacheKeyPrefix = null)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
_expiration = expiration;
_cacheKeyPrefix = cacheKeyPrefix;
}

/// <inheritdoc />
Expand All @@ -40,7 +45,7 @@ public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration =

private async ValueTask<IOperationDocument?> TryReadInternalAsync(OperationDocumentId documentId)
{
var buffer = (byte[]?)await _database.StringGetAsync(documentId.Value).ConfigureAwait(false);
var buffer = (byte[]?)await _database.StringGetAsync(GetCacheKey(documentId.Value)).ConfigureAwait(false);
return buffer is null ? null : new OperationDocument(Utf8GraphQLParser.Parse(buffer));
}

Expand All @@ -65,8 +70,23 @@ private async ValueTask SaveInternalAsync(
IOperationDocument document)
{
var promise = _expiration.HasValue
? _database.StringSetAsync(documentId.Value, document.ToArray(), _expiration.Value)
: _database.StringSetAsync(documentId.Value, document.ToArray());
? _database.StringSetAsync(GetCacheKey(documentId.Value), document.ToArray(), _expiration.Value)
: _database.StringSetAsync(GetCacheKey(documentId.Value), document.ToArray());
await promise.ConfigureAwait(false);
}

private string GetCacheKey(string documentId)
{
if (string.IsNullOrEmpty(documentId))
{
throw new ArgumentNullException(nameof(documentId));
}

if (string.IsNullOrEmpty(_cacheKeyPrefix))
{
return documentId;
}

return $"{_cacheKeyPrefix}:{{{documentId}}}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ await storage.SaveAsync(
result.MatchSnapshot();
}

[Fact]
public async Task ExecutePersistedOperation_With_CacheKeyPrefix()
{
// arrange
const string cacheKeyPrefix = "test-prefix";
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
var storage = new RedisOperationDocumentStorage(_database, cacheKeyPrefix: cacheKeyPrefix);

await storage.SaveAsync(
documentId,
new OperationDocumentSourceText("{ __typename }"));

var executor =
await new ServiceCollection()
.AddGraphQL()
.AddQueryType(c => c.Name("Query").Field("a").Resolve("b"))
.AddRedisOperationDocumentStorage(_ => _database, cacheKeyPrefix: cacheKeyPrefix)
.UseRequest((_, n) => async c =>
{
await n(c);

var documentInfo = c.OperationDocumentInfo;
if (documentInfo.Id == documentId && c.Result is IOperationResult r)
{
c.Result = OperationResultBuilder
.FromResult(r)
.SetExtension("persistedDocument", true)
.Build();
}
})
.UsePersistedOperationPipeline()
.BuildRequestExecutorAsync();

// act
var result = await executor.ExecuteAsync(OperationRequest.FromId(documentId));

// assert
result.MatchSnapshot();
}

[Fact]
public async Task ExecutePersistedOperation_After_Expiration()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"data": {
"__typename": "Query"
},
"extensions": {
"persistedDocument": true
}
}