diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/AzureBlobOperationDocumentStorage.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/AzureBlobOperationDocumentStorage.cs
index e94ae95557c..4c01b120b67 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/AzureBlobOperationDocumentStorage.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/AzureBlobOperationDocumentStorage.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
@@ -11,7 +12,9 @@ namespace HotChocolate.PersistedOperations.AzureBlobStorage;
///
public class AzureBlobOperationDocumentStorage : IOperationDocumentStorage
{
- private static readonly BlobOpenWriteOptions _defaultBlobOpenWriteOptions = new()
+ private static readonly char[] _fileExtension = ".graphql".ToCharArray();
+
+ private static readonly BlobOpenWriteOptions _writeOptions = new()
{
HttpHeaders = new BlobHttpHeaders
{
@@ -21,28 +24,20 @@ public class AzureBlobOperationDocumentStorage : IOperationDocumentStorage
}
};
- private readonly BlobContainerClient _blobContainerClient;
- private readonly string _blobNamePrefix;
- private readonly string _blobNameSuffix;
+ private readonly BlobContainerClient _client;
///
/// Initializes a new instance of the class.
///
- /// The blob container client instance.
- /// This prefix string is prepended before the hash of the document.
- /// This suffix is appended after the hash of the document.
- public AzureBlobOperationDocumentStorage(
- BlobContainerClient containerClient,
- string blobNamePrefix,
- string blobNameSuffix)
+ /// The blob container client instance.
+ public AzureBlobOperationDocumentStorage(BlobContainerClient client)
{
- ArgumentNullException.ThrowIfNull(containerClient);
- ArgumentNullException.ThrowIfNull(blobNamePrefix);
- ArgumentNullException.ThrowIfNull(blobNameSuffix);
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
- _blobContainerClient = containerClient;
- _blobNamePrefix = blobNamePrefix;
- _blobNameSuffix = blobNameSuffix;
+ _client = client;
}
///
@@ -60,20 +55,41 @@ public AzureBlobOperationDocumentStorage(
private async ValueTask TryReadInternalAsync(
OperationDocumentId documentId,
- CancellationToken cancellationToken)
+ CancellationToken ct)
{
- var blobClient = _blobContainerClient.GetBlobClient(BlobName(documentId));
+ var blobClient = _client.GetBlobClient(CreateFileName(documentId));
+ var buffer = ArrayPool.Shared.Rent(1024);
+ var position = 0;
try
{
- await using var blobStream = await blobClient
- .OpenReadAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
-
- await using var memoryStream = new MemoryStream();
- await blobStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
- return memoryStream.Length == 0
- ? null
- : new OperationDocument(Utf8GraphQLParser.Parse(memoryStream.ToArray()));
+ await using var blobStream = await blobClient.OpenReadAsync(cancellationToken: ct).ConfigureAwait(false);
+ while (true)
+ {
+ if (buffer.Length < position + 256)
+ {
+ var newBuffer = ArrayPool.Shared.Rent(buffer.Length * 2);
+ Array.Copy(buffer, newBuffer, buffer.Length);
+ ArrayPool.Shared.Return(buffer);
+ buffer = newBuffer;
+ }
+
+ var read = await blobStream.ReadAsync(buffer, position, 256, ct);
+ position += read;
+
+ if (read < 256)
+ {
+ break;
+ }
+ }
+
+ if (position == 0)
+ {
+ return null;
+ }
+
+ var span = new ReadOnlySpan(buffer, 0, position);
+ return new OperationDocument(Utf8GraphQLParser.Parse(span));
}
catch (RequestFailedException e)
{
@@ -84,6 +100,15 @@ public AzureBlobOperationDocumentStorage(
throw;
}
+ finally
+ {
+ if(position > 0)
+ {
+ buffer.AsSpan().Slice(0, position).Clear();
+ }
+
+ ArrayPool.Shared.Return(buffer);
+ }
}
///
@@ -92,10 +117,14 @@ public ValueTask SaveAsync(
IOperationDocument document,
CancellationToken cancellationToken = default)
{
- ArgumentNullException.ThrowIfNull(document);
+ if(document == null)
+ {
+ throw new ArgumentNullException(nameof(document));
+ }
+
if (OperationDocumentId.IsNullOrEmpty(documentId))
{
- throw new ArgumentNullException(nameof(documentId));
+ throw new ArgumentException(nameof(documentId));
}
return SaveInternalAsync(documentId, document, cancellationToken);
@@ -104,15 +133,35 @@ public ValueTask SaveAsync(
private async ValueTask SaveInternalAsync(
OperationDocumentId documentId,
IOperationDocument document,
- CancellationToken cancellationToken)
+ CancellationToken ct)
{
- var blobClient = _blobContainerClient.GetBlobClient(BlobName(documentId));
- await using var outStream = await blobClient
- .OpenWriteAsync(true, _defaultBlobOpenWriteOptions, cancellationToken).ConfigureAwait(false);
-
- await document.WriteToAsync(outStream, cancellationToken).ConfigureAwait(false);
- await outStream.FlushAsync(cancellationToken).ConfigureAwait(false);
+ var blobClient = _client.GetBlobClient(CreateFileName(documentId));
+ await using var outStream = await blobClient.OpenWriteAsync(true, _writeOptions, ct).ConfigureAwait(false);
+ await document.WriteToAsync(outStream, ct).ConfigureAwait(false);
+ await outStream.FlushAsync(ct).ConfigureAwait(false);
}
- private string BlobName(OperationDocumentId documentId) => $"{_blobNamePrefix}{documentId.Value}{_blobNameSuffix}";
+
+ private static string CreateFileName(OperationDocumentId documentId)
+ {
+ var length = documentId.Value.Length + _fileExtension.Length;
+ char[]? rented = null;
+ Span span = length <= 256
+ ? stackalloc char[length]
+ : rented = ArrayPool.Shared.Rent(length);
+
+ try
+ {
+ documentId.Value.AsSpan().CopyTo(span);
+ _fileExtension.AsSpan().CopyTo(span.Slice(documentId.Value.Length));
+ return new string(span);
+ }
+ finally
+ {
+ if (rented != null)
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+ }
}
diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions.cs
index 04968342032..b99b9c556d5 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions.cs
@@ -19,19 +19,22 @@ public static class HotChocolateAzureBlobStoragePersistedOperationsRequestExecut
/// A factory that resolves the Azure Blob Container Client that
/// shall be used for persistence.
///
- /// This prefix string is prepended before the hash of the document.
- /// This suffix is appended after the hash of the document.
public static IRequestExecutorBuilder AddAzureBlobStorageOperationDocumentStorage(
this IRequestExecutorBuilder builder,
- Func containerClientFactory,
- string blobNamePrefix = "",
- string blobNameSuffix = ".graphql")
+ Func containerClientFactory)
{
- ArgumentNullException.ThrowIfNull(builder);
- ArgumentNullException.ThrowIfNull(containerClientFactory);
+ if(builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if(containerClientFactory is null)
+ {
+ throw new ArgumentNullException(nameof(containerClientFactory));
+ }
return builder.ConfigureSchemaServices(
s => s.AddAzureBlobStorageOperationDocumentStorage(
- sp => containerClientFactory(sp.GetCombinedServices()), blobNamePrefix, blobNameSuffix));
+ sp => containerClientFactory(sp.GetCombinedServices())));
}
}
diff --git a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions.cs b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions.cs
index 6b8cfc029a2..67c1fb8a9d9 100644
--- a/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions.cs
+++ b/src/HotChocolate/PersistedOperations/src/PersistedOperations.AzureBlobStorage/Extensions/HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions.cs
@@ -6,7 +6,7 @@
namespace HotChocolate;
///
-/// Provides utility methods to setup dependency injection.
+/// Provides utility methods to set up dependency injection.
///
public static class HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions
{
@@ -20,22 +20,24 @@ public static class HotChocolateAzureBlobStoragePersistedOperationsServiceCollec
/// A factory that resolves the Azure Blob Container Client that
/// shall be used for persistence.
///
- /// This prefix string is prepended before the hash of the document.
- /// This suffix is appended after the hash of the document.
public static IServiceCollection AddAzureBlobStorageOperationDocumentStorage(
this IServiceCollection services,
- Func containerClientFactory,
- string blobNamePrefix = "",
- string blobNameSuffix = ".graphql"
- )
+ Func containerClientFactory)
{
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(containerClientFactory);
+ if(services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if(containerClientFactory == null)
+ {
+ throw new ArgumentNullException(nameof(containerClientFactory));
+ }
return services
.RemoveService()
.AddSingleton(
- sp => new AzureBlobOperationDocumentStorage(containerClientFactory(sp), blobNamePrefix, blobNameSuffix));
+ sp => new AzureBlobOperationDocumentStorage(containerClientFactory(sp)));
}
private static IServiceCollection RemoveService(
diff --git a/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/AzureBlobStorageOperationDocumentStorageTests.cs b/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/AzureBlobStorageOperationDocumentStorageTests.cs
index 15015f99de9..cf3e0111bec 100644
--- a/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/AzureBlobStorageOperationDocumentStorageTests.cs
+++ b/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/AzureBlobStorageOperationDocumentStorageTests.cs
@@ -9,8 +9,6 @@ namespace HotChocolate.PersistedOperations.AzureBlobStorage;
public class AzureBlobStorageOperationDocumentStorageTests : IClassFixture
{
private readonly BlobContainerClient _client;
- private const string Prefix = "hc_";
- private const string Suffix = ".graphql";
public AzureBlobStorageOperationDocumentStorageTests(AzureStorageBlobResource blobStorageResource)
{
@@ -23,7 +21,7 @@ public async Task Write_OperationDocument_To_Storage()
{
// arrange
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
var document = new OperationDocumentSourceText("{ foo }");
// act
@@ -41,21 +39,21 @@ public async Task Write_OperationDocument_documentId_Invalid()
{
// arrange
var documentId = new OperationDocumentId();
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
var document = new OperationDocumentSourceText("{ foo }");
// act
async Task Action() => await storage.SaveAsync(documentId, document);
// assert
- await Assert.ThrowsAsync(Action);
+ await Assert.ThrowsAsync(Action);
}
[Fact]
public async Task Write_OperationDocument_OperationDocument_Is_Null()
{
// arrange
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
// act
@@ -70,7 +68,7 @@ public async Task Read_OperationDocument_From_Storage()
{
// arrange
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
var buffer = "{ foo }"u8.ToArray();
await WriteBlob(documentId.Value, buffer);
@@ -89,7 +87,7 @@ public async Task Read_OperationDocument_documentId_Invalid()
{
// arrange
var documentId = new OperationDocumentId();
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
// act
async Task Action() => await storage.TryReadAsync(documentId);
@@ -116,5 +114,5 @@ private async Task WriteBlob(string key, byte[] buffer)
private async Task DeleteBlob(string key) => await _client.DeleteBlobAsync(BlobName(key));
- private static string BlobName(string key) => $"{Prefix}{key}{Suffix}";
+ private static string BlobName(string key) => $"{key}.graphql";
}
diff --git a/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/IntegrationTests.cs b/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/IntegrationTests.cs
index 8199361e1f2..e18e145f6ab 100644
--- a/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/IntegrationTests.cs
+++ b/src/HotChocolate/PersistedOperations/test/PersistedOperations.AzureBlobStorage.Tests/IntegrationTests.cs
@@ -8,9 +8,6 @@ namespace HotChocolate.PersistedOperations.AzureBlobStorage;
public class IntegrationTests : IClassFixture
{
- private const string Prefix = "hc_";
- private const string Suffix = ".graphql";
-
private readonly BlobContainerClient _client;
public IntegrationTests(AzureStorageBlobResource blobStorageResource)
@@ -24,7 +21,7 @@ public async Task ExecutePersistedOperation()
{
// arrange
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
await storage.SaveAsync(
documentId,
@@ -34,7 +31,7 @@ await storage.SaveAsync(
await new ServiceCollection()
.AddGraphQL()
.AddQueryType(c => c.Name("Query").Field("a").Resolve("b"))
- .AddAzureBlobStorageOperationDocumentStorage(_ => _client, Prefix, Suffix)
+ .AddAzureBlobStorageOperationDocumentStorage(_ => _client)
.UseRequest(n => async c =>
{
await n(c);
@@ -62,14 +59,14 @@ public async Task ExecutePersistedOperation_NotFound()
{
// arrange
var documentId = new OperationDocumentId(Guid.NewGuid().ToString("N"));
- var storage = new AzureBlobOperationDocumentStorage(_client, Prefix, Suffix);
+ var storage = new AzureBlobOperationDocumentStorage(_client);
await storage.SaveAsync(documentId, new OperationDocumentSourceText("{ __typename }"));
var executor =
await new ServiceCollection()
.AddGraphQL()
.AddQueryType(c => c.Name("Query").Field("a").Resolve("b"))
- .AddAzureBlobStorageOperationDocumentStorage(_ => _client, Prefix, Suffix)
+ .AddAzureBlobStorageOperationDocumentStorage(_ => _client)
.UseRequest(n => async c =>
{
await n(c);
diff --git a/website/src/docs/hotchocolate/v15/performance/automatic-persisted-operations.md b/website/src/docs/hotchocolate/v15/performance/automatic-persisted-operations.md
index cd5679fbff5..14c87c749a9 100644
--- a/website/src/docs/hotchocolate/v15/performance/automatic-persisted-operations.md
+++ b/website/src/docs/hotchocolate/v15/performance/automatic-persisted-operations.md
@@ -215,7 +215,7 @@ curl -g 'http://localhost:5000/graphql/?query={__typename}&extensions={"persiste
## Step 4: Use a persisted operation document storage
-If you run multiple Hot Chocolate server instances and want to preserve stored operation documents after a server restart, you can opt to use a persisted operation document storage. Hot Chocolate supports a file-system-based operation document storage, Azure Blob Storage or a Redis cache. See [the persisted operations manual](/docs/hotchocolate/v15/performance/persisted-operations) to learn more about each option. The following example uses a Redis cache.
+If you run multiple Hot Chocolate server instances and want to preserve stored operation documents after a server restart, you can opt to use a persisted operation document storage. Hot Chocolate supports a file-system-based operation document storage, Azure Blob Storage or a Redis cache.
1. Setup a Redis docker container.
diff --git a/website/src/docs/hotchocolate/v15/performance/persisted-operations.md b/website/src/docs/hotchocolate/v15/performance/persisted-operations.md
index 8756e73211b..0506559ddb3 100644
--- a/website/src/docs/hotchocolate/v15/performance/persisted-operations.md
+++ b/website/src/docs/hotchocolate/v15/performance/persisted-operations.md
@@ -86,7 +86,7 @@ This file is expected to contain the operation document that the hash was genera
> Warning: Do not forget to ensure that the server has access to the directory.
-## Redis
+### Redis
To load persisted operation documents from Redis, we have to add the following package.
@@ -108,7 +108,7 @@ public void ConfigureServices(IServiceCollection services)
Keys in the specified Redis database are expected to be operation IDs (hashes) and contain the actual operation document as the value.
-## Azure Blob Storage
+### Azure Blob Storage
To load persisted operation documents from Azure Blob Storage, we have to add the following package.