Skip to content

Commit e028bd5

Browse files
committed
AzureStorage auto create blob containers
Resolves #5167
1 parent f971998 commit e028bd5

File tree

10 files changed

+266
-9
lines changed

10 files changed

+266
-9
lines changed

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
1818
{
1919
var container = bsc.GetBlobContainerClient("mycontainer");
20-
await container.CreateIfNotExistsAsync();
2120

2221
var blobNameAndContent = Guid.NewGuid().ToString();
2322
await container.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs

+3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
});
1010

1111
var blobs = storage.AddBlobs("blobs");
12+
var blobContainer = blobs.AddBlobContainer("mycontainer");
13+
1214
var queues = storage.AddQueues("queues");
1315

1416
builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
1517
.WithExternalHttpEndpoints()
1618
.WithReference(blobs).WaitFor(blobs)
19+
.WithReference(blobContainer).WaitFor(blobContainer)
1720
.WithReference(queues).WaitFor(queues);
1821

1922
#if !SKIP_DASHBOARD_REFERENCE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Azure;
6+
using Azure.Provisioning;
7+
8+
namespace Aspire.Hosting;
9+
10+
/// <summary>
11+
/// A resource that represents an Azure Blob Storage container.
12+
/// </summary>
13+
/// <param name="name">The name of the resource.</param>
14+
/// <param name="blobStorage">The <see cref="AzureBlobStorageResource"/> that the resource is stored in.</param>
15+
public class AzureBlobStorageContainerResource(string name, AzureBlobStorageResource blobStorage) : Resource(name),
16+
IResourceWithConnectionString,
17+
IResourceWithParent<AzureBlobStorageResource>,
18+
IResourceWithAzureFunctionsConfig
19+
20+
{
21+
/// <summary>
22+
/// Gets the connection string template for the manifest for the Azure Blob Storage container resource.
23+
/// </summary>
24+
public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(Name);
25+
26+
/// <summary>
27+
/// Gets the parent <see cref="AzureBlobStorageResource"/> of this <see cref="AzureBlobStorageContainerResource"/>.
28+
/// </summary>
29+
public AzureBlobStorageResource Parent => blobStorage ?? throw new ArgumentNullException(nameof(blobStorage));
30+
31+
internal void ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
32+
=> Parent.ApplyAzureFunctionsConfiguration(target, connectionName, Name);
33+
34+
/// <summary>
35+
/// Converts the current instance to a provisioning entity.
36+
/// </summary>
37+
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobContainer"/> instance.</returns>
38+
internal global::Azure.Provisioning.Storage.BlobContainer ToProvisioningEntity()
39+
{
40+
global::Azure.Provisioning.Storage.BlobContainer blobContainer = new(Infrastructure.NormalizeBicepIdentifier(Name));
41+
42+
if (Name is not null)
43+
{
44+
blobContainer.Name = Name;
45+
}
46+
47+
return blobContainer;
48+
}
49+
50+
void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
51+
=> ApplyAzureFunctionsConfiguration(target, connectionName);
52+
}

src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs

+41-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Hosting.ApplicationModel;
5+
using Azure.Provisioning;
56

67
namespace Aspire.Hosting.Azure;
78

