Skip to content

Commit 495d592

Browse files
authored
AzureStorage auto create blob containers (#9008)
Resolves #5167
1 parent 74a965b commit 495d592

28 files changed

+861
-93
lines changed

.github/copilot-instructions.md

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Instructions for GitHub and VisualStudio Copilot
2+
### https://github.blog/changelog/2025-01-21-custom-repository-instructions-are-now-available-for-copilot-on-github-com-public-preview/
3+
4+
5+
## General
6+
7+
* Make only high confidence suggestions when reviewing code changes.
8+
* Always use the latest version C#, currently C# 13 features.
9+
* Files must have CRLF line endings.
10+
11+
## Formatting
12+
13+
* Apply code-formatting style defined in `.editorconfig`.
14+
* Prefer file-scoped namespace declarations and single-line using directives.
15+
* Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.).
16+
* Ensure that the final return statement of a method is on its own line.
17+
* Use pattern matching and switch expressions wherever possible.
18+
* Use `nameof` instead of string literals when referring to member names.
19+
20+
### Nullable Reference Types
21+
22+
* Declare variables non-nullable, and check for `null` at entry points.
23+
* Always use `is null` or `is not null` instead of `== null` or `!= null`.
24+
* Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
25+
26+
27+
### Testing
28+
29+
* We use xUnit SDK v3 with Microsoft.Testing.Platform (https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro)
30+
* Do not emit "Act", "Arrange" or "Assert" comments.
31+
* We do not use any mocking framework at the moment. Use NSubstitute, if necessary. Never use Moq.
32+
* Use "snake_case" for test method names but keep the original method under test intact.
33+
For example: when adding a test for methond "MethondToTest" instead of "MethondToTest_ShouldReturnSummarisedIssues" use "MethondToTest_should_return_summarised_issues".

Aspire.sln

-2
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Npgsql.EntityF
661661
EndProject
662662
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Components.Common.Tests", "tests\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj", "{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}"
663663
EndProject
664-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
665-
EndProject
666664
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
667665
EndProject
668666
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.AppService", "src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj", "{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}"

Directory.Build.props

+1-1
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/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
44
var queue = storage.AddQueues("queue");
55
var blob = storage.AddBlobs("blob");
6+
var myBlobContainer = blob.AddBlobContainer("myblobcontainer");
7+
68
var eventHub = builder.AddAzureEventHubs("eventhubs")
79
.RunAsEmulator()
810
.AddHub("myhub");
@@ -20,6 +22,7 @@
2022
var funcApp = builder.AddAzureFunctionsProject<Projects.AzureFunctionsEndToEnd_Functions>("funcapp")
2123
.WithExternalHttpEndpoints()
2224
.WithReference(eventHub).WaitFor(eventHub)
25+
.WithReference(myBlobContainer).WaitFor(myBlobContainer)
2326
#if !SKIP_UNSTABLE_EMULATORS
2427
.WithReference(serviceBus).WaitFor(serviceBus)
2528
.WithReference(cosmosDb).WaitFor(cosmosDb)

playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
using Azure.Storage.Blobs;
12
using Microsoft.Azure.Functions.Worker;
23
using Microsoft.Extensions.Logging;
34

45
namespace AzureFunctionsEndToEnd.Functions;
56

6-
public class MyAzureBlobTrigger(ILogger<MyAzureBlobTrigger> logger)
7+
public class MyAzureBlobTrigger(ILogger<MyAzureBlobTrigger> logger, BlobContainerClient containerClient)
78
{
89
[Function(nameof(MyAzureBlobTrigger))]
910
[BlobOutput("test-files/{name}.txt", Connection = "blob")]
10-
public string Run([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString)
11+
public async Task<string> RunAsync([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString, FunctionContext context)
1112
{
12-
logger.LogInformation("C# blob trigger function invoked with {message}...", triggerString);
13+
var blobName = (string)context.BindingContext.BindingData["name"]!;
14+
await containerClient.UploadBlobAsync(blobName, new BinaryData(triggerString));
15+
16+
logger.LogInformation("C# blob trigger function invoked for 'blobs/{source}' with {message}...", blobName, triggerString);
1317
return triggerString.ToUpper();
1418
}
1519
}
16-

playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public class MyHttpTrigger(
2222
#endif
2323
EventHubProducerClient eventHubProducerClient,
2424
QueueServiceClient queueServiceClient,
25-
BlobServiceClient blobServiceClient)
25+
BlobServiceClient blobServiceClient,
26+
BlobContainerClient blobContainerClient)
2627
{
2728
[Function("injected-resources")]
2829
public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
@@ -35,6 +36,7 @@ public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] Ht
3536
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected EventHubProducerClient namespace: {eventHubProducerClient.FullyQualifiedNamespace}");
3637
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected QueueServiceClient URI: {queueServiceClient.Uri}");
3738
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobServiceClient URI: {blobServiceClient.Uri}");
39+
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobContainerClient URI: {blobContainerClient.Uri}");
3840
return Results.Text(stringBuilder.ToString());
3941
}
4042
}

playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
builder.AddServiceDefaults();
77
builder.AddAzureQueueClient("queue");
88
builder.AddAzureBlobClient("blob");
9+
builder.AddAzureBlobContainerClient("myblobcontainer");
910
builder.AddAzureEventHubProducerClient("myhub");
1011
#if !SKIP_UNSTABLE_EMULATORS
1112
builder.AddAzureServiceBusClient("messaging");

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs

+20-11
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,26 @@
99
builder.AddServiceDefaults();
1010

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

1416
var app = builder.Build();
1517

1618
app.MapDefaultEndpoints();
17-
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
18-
{
19-
var container = bsc.GetBlobContainerClient("mycontainer");
20-
await container.CreateIfNotExistsAsync();
2119

20+
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc, [FromKeyedServices("foocontainer")] BlobContainerClient keyedContainerClient1) =>
21+
{
22+
var blobNames = new List<string>();
2223
var blobNameAndContent = Guid.NewGuid().ToString();
23-
await container.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
2424

25-
var blobs = container.GetBlobsAsync();
25+
await keyedContainerClient1.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
2626

27-
var blobNames = new List<string>();
27+
var directContainerClient = bsc.GetBlobContainerClient(blobContainerName: "test-container-1");
28+
await directContainerClient.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
2829

29-
await foreach (var blob in blobs)
30-
{
31-
blobNames.Add(blob.Name);
32-
}
30+
await ReadBlobsAsync(directContainerClient, blobNames);
31+
await ReadBlobsAsync(keyedContainerClient1, blobNames);
3332

3433
var queue = qsc.GetQueueClient("myqueue");
3534
await queue.CreateIfNotExistsAsync();
@@ -39,3 +38,13 @@
3938
});
4039

4140
app.Run();
41+
42+
static async Task ReadBlobsAsync(BlobContainerClient containerClient, List<string> output)
43+
{
44+
output.Add(containerClient.Uri.ToString());
45+
var blobs = containerClient.GetBlobsAsync();
46+
await foreach (var blob in blobs)
47+
{
48+
output.Add(blob.Name);
49+
}
50+
}

playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs

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

1111
var blobs = storage.AddBlobs("blobs");
12+
blobs.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1");
13+
blobs.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2");
14+
1215
var queues = storage.AddQueues("queues");
1316

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+
1424
builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
1525
.WithExternalHttpEndpoints()
1626
.WithReference(blobs).WaitFor(blobs)
27+
.WithReference(blobContainer2).WaitFor(blobContainer2)
1728
.WithReference(queues).WaitFor(queues);
1829

1930
#if !SKIP_DASHBOARD_REFERENCE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 System.Diagnostics.CodeAnalysis;
5+
using System.Runtime.CompilerServices;
6+
using Aspire.Hosting.ApplicationModel;
7+
using Aspire.Hosting.Azure;
8+
using Azure.Provisioning;
9+
10+
namespace Aspire.Hosting;
11+
12+
/// <summary>
13+
/// A resource that represents an Azure Blob Storage container.
14+
/// </summary>
15+
/// <param name="name">The name of the resource.</param>
16+
/// <param name="blobContainerName">The name of the blob container.</param>
17+
/// <param name="parent">The <see cref="AzureBlobStorageResource"/> that the resource is stored in.</param>
18+
public class AzureBlobStorageContainerResource(string name, string blobContainerName, AzureBlobStorageResource parent) : Resource(name),
19+
IResourceWithConnectionString,
20+
IResourceWithParent<AzureBlobStorageResource>
21+
{
22+
/// <summary>
23+
/// Gets the blob container name.
24+
/// </summary>
25+
public string BlobContainerName { get; } = ThrowIfNullOrEmpty(blobContainerName);
26+
27+
/// <summary>
28+
/// Gets the connection string template for the manifest for the Azure Blob Storage container resource.
29+
/// </summary>
30+
public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(BlobContainerName);
31+
32+
/// <summary>
33+
/// Gets the parent <see cref="AzureBlobStorageResource"/> of this <see cref="AzureBlobStorageContainerResource"/>.
34+
/// </summary>
35+
public AzureBlobStorageResource Parent => parent ?? throw new ArgumentNullException(nameof(parent));
36+
37+
/// <summary>
38+
/// Converts the current instance to a provisioning entity.
39+
/// </summary>
40+
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobContainer"/> instance.</returns>
41+
internal global::Azure.Provisioning.Storage.BlobContainer ToProvisioningEntity()
42+
{
43+
global::Azure.Provisioning.Storage.BlobContainer blobContainer = new(Infrastructure.NormalizeBicepIdentifier(Name))
44+
{
45+
Name = BlobContainerName
46+
};
47+
48+
return blobContainer;
49+
}
50+
51+
private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
52+
{
53+
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
54+
return argument;
55+
}
56+
}

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

