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
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
Expand Up @@ -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}"
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Copy link
Member Author

Choose a reason for hiding this comment

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

Building new API and experimenting is horribly difficult because of these (and many more) "warnings as errors".
I'll revert this when I get to the finish line.

Copy link
Member Author

Choose a reason for hiding this comment

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

Opps, I forgot to rollback this. I'll fix it in a follow up.

Copy link
Member

Choose a reason for hiding this comment

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

Did you revert this?

Copy link
Member Author

Choose a reason for hiding this comment

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

</PropertyGroup>

<!-- OS/Architecture properties for local development resources -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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)
Expand Down
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
Expand Up @@ -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)
Expand All @@ -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
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Up @@ -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
Expand Down
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
Member 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
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 All @@ -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>
Expand All @@ -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)
Expand All @@ -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
Expand Up @@ -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();
Expand All @@ -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