@@ -15,6 +16,8 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
1516
IResourceWithParent<AzureStorageResource>,
1617
IResourceWithAzureFunctionsConfig
1718
{
19+
internal List<AzureBlobStorageContainerResource> BlobContainers { get; } = [];
20+
1821
/// <summary>
1922
/// Gets the parent AzureStorageResource of this AzureBlobStorageResource.
2023
/// </summary>
@@ -26,7 +29,25 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
2629
public ReferenceExpression ConnectionStringExpression =>
2730
Parent.GetBlobConnectionString();
2831

29-
void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
32+
internal ReferenceExpression GetConnectionString(string? blobContainerName)
33+
{
34+
if (string.IsNullOrEmpty(blobContainerName))
35+
{
36+
return ConnectionStringExpression;
37+
}
38+
39+
var builder = new ReferenceExpressionBuilder();
40+
builder.Append($"{ConnectionStringExpression}");
41+
42+
if (!string.IsNullOrEmpty(blobContainerName))
43+
{
44+
builder.Append($"/{blobContainerName}");
45+
}
46+
47+
return builder.Build();
48+
}
49+
50+
internal void ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName, string? blobContainerName = null)
3051
{
3152
if (Parent.IsEmulator)
3253
{
@@ -42,10 +63,29 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction
4263
// uses the queue service for its internal bookkeeping on blob triggers.
4364
target[$"{connectionName}__blobServiceUri"] = Parent.BlobEndpoint;
4465
target[$"{connectionName}__queueServiceUri"] = Parent.QueueEndpoint;
66+
4567
// Injected to support Aspire client integration for Azure Storage.
4668
// We don't inject the queue resource here since we on;y want it to
4769
// be accessible by the Functions host.
4870
target[$"{AzureStorageResource.BlobsConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.BlobEndpoint;
71+
72+
if (blobContainerName is not null)
73+
{
74+
target[$"{AzureStorageResource.BlobsConnectionKeyPrefix}__{connectionName}__BlobContainerName"] = blobContainerName;
75+
}
4976
}
5077
}
78+
79+
/// <summary>
80+
/// Converts the current instance to a provisioning entity.
81+
/// </summary>
82+
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobService"/> instance.</returns>
83+
internal global::Azure.Provisioning.Storage.BlobService ToProvisioningEntity()
84+
{
85+
global::Azure.Provisioning.Storage.BlobService service = new(Infrastructure.NormalizeBicepIdentifier(Name));
86+
return service;
87+
}
88+
89+
void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
90+
=> ApplyAzureFunctionsConfiguration(target, connectionName);
5191
}

src/Aspire.Hosting.Azure.Storage/AzureStorageEmulatorConnectionString.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal static class AzureStorageEmulatorConnectionString
1212

1313
private static void AppendEndpointExpression(ReferenceExpressionBuilder builder, string key, EndpointReference endpoint)
1414
{
15-
builder.Append($"{key}=http://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)}/devstoreaccount1;");
15+
builder.Append($"{key}=http://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)}/devstoreaccount1");
1616
}
1717

1818
public static ReferenceExpression Create(EndpointReference? blobEndpoint = null, EndpointReference? queueEndpoint = null, EndpointReference? tableEndpoint = null)

src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs

+72-3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ public static IResourceBuilder<AzureStorageResource> AddAzureStorage(this IDistr
8181
infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.QueueUri });
8282
infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.TableUri });
8383

84+
var azureResource = (AzureStorageResource)infrastructure.AspireResource;
85+
86+
foreach (var blobStorageResources in azureResource.Blobs)
87+
{
88+
foreach (var blobContainer in blobStorageResources.BlobContainers)
89+
{
90+
var cdkBlobContainer = blobContainer.ToProvisioningEntity();
91+
cdkBlobContainer.Parent = blobs;
92+
infrastructure.Add(cdkBlobContainer);
93+
}
94+
}
95+
8496
// We need to output name to externalize role assignments.
8597
infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = storageAccount.Name });
8698
};
@@ -126,15 +138,36 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
126138
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (@event, ct) =>
127139
{
128140
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
129-
130-
if (connectionString == null)
141+
if (connectionString is null)
131142
{
132-
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
143+
throw new DistributedApplicationException($"BeforeResourceStartedEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
133144
}
134145

135146
blobServiceClient = CreateBlobServiceClient(connectionString);
136147
});
137148

149+
builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
150+
{
151+
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
152+
if (connectionString is null)
153+
{
154+
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
155+
}
156+
157+
if (blobServiceClient is null)
158+
{
159+
throw new DistributedApplicationException($"BlobServiceClient was not created for the '{builder.Resource.Name}' resource.");
160+
}
161+
162+
foreach (var blobStorageResources in builder.Resource.Blobs)
163+
{
164+
foreach (var blobContainer in blobStorageResources.BlobContainers)
165+
{
166+
await blobServiceClient.GetBlobContainerClient(blobContainer.Name).CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
167+
}
168+
}
169+
});
170+
138171
var healthCheckKey = $"{builder.Resource.Name}_check";
139172

