diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsRequestExecutorBuilderExtensions.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsRequestExecutorBuilderExtensions.cs
index f95141d9ffa..d7115272a30 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsRequestExecutorBuilderExtensions.cs
@@ -22,10 +22,14 @@ public static class HotChocolateRedisPersistedOperationsRequestExecutorBuilderEx
///
/// A timeout after which an operation document is removed from the Redis cache.
///
+ ///
+ /// An optional prefix for the cache keys used to store operation documents.
+ ///
public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
this IRequestExecutorBuilder builder,
Func databaseFactory,
- TimeSpan? queryExpiration = null)
+ TimeSpan? queryExpiration = null,
+ string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(databaseFactory);
@@ -33,7 +37,8 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
return builder.ConfigureSchemaServices(
s => s.AddRedisOperationDocumentStorage(
sp => databaseFactory(sp.GetCombinedServices()),
- queryExpiration));
+ queryExpiration,
+ cacheKeyPrefix));
}
///
@@ -48,10 +53,14 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
///
/// A timeout after which an operation document is removed from the Redis cache.
///
+ ///
+ /// An optional prefix for the cache keys used to store operation documents.
+ ///
public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
this IRequestExecutorBuilder builder,
Func multiplexerFactory,
- TimeSpan? queryExpiration = null)
+ TimeSpan? queryExpiration = null,
+ string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(multiplexerFactory);
@@ -59,7 +68,8 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
return builder.ConfigureSchemaServices(
s => s.AddRedisOperationDocumentStorage(
sp => multiplexerFactory(sp.GetCombinedServices()).GetDatabase(),
- queryExpiration));
+ queryExpiration,
+ cacheKeyPrefix));
}
///
@@ -73,14 +83,19 @@ public static IRequestExecutorBuilder AddRedisOperationDocumentStorage(
///
/// A timeout after which an operation document is removed from the Redis cache.
///
+ ///
+ /// An optional prefix for the cache keys used to store operation documents.
+ ///
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(),
- queryExpiration);
+ queryExpiration,
+ cacheKeyPrefix);
}
}
diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsServiceCollectionExtensions.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsServiceCollectionExtensions.cs
index c4a228281e9..d7dc15f7820 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsServiceCollectionExtensions.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/Extensions/HotChocolateRedisPersistedOperationsServiceCollectionExtensions.cs
@@ -23,10 +23,15 @@ public static class HotChocolateRedisPersistedOperationsServiceCollectionExtensi
///
/// A timeout after which an operation document is removed from the Redis cache.
///
+ ///
+ /// 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.
+ ///
public static IServiceCollection AddRedisOperationDocumentStorage(
this IServiceCollection services,
Func databaseFactory,
- TimeSpan? queryExpiration = null)
+ TimeSpan? queryExpiration = null,
+ string? cacheKeyPrefix = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(databaseFactory);
@@ -34,7 +39,7 @@ public static IServiceCollection AddRedisOperationDocumentStorage(
return services
.RemoveService()
.AddSingleton(
- sp => new RedisOperationDocumentStorage(databaseFactory(sp), queryExpiration));
+ sp => new RedisOperationDocumentStorage(databaseFactory(sp), queryExpiration, cacheKeyPrefix));
}
private static IServiceCollection RemoveService(
diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/RedisOperationDocumentStorage.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/RedisOperationDocumentStorage.cs
index c579d8c9f5c..5dab99d88dd 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/RedisOperationDocumentStorage.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.Redis/RedisOperationDocumentStorage.cs
@@ -11,6 +11,7 @@ public class RedisOperationDocumentStorage : IOperationDocumentStorage
{
private readonly IDatabase _database;
private readonly TimeSpan? _expiration;
+ private readonly string? _cacheKeyPrefix;
///
/// Initializes a new instance of the class.
@@ -19,10 +20,14 @@ public class RedisOperationDocumentStorage : IOperationDocumentStorage
///
/// A time span after which an operation document will be removed from the cache.
///
- public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration = null)
+ ///
+ /// An optional prefix for the cache keys used to store operation documents.
+ ///
+ public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration = null, string? cacheKeyPrefix = null)
{
_database = database ?? throw new ArgumentNullException(nameof(database));
_expiration = expiration;
+ _cacheKeyPrefix = cacheKeyPrefix;
}
///
@@ -40,7 +45,7 @@ public RedisOperationDocumentStorage(IDatabase database, TimeSpan? expiration =
private async ValueTask 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));
}
@@ -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}}}";
+ }
}
diff --git a/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/IntegrationTests.cs b/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/IntegrationTests.cs
index 18729b9ac00..9edfa2457a6 100644
--- a/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/IntegrationTests.cs
+++ b/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/IntegrationTests.cs
@@ -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()
{
diff --git a/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/__snapshots__/IntegrationTests.ExecutePersistedOperation_With_CacheKeyPrefix.snap b/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/__snapshots__/IntegrationTests.ExecutePersistedOperation_With_CacheKeyPrefix.snap
new file mode 100644
index 00000000000..20b47cc26bc
--- /dev/null
+++ b/src/HotChocolate/PersistedOperations/test/PersistedOperations.Redis.Tests/__snapshots__/IntegrationTests.ExecutePersistedOperation_With_CacheKeyPrefix.snap
@@ -0,0 +1,8 @@
+{
+ "data": {
+ "__typename": "Query"
+ },
+ "extensions": {
+ "persistedDocument": true
+ }
+}