+34
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,10 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
1516
IResourceWithParent<AzureStorageResource>,
1617
IResourceWithAzureFunctionsConfig
1718
{
19+
// NOTE: if ever these contants are changed, the AzureBlobStorageContainerSettings in Aspire.Azure.Storage.Blobs class should be updated as well.
20+
private const string Endpoint = nameof(Endpoint);
21+
private const string ContainerName = nameof(ContainerName);
22+
1823
/// <summary>
1924
/// Gets the parent AzureStorageResource of this AzureBlobStorageResource.
2025
/// </summary>
@@ -26,6 +31,24 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
2631
public ReferenceExpression ConnectionStringExpression =>
2732
Parent.GetBlobConnectionString();
2833

34+
internal ReferenceExpression GetConnectionString(string? blobContainerName)
35+
{
36+
if (string.IsNullOrEmpty(blobContainerName))
37+
{
38+
return ConnectionStringExpression;
39+
}
40+
41+
ReferenceExpressionBuilder builder = new();
42+
builder.Append($"{Endpoint}=\"{ConnectionStringExpression}\";");
43+
44+
if (!string.IsNullOrEmpty(blobContainerName))
45+
{
46+
builder.Append($"{ContainerName}={blobContainerName};");
47+
}
48+
49+
return builder.Build();
50+
}
51+
2952
void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
3053
{
3154
if (Parent.IsEmulator)
@@ -42,10 +65,21 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction
4265
// uses the queue service for its internal bookkeeping on blob triggers.
4366
target[$"{connectionName}__blobServiceUri"] = Parent.BlobEndpoint;
4467
target[$"{connectionName}__queueServiceUri"] = Parent.QueueEndpoint;
68+
4569
// Injected to support Aspire client integration for Azure Storage.
4670
// We don't inject the queue resource here since we on;y want it to
4771
// be accessible by the Functions host.
4872
target[$"{AzureStorageResource.BlobsConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.BlobEndpoint;
4973
}
5074
}
75+
76+
/// <summary>
77+
/// Converts the current instance to a provisioning entity.
78+
/// </summary>
79+
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobService"/> instance.</returns>
80+
internal global::Azure.Provisioning.Storage.BlobService ToProvisioningEntity()
81+
{
82+
global::Azure.Provisioning.Storage.BlobService service = new(Infrastructure.NormalizeBicepIdentifier(Name));
83+
return service;
84+
}
5185
}

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ internal static class AzureStorageEmulatorConnectionString
1010
// Use defaults from https://learn.microsoft.com/azure/storage/common/storage-configure-connection-string#connect-to-the-emulator-account-using-the-shortcut
1111
private const string ConnectionStringHeader = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;";
1212

13-
private static void AppendEndpointExpression(ReferenceExpressionBuilder builder, string key, EndpointReference endpoint)
14-
{
15-
builder.Append($"{key}=http://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)}/devstoreaccount1;");
16-
}
17-
1813
public static ReferenceExpression Create(EndpointReference? blobEndpoint = null, EndpointReference? queueEndpoint = null, EndpointReference? tableEndpoint = null)
1914
{
2015
var builder = new ReferenceExpressionBuilder();
@@ -34,5 +29,10 @@ public static ReferenceExpression Create(EndpointReference? blobEndpoint = null,
3429
}
3530

3631
return builder.Build();
32+
33+
static void AppendEndpointExpression(ReferenceExpressionBuilder builder, string key, EndpointReference endpoint)
34+
{
35+
builder.Append($"{key}=http://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)}/devstoreaccount1;");
36+
}
3737
}
3838
}

0 commit comments

Comments
 (0)