Skip to content

AzureStorage auto create blob containers #9008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 7, 2025
Merged
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Instructions for GitHub and VisualStudio Copilot
### https://github.blog/changelog/2025-01-21-custom-repository-instructions-are-now-available-for-copilot-on-github-com-public-preview/


## General

* Make only high confidence suggestions when reviewing code changes.
* Always use the latest version C#, currently C# 13 features.
* Files must have CRLF line endings.

## Formatting

* Apply code-formatting style defined in `.editorconfig`.
* Prefer file-scoped namespace declarations and single-line using directives.
* Insert a newline before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.).
* Ensure that the final return statement of a method is on its own line.
* Use pattern matching and switch expressions wherever possible.
* Use `nameof` instead of string literals when referring to member names.

### Nullable Reference Types

* Declare variables non-nullable, and check for `null` at entry points.
* Always use `is null` or `is not null` instead of `== null` or `!= null`.
* Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.


### Testing

* We use xUnit SDK v3 with Microsoft.Testing.Platform (https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro)
* Do not emit "Act", "Arrange" or "Assert" comments.
* We do not use any mocking framework at the moment. Use NSubstitute, if necessary. Never use Moq.
* Use "snake_case" for test method names but keep the original method under test intact.
For example: when adding a test for methond "MethondToTest" instead of "MethondToTest_ShouldReturnSummarisedIssues" use "MethondToTest_should_return_summarised_issues".
2 changes: 0 additions & 2 deletions Aspire.sln
Original file line number Diff line number Diff line change
@@ -661,8 +661,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Npgsql.EntityF
EndProject
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
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}"
EndProject
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}"
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@
<!-- Issue: https://github.com/dotnet/aspire/issues/8488 -->
<!-- xUnit1051: Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive. -->
<!-- TODO: Re-enable and remove this. -->
<NoWarn>$(NoWarn);xUnit1051</NoWarn>
<NoWarn>$(NoWarn);xUnit1051;CS0162;CS1591;CS9113;IDE0059;IDE0051;IDE2000;IDE0005</NoWarn>
</PropertyGroup>

<!-- OS/Architecture properties for local development resources -->
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
var queue = storage.AddQueues("queue");
var blob = storage.AddBlobs("blob");
var myBlobContainer = blob.AddBlobContainer("myblobcontainer");

var eventHub = builder.AddAzureEventHubs("eventhubs")
.RunAsEmulator()
.AddHub("myhub");
@@ -20,6 +22,7 @@
var funcApp = builder.AddAzureFunctionsProject<Projects.AzureFunctionsEndToEnd_Functions>("funcapp")
.WithExternalHttpEndpoints()
.WithReference(eventHub).WaitFor(eventHub)
.WithReference(myBlobContainer).WaitFor(myBlobContainer)
#if !SKIP_UNSTABLE_EMULATORS
.WithReference(serviceBus).WaitFor(serviceBus)
.WithReference(cosmosDb).WaitFor(cosmosDb)
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

namespace AzureFunctionsEndToEnd.Functions;