140173
builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp =>
@@ -281,6 +314,26 @@ public static IResourceBuilder<AzureBlobStorageResource> AddBlobs(this IResource
281314
ArgumentException.ThrowIfNullOrEmpty(name);
282315

283316
var resource = new AzureBlobStorageResource(name, builder.Resource);
317+
builder.Resource.Blobs.Add(resource);
318+
319+
return builder.ApplicationBuilder.AddResource(resource);
320+
}
321+
322+
/// <summary>
323+
/// Creates a builder for the <see cref="AzureBlobStorageContainerResource"/> which can be referenced to get the Azure Storage blob container endpoint for the storage account.
324+
/// </summary>
325+
/// <param name="builder">The <see cref="IResourceBuilder{T}"/> for <see cref="AzureBlobStorageResource"/>/</param>
326+
/// <param name="name">The name of the resource.</param>
327+
/// <returns>An <see cref="IResourceBuilder{T}"/> for the <see cref="AzureBlobStorageContainerResource"/>.</returns>
328+
public static IResourceBuilder<AzureBlobStorageContainerResource> AddBlobContainer(this IResourceBuilder<AzureBlobStorageResource> builder, [ResourceName] string name)
329+
{
330+
ArgumentNullException.ThrowIfNull(builder);
331+
ArgumentException.ThrowIfNullOrEmpty(name);
332+
333+
AzureBlobStorageContainerResource resource = new(name, builder.Resource);
334+
335+
builder.Resource.BlobContainers.Add(resource);
336+
284337
return builder.ApplicationBuilder.AddResource(resource);
285338
}
286339

@@ -314,6 +367,22 @@ public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResour
314367
return builder.ApplicationBuilder.AddResource(resource);
315368
}
316369

370+
/// <summary>
371+
/// Allows setting the properties of an Azure Blob Storage container resource.
372+
/// </summary>
373+
/// <param name="builder">The Azure Blob Storage container resource builder.</param>
374+
/// <param name="configure">A method that can be used for customizing the <see cref="AzureBlobStorageContainerResource"/>.</param>
375+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
376+
public static IResourceBuilder<AzureBlobStorageContainerResource> WithProperties(this IResourceBuilder<AzureBlobStorageContainerResource> builder, Action<AzureBlobStorageContainerResource> configure)
377+
{
378+
ArgumentNullException.ThrowIfNull(builder);
379+
ArgumentNullException.ThrowIfNull(configure);
380+
381+
configure(builder.Resource);
382+
383+
return builder;
384+
}
385+
317386
/// <summary>
318387
/// Assigns the specified roles to the given resource, granting it the necessary permissions
319388
/// on the target Azure Storage account. This replaces the default role assignments for the resource.

