Skip to content

Commit fc2e36e

Browse files
committed
AddAzureBlobContainerClient() and various fixes
1 parent d5db080 commit fc2e36e

15 files changed

+289
-84
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<!-- Issue: https://github.com/dotnet/aspire/issues/8488 -->
3838
<!-- xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive. -->
3939
<!-- TODO: Re-enable and remove this. -->
40-
<NoWarn>$(NoWarn);xUnit1051</NoWarn>
40+
<NoWarn>$(NoWarn);xUnit1051;CS0162;CS1591;CS9113;IDE0059;IDE0051;IDE2000;IDE0005</NoWarn>
4141
</PropertyGroup>
4242

4343
<!-- OS/Architecture properties for local development resources -->

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
builder.AddServiceDefaults();
1010

1111
builder.AddAzureBlobClient("blobs");
12+
builder.AddAzureBlobContainerClient("foocontainer");
13+
1214
builder.AddAzureQueueClient("queues");
1315

1416
var app = builder.Build();
1517

1618
app.MapDefaultEndpoints();
17-
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
19+
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc, BlobContainerClient bcc) =>
1820
{
19-
var container = bsc.GetBlobContainerClient("mycontainer");
21+
var container = bsc.GetBlobContainerClient(blobContainerName: "test-container-1");
2022

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

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@
99
});
1010

1111
var blobs = storage.AddBlobs("blobs");
12-
var blobContainer = blobs.AddBlobContainer("mycontainer");
12+
blobs.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1");
13+
blobs.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2");
1314

1415
var queues = storage.AddQueues("queues");
1516

17+
var storage2 = builder.AddAzureStorage("storage2").RunAsEmulator(container =>
18+
{
19+
container.WithDataBindMount();
20+
});
21+
var blobs2 = storage2.AddBlobs("blobs2");
22+
var blobContainer2 = blobs2.AddBlobContainer("foocontainer", blobContainerName: "foo-container");
23+
1624
builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
1725
.WithExternalHttpEndpoints()
1826
.WithReference(blobs).WaitFor(blobs)
19-
.WithReference(blobContainer).WaitFor(blobContainer)
27+
.WithReference(blobContainer2).WaitFor(blobContainer2)
2028
.WithReference(queues).WaitFor(queues);
2129