public class MyAzureBlobTrigger(ILogger<MyAzureBlobTrigger> logger)
public class MyAzureBlobTrigger(ILogger<MyAzureBlobTrigger> logger, BlobContainerClient containerClient)
{
[Function(nameof(MyAzureBlobTrigger))]
[BlobOutput("test-files/{name}.txt", Connection = "blob")]
public string Run([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString)
public async Task<string> RunAsync([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString, FunctionContext context)
{
logger.LogInformation("C# blob trigger function invoked with {message}...", triggerString);
var blobName = (string)context.BindingContext.BindingData["name"]!;
await containerClient.UploadBlobAsync(blobName, new BinaryData(triggerString));

logger.LogInformation("C# blob trigger function invoked for 'blobs/{source}' with {message}...", blobName, triggerString);
return triggerString.ToUpper();
}
}

Original file line number Diff line number Diff line change
@@ -22,7 +22,8 @@ public class MyHttpTrigger(
#endif
EventHubProducerClient eventHubProducerClient,
QueueServiceClient queueServiceClient,
BlobServiceClient blobServiceClient)
BlobServiceClient blobServiceClient,
BlobContainerClient blobContainerClient)
{
[Function("injected-resources")]
public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
@@ -35,6 +36,7 @@ public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] Ht
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected EventHubProducerClient namespace: {eventHubProducerClient.FullyQualifiedNamespace}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected QueueServiceClient URI: {queueServiceClient.Uri}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobServiceClient URI: {blobServiceClient.Uri}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobContainerClient URI: {blobContainerClient.Uri}");
return Results.Text(stringBuilder.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
builder.AddServiceDefaults();
builder.AddAzureQueueClient("queue");
builder.AddAzureBlobClient("blob");
builder.AddAzureBlobContainerClient("myblobcontainer");
builder.AddAzureEventHubProducerClient("myhub");
#if !SKIP_UNSTABLE_EMULATORS
builder.AddAzureServiceBusClient("messaging");
Original file line number Diff line number Diff line change
@@ -9,27 +9,26 @@
builder.AddServiceDefaults();

builder.AddAzureBlobClient("blobs");
builder.AddKeyedAzureBlobContainerClient("foocontainer");

builder.AddAzureQueueClient("queues");

var app = builder.Build();

app.MapDefaultEndpoints();
app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc) =>
{
var container = bsc.GetBlobContainerClient("mycontainer");
await container.CreateIfNotExistsAsync();

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

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

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

await foreach (var blob in blobs)
{
blobNames.Add(blob.Name);
}
await ReadBlobsAsync(directContainerClient, blobNames);
await ReadBlobsAsync(keyedContainerClient1, blobNames);

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

app.Run();

static async Task ReadBlobsAsync(BlobContainerClient containerClient, List<string> output)
{
output.Add(containerClient.Uri.ToString());
var blobs = containerClient.GetBlobsAsync();
await foreach (var blob in blobs)
{
output.Add(blob.Name);
}
}
Original file line number Diff line number Diff line change
@@ -9,11 +9,22 @@
});

var blobs = storage.AddBlobs("blobs");
blobs.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1");
blobs.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2");

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

var storage2 = builder.AddAzureStorage("storage2").RunAsEmulator(container =>
{
container.WithDataBindMount();
});
var blobs2 = storage2.AddBlobs("blobs2");
var blobContainer2 = blobs2.AddBlobContainer("foocontainer", blobContainerName: "foo-container");

builder.AddProject<Projects.AzureStorageEndToEnd_ApiService>("api")
.WithExternalHttpEndpoints()
.WithReference(blobs).WaitFor(blobs)
.WithReference(blobContainer2).WaitFor(blobContainer2)
.WithReference(queues).WaitFor(queues);

#if !SKIP_DASHBOARD_REFERENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Azure.Provisioning;

namespace Aspire.Hosting;

/// <summary>
/// A resource that represents an Azure Blob Storage container.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="blobContainerName">The name of the blob container.</param>
/// <param name="parent">The <see cref="AzureBlobStorageResource"/> that the resource is stored in.</param>
public class AzureBlobStorageContainerResource(string name, string blobContainerName, AzureBlobStorageResource parent) : Resource(name),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this implement IResourceWithAzureFunctionsConfig? cc @captainsafia

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My tests (per @captainsafia's guidance) didn't suggest any AF-specific implementations were necessary. As far as my limited knowledge goes, the functions require connections to the storage but not individual components within.

IResourceWithConnectionString,
IResourceWithParent<AzureBlobStorageResource>
{
/// <summary>
/// Gets the blob container name.
/// </summary>
public string BlobContainerName { get; } = ThrowIfNullOrEmpty(blobContainerName);

/// <summary>
/// Gets the connection string template for the manifest for the Azure Blob Storage container resource.
/// </summary>
public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(BlobContainerName);

/// <summary>
/// Gets the parent <see cref="AzureBlobStorageResource"/> of this <see cref="AzureBlobStorageContainerResource"/>.
/// </summary>
public AzureBlobStorageResource Parent => parent ?? throw new ArgumentNullException(nameof(parent));

/// <summary>
/// Converts the current instance to a provisioning entity.
/// </summary>
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobContainer"/> instance.</returns>
internal global::Azure.Provisioning.Storage.BlobContainer ToProvisioningEntity()
{
global::Azure.Provisioning.Storage.BlobContainer blobContainer = new(Infrastructure.NormalizeBicepIdentifier(Name))
{
Name = BlobContainerName
};

return blobContainer;
}

private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
return argument;
}
}
34 changes: 34 additions & 0 deletions src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs
Original file line number Diff line number Diff line change
@@ -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;