src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ namespace Aspire.Hosting.Azure;
1515
public class AzureStorageResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
1616
: AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithAzureFunctionsConfig
1717
{
18+
internal const string BlobsConnectionKeyPrefix = "Aspire__Azure__Storage__Blobs";
19+
internal const string QueuesConnectionKeyPrefix = "Aspire__Azure__Storage__Queues";
20+
internal const string TablesConnectionKeyPrefix = "Aspire__Azure__Data__Tables";
21+
1822
private EndpointReference EmulatorBlobEndpoint => new(this, "blob");
1923
private EndpointReference EmulatorQueueEndpoint => new(this, "queue");
2024
private EndpointReference EmulatorTableEndpoint => new(this, "table");
2125

22-
internal const string BlobsConnectionKeyPrefix = "Aspire__Azure__Storage__Blobs";
23-
internal const string QueuesConnectionKeyPrefix = "Aspire__Azure__Storage__Queues";
24-
internal const string TablesConnectionKeyPrefix = "Aspire__Azure__Data__Tables";
26+
internal List<AzureBlobStorageResource> Blobs { get; } = [];
2527

2628
/// <summary>
2729
/// Gets the "blobEndpoint" output reference from the bicep template for the Azure Storage resource.

tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs

+36
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,40 @@ public async Task VerifyAzureStorageEmulatorResource()
8888
var downloadResult = (await blobClient.DownloadContentAsync()).Value;
8989
Assert.Equal("testValue", downloadResult.Content.ToString());
9090
}
91+
92+
[Fact]
93+
[RequiresDocker]
94+
public async Task VerifyAzureStorageEmulatorBlobContainer()
95+
{
96+
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
97+
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
98+
var blobs = storage.AddBlobs("BlobConnection");
99+
var blobContainer = blobs.AddBlobContainer("testblobcontainer");
100+
101+
using var app = builder.Build();
102+
await app.StartAsync();
103+
104+
var hb = Host.CreateApplicationBuilder();
105+
hb.Configuration["ConnectionStrings:BlobConnection"] = await blobs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
106+
hb.AddAzureBlobClient("BlobConnection");
107+
108+
using var host = hb.Build();
109+
await host.StartAsync();
110+
111+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
112+
await rns.WaitForResourceHealthyAsync(blobContainer.Resource.Name, CancellationToken.None);
113+
114+
var serviceClient = host.Services.GetRequiredService<BlobServiceClient>();
115+
var blobContainerClient = serviceClient.GetBlobContainerClient("testblobcontainer");
116+
117+
var exists = await blobContainerClient.ExistsAsync();
118+
119+
var blobNameAndContent = Guid.NewGuid().ToString();
120+
var response = await blobContainerClient.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
121+
122+
var blobClient = blobContainerClient.GetBlobClient(blobNameAndContent);
123+
124+
var downloadResult = (await blobClient.DownloadContentAsync()).Value;
125+
Assert.Equal(blobNameAndContent, downloadResult.Content.ToString());
126+
}
91127
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@description('The location for the resource(s) to be deployed.')
2+
param location string = resourceGroup().location
3+
4+
resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
5+
name: take('storage${uniqueString(resourceGroup().id)}', 24)
6+
kind: 'StorageV2'
7+
location: location
8+
sku: {
9+
name: 'Standard_GRS'
10+
}
11+
properties: {
12+
accessTier: 'Hot'
13+
allowSharedKeyAccess: false
14+
minimumTlsVersion: 'TLS1_2'
15+
networkAcls: {
16+
defaultAction: 'Allow'
17+
}
18+
}
19+
tags: {
20+
'aspire-resource-name': 'storage'
21+
}
22+
}
23+
24+
resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = {
25+
name: 'default'
26+
parent: storage
27+
}
28+
29+
resource my_blob_container 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = {
30+
name: 'my-blob-container'
31+
parent: blobs
32+
}
33+
34+
output blobEndpoint string = storage.properties.primaryEndpoints.blob
35+
36+
output queueEndpoint string = storage.properties.primaryEndpoints.queue
37+
38+
output tableEndpoint string = storage.properties.primaryEndpoints.table
39+
40+
output name string = storage.name

tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs

+16
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,20 @@ public async Task AddAzureStorage_RunAsEmulator_SetSkipApiVersionCheck()
163163

164164
Assert.Contains("--skipApiVersionCheck", args);
165165
}
166+
167+
[Fact]
168+
public async Task ResourceNamesBicepValid()
169+
{
170+
using var builder = TestDistributedApplicationBuilder.Create();
171+
var storage = builder.AddAzureStorage("storage");
172+
173+
var blobs = storage.AddBlobs("myblobs");
174+
var blob = blobs.AddBlobContainer("my-blob-container");
175+
var queues = storage.AddQueues("myqueues");
176+
var tables = storage.AddTables("mytables");
177+
178+
var manifest = await AzureManifestUtils.GetManifestWithBicep(storage.Resource);
179+
180+
await Verifier.Verify(manifest.BicepText, extension: "bicep");
181+
}
166182
}

0 commit comments

Comments
 (0)