Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
Fix tests
  • Loading branch information
sebastienros committed Jul 3, 2025
commit 0e092a27abb14d7f83e3d307734f7d30c6de3bce
77 changes: 47 additions & 30 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,37 +74,47 @@ public static IResourceBuilder<AzureStorageResource> AddAzureStorage(this IDistr

var azureResource = (AzureStorageResource)infrastructure.AspireResource;

if (azureResource.BlobContainers.Count > 0)
if (azureResource.BlobStorageResource is not null)
{
var blobs = new BlobService("blobs")
{
Parent = storageAccount
};
infrastructure.Add(blobs);
var blobService = azureResource.BlobStorageResource.ToProvisioningEntity();
blobService.Parent = storageAccount;
infrastructure.Add(blobService);

foreach (var blobContainer in azureResource.BlobContainers)
if (azureResource.BlobContainers.Count > 0)
{
var cdkBlobContainer = blobContainer.ToProvisioningEntity();
cdkBlobContainer.Parent = blobs;
infrastructure.Add(cdkBlobContainer);
foreach (var blobContainer in azureResource.BlobContainers)
{
var cdkBlobContainer = blobContainer.ToProvisioningEntity();
cdkBlobContainer.Parent = blobService;
infrastructure.Add(cdkBlobContainer);
}
}
}

if (azureResource.Queues.Count > 0)
if (azureResource.QueueStorageResource is not null)
{
var queues = new QueueService("queues")
{
Parent = storageAccount
};
infrastructure.Add(queues);
foreach (var queue in azureResource.Queues)
var queueService = azureResource.QueueStorageResource.ToProvisioningEntity();
queueService.Parent = storageAccount;
infrastructure.Add(queueService);

if (azureResource.Queues.Count > 0)
{
var cdkQueue = queue.ToProvisioningEntity();
cdkQueue.Parent = queues;
infrastructure.Add(cdkQueue);
foreach (var queue in azureResource.Queues)
{
var cdkQueue = queue.ToProvisioningEntity();
cdkQueue.Parent = queueService;
infrastructure.Add(cdkQueue);
}
}
}

if (azureResource.TableStorageResource is not null)
{
var tableService = azureResource.TableStorageResource.ToProvisioningEntity();
tableService.Parent = storageAccount;
infrastructure.Add(tableService);
}

infrastructure.Add(new ProvisioningOutput("blobEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.BlobUri });
infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.QueueUri });
infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.TableUri });
Expand Down Expand Up @@ -152,16 +162,19 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc

BlobServiceClient? blobServiceClient = null;
QueueServiceClient? queueServiceClient = null;