2230
#if !SKIP_DASHBOARD_REFERENCE

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class AzureBlobStorageContainerResource(string name, string blobContainer
2828
/// <summary>
2929
/// Gets the connection string template for the manifest for the Azure Blob Storage container resource.
3030
/// </summary>
31-
public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(Name);
31+
public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(BlobContainerName);
3232

3333
/// <summary>
3434
/// Gets the parent <see cref="AzureBlobStorageResource"/> of this <see cref="AzureBlobStorageContainerResource"/>.
@@ -41,7 +41,7 @@ public class AzureBlobStorageContainerResource(string name, string blobContainer
4141
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobContainer"/> instance.</returns>
4242
internal global::Azure.Provisioning.Storage.BlobContainer ToProvisioningEntity()
4343
{
44-
global::Azure.Provisioning.Storage.BlobContainer blobContainer = new(Infrastructure.NormalizeBicepIdentifier(Name))
44+
global::Azure.Provisioning.Storage.BlobContainer blobContainer = new(Infrastructure.NormalizeBicepIdentifier(BlobContainerName))
4545
{
4646
Name = BlobContainerName
4747
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ internal ReferenceExpression GetConnectionString(string? blobContainerName)
4141

4242
if (!string.IsNullOrEmpty(blobContainerName))
4343
{
44-
builder.Append($"/{blobContainerName}");
44+
builder.Append($"ContainerName={blobContainerName};");
4545
}
4646

4747
return builder.Build();

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -148,22 +148,22 @@ public static IResourceBuilder<AzureStorageResource> RunAsEmulator(this IResourc
148148

149149
builder.ApplicationBuilder.Eventing.Subscribe<ResourceReadyEvent>(builder.Resource, async (@event, ct) =>
150150
{
151-
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
152-
if (connectionString is null)
151+
if (blobServiceClient is null)
153152
{
154-
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
153+
throw new DistributedApplicationException($"BlobServiceClient was not created for the '{builder.Resource.Name}' resource.");
155154
}
156155

157-
if (blobServiceClient is null)
156+
var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false);
157+
if (connectionString is null)
158158
{
159-
throw new DistributedApplicationException($"BlobServiceClient was not created for the '{builder.Resource.Name}' resource.");
159+
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{builder.Resource.Name}' resource but the connection string was null.");
160160
}
161161

162162
foreach (var blobStorageResources in builder.Resource.Blobs)
163163
{
164164
foreach (var blobContainer in blobStorageResources.BlobContainers)
165165
{
166-
await blobServiceClient.GetBlobContainerClient(blobContainer.Name).CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
166+
await blobServiceClient.GetBlobContainerClient(blobContainer.BlobContainerName).CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false);
167167
}
168168
}
169169
});
@@ -370,22 +370,6 @@ public static IResourceBuilder<AzureQueueStorageResource> AddQueues(this IResour
370370
return builder.ApplicationBuilder.AddResource(resource);
371371
}
372372

373-
/// <summary>
374-
/// Allows setting the properties of an Azure Blob Storage container resource.
375-
/// </summary>
376-
/// <param name="builder">The Azure Blob Storage container resource builder.</param>
377-
/// <param name="configure">A method that can be used for customizing the <see cref="AzureBlobStorageContainerResource"/>.</param>
378-
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
379-
public static IResourceBuilder<AzureBlobStorageContainerResource> WithProperties(this IResourceBuilder<AzureBlobStorageContainerResource> builder, Action<AzureBlobStorageContainerResource> configure)
380-
{
381-
ArgumentNullException.ThrowIfNull(builder);
382-
ArgumentNullException.ThrowIfNull(configure);
383-
384-
configure(builder.Resource);
385-
386-
return builder;
387-
}
388-
389373
/// <summary>
390374
/// Assigns the specified roles to the given resource, granting it the necessary permissions
391375
/// on the target Azure Storage account. This replaces the default role assignments for the resource.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.Azure.Common;
5+
using Aspire.Azure.Storage.Blobs;
6+
using Azure.Core;
7+
using Azure.Core.Extensions;
8+
using Azure.Storage.Blobs;
9+
using HealthChecks.Azure.Storage.Blobs;
10+
using Microsoft.Extensions.Azure;
11+
using Microsoft.Extensions.Configuration;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Diagnostics.HealthChecks;
14+
15+
namespace Microsoft.Extensions.Hosting;
16+
17+
partial class AspireBlobStorageExtensions
18+
{
19+
private sealed class BlobStorageComponent : AzureComponent<AzureStorageBlobsSettings, BlobServiceClient, BlobClientOptions>
20+
{
21+
protected override IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddClient(
22+
AzureClientFactoryBuilder azureFactoryBuilder, AzureStorageBlobsSettings settings, string connectionName,
23+
string configurationSectionName)
24+
{
25+
return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory<BlobServiceClient, BlobClientOptions>((options, cred) =>
26+
{
27+
var connectionString = settings.ConnectionString;
28+
if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null)
29+
{
30+
throw new InvalidOperationException($"A BlobServiceClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a 'ConnectionString' or 'ServiceUri' in the '{configurationSectionName}' configuration section.");
31+
}
32+
33+
return !string.IsNullOrEmpty(connectionString) ? new BlobServiceClient(connectionString, options) :
34+
cred is not null ? new BlobServiceClient(settings.ServiceUri, cred, options) :
35+
new BlobServiceClient(settings.ServiceUri, options);
36+
}, requiresCredential: false);
37+
}
38+
39+
protected override void BindClientOptionsToConfiguration(IAzureClientBuilder<BlobServiceClient, BlobClientOptions> clientBuilder, IConfiguration configuration)
40+
{
41+
#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works
42+
clientBuilder.ConfigureOptions(options => configuration.Bind(options));
43+
#pragma warning restore IDE0200
44+
}
45+
46+
protected override void BindSettingsToConfiguration(AzureStorageBlobsSettings settings, IConfiguration configuration)
47+
{
48+
configuration.Bind(settings);
49+
}
50+
51+
protected override IHealthCheck CreateHealthCheck(BlobServiceClient client, AzureStorageBlobsSettings settings)
52+
=> new AzureBlobStorageHealthCheck(client);
53+
54+
protected override bool GetHealthCheckEnabled(AzureStorageBlobsSettings settings)
55+
=> !settings.DisableHealthChecks;
56+
57+
protected override TokenCredential? GetTokenCredential(AzureStorageBlobsSettings settings)
58+
=> settings.Credential;
59+
60+
protected override bool GetMetricsEnabled(AzureStorageBlobsSettings settings)
61+
=> false;
62+
63+
protected override bool GetTracingEnabled(AzureStorageBlobsSettings settings)
64+
=> !settings.DisableTracing;
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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 Azure.Storage.Blobs;
5+
using Microsoft.Extensions.Diagnostics.HealthChecks;
6+
7+
namespace Microsoft.Extensions.Hosting;
8+
9+
partial class AspireBlobStorageExtensions
10+
{
11+
partial class BlobStorageContainerComponent
12+
{
13+
/// <summary>
14+
/// Azure Blob Storage container health check.
15+
/// </summary>
16+
/// <param name="blobContainerClient">
17+
/// The <see cref="BlobContainerClient"/> used to perform Azure Blob Storage container operations.
18+
/// Azure SDK recommends treating clients as singletons <see href="https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/"/>,
19+
/// so this should be the exact same instance used by other parts of the application.
20+
/// </param>
21+
private sealed class AzureBlobStorageContainerHealthCheck(BlobContainerClient blobContainerClient) : IHealthCheck
22+
{
23+
private readonly BlobContainerClient _blobServiceClient = blobContainerClient ?? throw new ArgumentNullException(nameof(blobContainerClient));
24+
25+
/// <inheritdoc />
26+
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
27+
{
28+
try
29+
{
30+
await _blobServiceClient.ExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
31+
return HealthCheckResult.Healthy();
32+
}
33+
catch (Exception ex)
34+
{
35+
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
36+
}
37+
}
38+
}
39+
}
40+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.Azure.Common;
5+
using Aspire.Azure.Storage.Blobs;
6+
using Azure.Core;
7+
using Azure.Core.Extensions;
8+
using Azure.Storage.Blobs;
9+
using Microsoft.Extensions.Azure;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Diagnostics.HealthChecks;
13+
14+
namespace Microsoft.Extensions.Hosting;
15+
16+
partial class AspireBlobStorageExtensions
17+
{
18+
private sealed partial class BlobStorageContainerComponent : AzureComponent<AzureBlobStorageContainerSettings, BlobContainerClient, BlobClientOptions>
19+
{
20+
protected override IAzureClientBuilder<BlobContainerClient, BlobClientOptions> AddClient(
21+
AzureClientFactoryBuilder azureFactoryBuilder, AzureBlobStorageContainerSettings settings, string connectionName, string configurationSectionName)
22+
{
23+
return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory<BlobContainerClient, BlobClientOptions>((options, cred) =>
24+
{
25+
if (string.IsNullOrEmpty(settings.BlobContainerName))
26+
{
27+
throw new InvalidOperationException($"The connection string '{connectionName}' does not exist or is missing the container name.");
28+
}
29+
30+
var connectionString = settings.ConnectionString;
31+
if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null)
32+
{
33+
throw new InvalidOperationException($"A BlobServiceClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a 'ConnectionString' or 'ServiceUri' in the '{configurationSectionName}' configuration section.");
34+
}
35+
36+
var blobServiceClient = !string.IsNullOrEmpty(connectionString) ? new BlobServiceClient(connectionString, options) :
37+
cred is not null ? new BlobServiceClient(settings.ServiceUri, cred, options) :
38+
new BlobServiceClient(settings.ServiceUri, options);
39+
40+
var containerClient = blobServiceClient.GetBlobContainerClient(settings.BlobContainerName);
41+
return containerClient;
42+
43+
}, requiresCredential: false);
44+
}
45+
46+
protected override void BindClientOptionsToConfiguration(IAzureClientBuilder<BlobContainerClient, BlobClientOptions> clientBuilder, IConfiguration configuration)
47+
{
48+
#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works
49+
clientBuilder.ConfigureOptions(options => configuration.Bind(options));
50+
#pragma warning restore IDE0200
51+
}
52+
53+
protected override void BindSettingsToConfiguration(AzureBlobStorageContainerSettings settings, IConfiguration configuration)
54+
{
55+
configuration.Bind(settings);
56+
}
57+
58+
protected override IHealthCheck CreateHealthCheck(BlobContainerClient client, AzureBlobStorageContainerSettings settings)
59+
=> new AzureBlobStorageContainerHealthCheck(client);
60+
61+
protected override bool GetHealthCheckEnabled(AzureBlobStorageContainerSettings settings)
62+
=> !settings.DisableHealthChecks;
63+
64+
protected override TokenCredential? GetTokenCredential(AzureBlobStorageContainerSettings settings)
65+
=> settings.Credential;
66+
67+
protected override bool GetMetricsEnabled(AzureBlobStorageContainerSettings settings)
68+
=> false;
69+
70+
protected override bool GetTracingEnabled(AzureBlobStorageContainerSettings settings)
71+
=> !settings.DisableTracing;
72+
}
73+
}

0 commit comments

Comments
 (0)