@@ -15,6 +16,10 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
IResourceWithParent<AzureStorageResource>,
IResourceWithAzureFunctionsConfig
{
// NOTE: if ever these contants are changed, the AzureBlobStorageContainerSettings in Aspire.Azure.Storage.Blobs class should be updated as well.
private const string Endpoint = nameof(Endpoint);
private const string ContainerName = nameof(ContainerName);

/// <summary>
/// Gets the parent AzureStorageResource of this AzureBlobStorageResource.
/// </summary>
@@ -26,6 +31,24 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage)
public ReferenceExpression ConnectionStringExpression =>
Parent.GetBlobConnectionString();

internal ReferenceExpression GetConnectionString(string? blobContainerName)
{
if (string.IsNullOrEmpty(blobContainerName))
{
return ConnectionStringExpression;
}

ReferenceExpressionBuilder builder = new();
builder.Append($"{Endpoint}=\"{ConnectionStringExpression}\";");

if (!string.IsNullOrEmpty(blobContainerName))
{
builder.Append($"{ContainerName}={blobContainerName};");
}

return builder.Build();
}

void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
{
if (Parent.IsEmulator)
@@ -42,10 +65,21 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction
// uses the queue service for its internal bookkeeping on blob triggers.
target[$"{connectionName}__blobServiceUri"] = Parent.BlobEndpoint;
target[$"{connectionName}__queueServiceUri"] = Parent.QueueEndpoint;

// Injected to support Aspire client integration for Azure Storage.
// We don't inject the queue resource here since we on;y want it to
// be accessible by the Functions host.
target[$"{AzureStorageResource.BlobsConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.BlobEndpoint;
}
}

/// <summary>
/// Converts the current instance to a provisioning entity.
/// </summary>
/// <returns>A <see cref="global::Azure.Provisioning.Storage.BlobService"/> instance.</returns>
internal global::Azure.Provisioning.Storage.BlobService ToProvisioningEntity()
{
global::Azure.Provisioning.Storage.BlobService service = new(Infrastructure.NormalizeBicepIdentifier(Name));
return service;
}
}
Original file line number Diff line number Diff line change
@@ -10,11 +10,6 @@ internal static class AzureStorageEmulatorConnectionString
// Use defaults from https://learn.microsoft.com/azure/storage/common/storage-configure-connection-string#connect-to-the-emulator-account-using-the-shortcut
private const string ConnectionStringHeader = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;";

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

public static ReferenceExpression Create(EndpointReference? blobEndpoint = null, EndpointReference? queueEndpoint = null, EndpointReference? tableEndpoint = null)
{
var builder = new ReferenceExpressionBuilder();
@@ -34,5 +29,10 @@ public static ReferenceExpression Create(EndpointReference? blobEndpoint = null,
}

return builder.Build();

static void AppendEndpointExpression(ReferenceExpressionBuilder builder, string key, EndpointReference endpoint)
{
builder.Append($"{key}=http://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)}/devstoreaccount1;");
}
}
}
Loading
Oops, something went wrong.