builder
.OnBeforeResourceStarted(async (storage, @event, ct) =>
{
// The BlobServiceClient is created before the health check is run.
// The BlobServiceClient and QueueServiceClient are created before the health check is run.
// We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so
// we use BeforeResourceStartedEvent

var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
blobServiceClient = CreateBlobServiceClient(connectionString);
var blobConnectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
blobServiceClient = CreateBlobServiceClient(blobConnectionString);

var queueConnectionString = await builder.Resource.GetQueueConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null.");
queueServiceClient = CreateQueueServiceClient(queueConnectionString);
})
.OnResourceReady(async (storage, @event, ct) =>
{
Expand Down Expand Up @@ -333,6 +346,7 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobService(this IRe
ArgumentException.ThrowIfNullOrEmpty(name);

var resource = new AzureBlobStorageResource(name, builder.Resource);
builder.Resource.BlobStorageResource = resource;

string? connectionString = null;

Expand Down Expand Up @@ -370,13 +384,13 @@ public static IResourceBuilder<AzureBlobStorageContainerResource> AddBlobContain
blobContainerName ??= name;

// Create a Blob Service resource implicitly
if (builder.ApplicationBuilder.Resources.OfType<AzureBlobStorageResource>().FirstOrDefault() is not AzureBlobStorageResource blobStorageResource)
if (builder.Resource.BlobStorageResource is null)
{
var blobServiceName = $"{builder.Resource.Name}-blobs";
blobStorageResource = AddBlobService(builder, blobServiceName).Resource;
AddBlobService(builder, blobServiceName);
}

AzureBlobStorageContainerResource resource = new(name, blobContainerName, blobStorageResource);
AzureBlobStorageContainerResource resource = new(name, blobContainerName, builder.Resource.BlobStorageResource!);
builder.Resource.BlobContainers.Add(resource);

string? connectionString = null;
Expand Down Expand Up @@ -461,6 +475,8 @@ public static IResourceBuilder<AzureTableStorageResource> AddTableService(this I
ArgumentException.ThrowIfNullOrEmpty(name);

var resource = new AzureTableStorageResource(name, builder.Resource);
builder.Resource.TableStorageResource = resource;

return builder.ApplicationBuilder.AddResource(resource);
}

Expand Down Expand Up @@ -491,6 +507,7 @@ public static IResourceBuilder<AzureQueueStorageResource> AddQueueService(this I
ArgumentException.ThrowIfNullOrEmpty(name);

var resource = new AzureQueueStorageResource(name, builder.Resource);
builder.Resource.QueueStorageResource = resource;

string? connectionString = null;

Expand Down Expand Up @@ -528,13 +545,13 @@ public static IResourceBuilder<AzureQueueStorageQueueResource> AddQueue(this IRe
queueName ??= name;

// Create a Queue Service resource implicitly
if (builder.ApplicationBuilder.Resources.OfType<AzureQueueStorageResource>().FirstOrDefault() is not AzureQueueStorageResource queueStorageResource)
if (builder.Resource.QueueStorageResource is null)
{
var queueServiceName = $"{builder.Resource.Name}-queues";
queueStorageResource = AddQueueService(builder, queueServiceName).Resource;
AddQueueService(builder, queueServiceName);
}

AzureQueueStorageQueueResource resource = new(name, queueName, queueStorageResource);
AzureQueueStorageQueueResource resource = new(name, queueName, builder.Resource.QueueStorageResource!);
builder.Resource.Queues.Add(resource);

string? connectionString = null;
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class AzureStorageResource(string name, Action<AzureResourceInfrastructur
private EndpointReference EmulatorQueueEndpoint => new(this, "queue");
private EndpointReference EmulatorTableEndpoint => new(this, "table");

internal AzureBlobStorageResource? BlobStorageResource { get; set; }
internal AzureQueueStorageResource? QueueStorageResource { get; set; }
internal AzureTableStorageResource? TableStorageResource { get; set; }

internal List<AzureBlobStorageContainerResource> BlobContainers { get; } = [];

internal List<AzureQueueStorageQueueResource> Queues { get; } = [];
Expand Down
11 changes: 11 additions & 0 deletions src/Aspire.Hosting.Azure.Storage/AzureTableStorageResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Azure.Provisioning;

namespace Aspire.Hosting.Azure;

Expand Down Expand Up @@ -40,4 +41,14 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction
target[$"{AzureStorageResource.TablesConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.TableEndpoint; // Updated for consistency
}
}

/// <summary>
/// Converts the current instance to a provisioning entity.
/// </summary>
/// <returns>A <see cref="global::Azure.Provisioning.Storage.TableService"/> instance.</returns>
internal global::Azure.Provisioning.Storage.TableService ToProvisioningEntity()
{
global::Azure.Provisioning.Storage.TableService service = new(Infrastructure.NormalizeBicepIdentifier(Name));
return service;
}
}
51 changes: 51 additions & 0 deletions src/Aspire.Hosting.Azure.Storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
> NOTE: Developers must have Owner access to the target subscription so that role assignments
> can be configured for the provisioned resources.


Check failure on line 39 in src/Aspire.Hosting.Azure.Storage/README.md

View workflow job for this annotation

GitHub Actions / lint

Multiple consecutive blank lines [Expected: 1; Actual: 2]
## Usage example

In the _AppHost.cs_ file of `AppHost`, add a Blob (can use tables or queues also) Storage connection and consume the connection using the following methods:
Expand All @@ -53,6 +54,56 @@
builder.AddAzureBlobServiceClient("blobs");
```

## Creating and using blob containers and queues directly

You can create and use blob containers and queues directly by adding them to your storage resource. This allows you to provision and reference specific containers or queues for your services.

### Adding a blob container

```csharp
var storage = builder.AddAzureStorage("storage");
var container = storage.AddBlobContainer("my-container");
```

You can then pass the container reference to a project:

```csharp
builder.AddProject<Projects.MyService>()
.WithReference(container);
```

In your service, consume the container using:

```csharp
builder.AddAzureBlobContainerClient("my-container");
```

This will register a singleton of type `BlobContainerClient`.

### Adding a queue

```csharp
var storage = builder.AddAzureStorage("storage");
var queue = storage.AddQueue("my-queue");
```

Pass the queue reference to a project:

```csharp
builder.AddProject<Projects.MyService>()
.WithReference(queue);
```

In your service, consume the queue using:

```csharp
builder.AddAzureQueue("my-queue");
```

This will register a singleton of type `QueueClient`.

This approach allows you to define and use specific blob containers and queues as first-class resources in your Aspire application model.

## Additional documentation

* https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Blobs/README.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForQueueBlocksDependentReso
[RequiresDocker]
public async Task VerifyAzureStorageEmulatorResource()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));

var blobsResourceName = "BlobConnection";
var blobContainerName = "my-container";
var queuesResourceName = "QueuesConnection";
Expand Down Expand Up @@ -184,15 +186,19 @@ public async Task VerifyAzureStorageEmulatorResource()
using var host = hb.Build();
await host.StartAsync();

var rns = app.Services.GetRequiredService<ResourceNotificationService>();

await rns.WaitForResourceHealthyAsync(storage.Resource.Name, cts.Token);

var blobContainerClient = host.Services.GetRequiredService<BlobContainerClient>();
var blobClient = blobContainerClient.GetBlobClient("testKey");
var queueClient = host.Services.GetRequiredService<QueueClient>();

await blobClient.UploadAsync(BinaryData.FromString("testValue"));

var downloadResult = (await blobClient.DownloadContentAsync()).Value;
Assert.Equal("testValue", downloadResult.Content.ToString());

var queueClient = host.Services.GetRequiredService<QueueClient>();
await queueClient.SendMessageAsync("Hello, World!");
var peekedMessages = await queueClient.PeekMessagesAsync(1);
Assert.Single(peekedMessages.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}

resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
resource blob 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
name: 'default'
parent: storage
}

resource queue 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = {
name: 'default'
parent: storage
}

resource table 'Microsoft.Storage/storageAccounts/tableServices@2024-01-01' = {
parent: storage
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

output queueEndpoint string = storage.properties.primaryEndpoints.queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}

resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
name: 'default'
parent: storage
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

output queueEndpoint string = storage.properties.primaryEndpoints.queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}

resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
name: 'default'
parent: storage
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

output queueEndpoint string = storage.properties.primaryEndpoints.queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}

resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
name: 'default'
parent: storage
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob

output queueEndpoint string = storage.properties.primaryEndpoints.queue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,28 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
}
}

resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
resource storage_blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
name: 'default'
parent: storage
}

resource myContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = {
name: 'my-blob-container'
parent: blobs
parent: storage_blobs
}

resource myqueues 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = {
name: 'default'
parent: storage
}

resource myqueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = {
name: 'my-queue'
parent: myqueues
}

resource mytables 'Microsoft.Storage/storageAccounts/tableServices@2024-01-01' = {
parent: storage
}

output blobEndpoint string = storage.properties.primaryEndpoints.blob
Expand All @@ -37,4 +51,4 @@ output queueEndpoint string = storage.properties.primaryEndpoints.queue

output tableEndpoint string = storage.properties.primaryEndpoints.table

output name string = storage.name
output name string = storage.name
Loading