-
-
Notifications
You must be signed in to change notification settings - Fork 749
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Azure Blob Storage as storage backend for Persisted Operations (#…
- Loading branch information
Showing
21 changed files
with
888 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
...dOperations/src/PersistedOperations.AzureBlobStorage/AzureBlobOperationDocumentStorage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
using Azure; | ||
using Azure.Storage.Blobs; | ||
using Azure.Storage.Blobs.Models; | ||
using HotChocolate.Execution; | ||
using HotChocolate.Language; | ||
|
||
namespace HotChocolate.PersistedOperations.AzureBlobStorage; | ||
|
||
/// <summary> | ||
/// An implementation of <see cref="IOperationDocumentStorage"/> that uses Redis as a storage. | ||
/// </summary> | ||
public class AzureBlobOperationDocumentStorage : IOperationDocumentStorage | ||
{ | ||
private static readonly BlobOpenWriteOptions _defaultBlobOpenWriteOptions = new() | ||
{ | ||
HttpHeaders = new BlobHttpHeaders | ||
{ | ||
ContentType = "application/graphql", | ||
ContentDisposition = "inline", | ||
CacheControl = "public, max-age=604800, immutable" | ||
} | ||
}; | ||
|
||
private readonly BlobContainerClient _blobContainerClient; | ||
private readonly string _blobNamePrefix; | ||
private readonly string _blobNameSuffix; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the class. | ||
/// </summary> | ||
/// <param name="containerClient">The blob container client instance.</param> | ||
/// <param name="blobNamePrefix">This prefix string is prepended before the hash of the document.</param> | ||
/// <param name="blobNameSuffix">This suffix is appended after the hash of the document.</param> | ||
public AzureBlobOperationDocumentStorage( | ||
BlobContainerClient containerClient, | ||
string blobNamePrefix, | ||
string blobNameSuffix) | ||
{ | ||
ArgumentNullException.ThrowIfNull(containerClient); | ||
ArgumentNullException.ThrowIfNull(blobNamePrefix); | ||
ArgumentNullException.ThrowIfNull(blobNameSuffix); | ||
|
||
_blobContainerClient = containerClient; | ||
_blobNamePrefix = blobNamePrefix; | ||
_blobNameSuffix = blobNameSuffix; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public ValueTask<IOperationDocument?> TryReadAsync( | ||
OperationDocumentId documentId, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
if (OperationDocumentId.IsNullOrEmpty(documentId)) | ||
{ | ||
throw new ArgumentNullException(nameof(documentId)); | ||
} | ||
|
||
return TryReadInternalAsync(documentId, cancellationToken); | ||
} | ||
|
||
private async ValueTask<IOperationDocument?> TryReadInternalAsync( | ||
OperationDocumentId documentId, | ||
CancellationToken cancellationToken) | ||
{ | ||
var blobClient = _blobContainerClient.GetBlobClient(BlobName(documentId)); | ||
|
||
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())); | ||
} | ||
catch (RequestFailedException e) | ||
{ | ||
if (e.Status == 404) | ||
{ | ||
return null; | ||
} | ||
|
||
throw; | ||
} | ||
} | ||
|
||
/// <inheritdoc /> | ||
public ValueTask SaveAsync( | ||
OperationDocumentId documentId, | ||
IOperationDocument document, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
ArgumentNullException.ThrowIfNull(document); | ||
if (OperationDocumentId.IsNullOrEmpty(documentId)) | ||
{ | ||
throw new ArgumentNullException(nameof(documentId)); | ||
} | ||
|
||
return SaveInternalAsync(documentId, document, cancellationToken); | ||
} | ||
|
||
private async ValueTask SaveInternalAsync( | ||
OperationDocumentId documentId, | ||
IOperationDocument document, | ||
CancellationToken cancellationToken) | ||
{ | ||
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); | ||
} | ||
|
||
private string BlobName(OperationDocumentId documentId) => $"{_blobNamePrefix}{documentId.Value}{_blobNameSuffix}"; | ||
} |
37 changes: 37 additions & 0 deletions
37
...nsions/HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using Azure.Storage.Blobs; | ||
using HotChocolate.Execution.Configuration; | ||
using HotChocolate; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection; | ||
|
||
/// <summary> | ||
/// Provides utility methods to setup dependency injection. | ||
/// </summary> | ||
public static class HotChocolateAzureBlobStoragePersistedOperationsRequestExecutorBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Adds an Azure Blob Storage based operation document storage to the service collection. | ||
/// </summary> | ||
/// <param name="builder"> | ||
/// The service collection to which the services are added. | ||
/// </param> | ||
/// <param name="containerClientFactory"> | ||
/// A factory that resolves the Azure Blob Container Client that | ||
/// shall be used for persistence. | ||
/// </param> | ||
/// <param name="blobNamePrefix">This prefix string is prepended before the hash of the document.</param> | ||
/// <param name="blobNameSuffix">This suffix is appended after the hash of the document.</param> | ||
public static IRequestExecutorBuilder AddAzureBlobStorageOperationDocumentStorage( | ||
this IRequestExecutorBuilder builder, | ||
Func<IServiceProvider, BlobContainerClient> containerClientFactory, | ||
string blobNamePrefix = "", | ||
string blobNameSuffix = ".graphql") | ||
{ | ||
ArgumentNullException.ThrowIfNull(builder); | ||
ArgumentNullException.ThrowIfNull(containerClientFactory); | ||
|
||
return builder.ConfigureSchemaServices( | ||
s => s.AddAzureBlobStorageOperationDocumentStorage( | ||
sp => containerClientFactory(sp.GetCombinedServices()), blobNamePrefix, blobNameSuffix)); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
.../Extensions/HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using Azure.Storage.Blobs; | ||
using HotChocolate.Execution; | ||
using HotChocolate.PersistedOperations.AzureBlobStorage; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace HotChocolate; | ||
|
||
/// <summary> | ||
/// Provides utility methods to setup dependency injection. | ||
/// </summary> | ||
public static class HotChocolateAzureBlobStoragePersistedOperationsServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Adds an Azure Blob Storage based operation document storage to the service collection. | ||
/// </summary> | ||
/// <param name="services"> | ||
/// The service collection to which the services are added. | ||
/// </param> | ||
/// <param name="containerClientFactory"> | ||
/// A factory that resolves the Azure Blob Container Client that | ||
/// shall be used for persistence. | ||
/// </param> | ||
/// <param name="blobNamePrefix">This prefix string is prepended before the hash of the document.</param> | ||
/// <param name="blobNameSuffix">This suffix is appended after the hash of the document.</param> | ||
public static IServiceCollection AddAzureBlobStorageOperationDocumentStorage( | ||
this IServiceCollection services, | ||
Func<IServiceProvider, BlobContainerClient> containerClientFactory, | ||
string blobNamePrefix = "", | ||
string blobNameSuffix = ".graphql" | ||
) | ||
{ | ||
ArgumentNullException.ThrowIfNull(services); | ||
ArgumentNullException.ThrowIfNull(containerClientFactory); | ||
|
||
return services | ||
.RemoveService<IOperationDocumentStorage>() | ||
.AddSingleton<IOperationDocumentStorage>( | ||
sp => new AzureBlobOperationDocumentStorage(containerClientFactory(sp), blobNamePrefix, blobNameSuffix)); | ||
} | ||
|
||
private static IServiceCollection RemoveService<TService>( | ||
this IServiceCollection services) | ||
{ | ||
var serviceDescriptor = services.FirstOrDefault(t => t.ServiceType == typeof(TService)); | ||
|
||
if (serviceDescriptor != null) | ||
{ | ||
services.Remove(serviceDescriptor); | ||
} | ||
|
||
return services; | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
...istedOperations.AzureBlobStorage/HotChocolate.PersistedOperations.AzureBlobStorage.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="Current"> | ||
|
||
<PropertyGroup> | ||
<PackageId>HotChocolate.PersistedOperations.AzureBlobStorage</PackageId> | ||
<Description>An implementation of Hot Chocolate persisted operations using an Azure (R) Storage Account.</Description> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\Core\src\Core\HotChocolate.Core.csproj" /> | ||
<ProjectReference Include="..\..\..\Utilities\src\Utilities\HotChocolate.Utilities.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Compile Update="Properties\Resources.Designer.cs"> | ||
<DesignTime>True</DesignTime> | ||
<AutoGen>True</AutoGen> | ||
<DependentUpon>Resources.resx</DependentUpon> | ||
</Compile> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<EmbeddedResource Update="Properties\Resources.resx"> | ||
<Generator>ResXFileCodeGenerator</Generator> | ||
<LastGenOutput>Resources.Designer.cs</LastGenOutput> | ||
</EmbeddedResource> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Azure.Storage.Blobs" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.