Skip to content
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
15 changes: 8 additions & 7 deletions src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs
Original file line number Diff line number Diff line change
@@ -16,10 +16,6 @@ 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>
@@ -39,13 +35,18 @@ internal ReferenceExpression GetConnectionString(string? blobContainerName)
}

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

if (!string.IsNullOrEmpty(blobContainerName))
if (Parent.IsEmulator)
{
builder.AppendFormatted(ConnectionStringExpression);
}
else
{
builder.Append($"{ContainerName}={blobContainerName};");
builder.Append($"Endpoint={ConnectionStringExpression}");
}

builder.Append($";ContainerName={blobContainerName}");

return builder.Build();
}

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 System.Data.Common;
using System.Text.RegularExpressions;
using Aspire.Azure.Common;

namespace Aspire.Azure.Storage.Blobs;
@@ -11,6 +12,9 @@ namespace Aspire.Azure.Storage.Blobs;
/// </summary>
public sealed partial class AzureBlobStorageContainerSettings : AzureStorageBlobsSettings, IConnectionStringSettings
{
[GeneratedRegex(@"(?i)ContainerName\s*=\s*([^;]+);?", RegexOptions.IgnoreCase)]
private static partial Regex ContainerNameRegex();

/// <summary>
/// Gets or sets the name of the blob container.
/// </summary>
@@ -23,15 +27,31 @@ void IConnectionStringSettings.ParseConnectionString(string? connectionString)
return;
}

// NOTE: if ever these contants are changed, the AzureBlobStorageResource in Aspire.Hosting.Azure.Storage class should be updated as well.
const string Endpoint = nameof(Endpoint);
const string ContainerName = nameof(ContainerName);

DbConnectionStringBuilder builder = new() { ConnectionString = connectionString };
if (builder.TryGetValue(Endpoint, out var endpoint) && builder.TryGetValue(ContainerName, out var containerName))

if (builder.TryGetValue("ContainerName", out var containerName))
{
BlobContainerName = containerName?.ToString();

// Remove the ContainerName property from the connection string as BlobServiceClient would fail to parse it.
connectionString = ContainerNameRegex().Replace(connectionString, "");

// NB: we can't remove ContainerName by using the DbConnectionStringBuilder as it would escape the AccountKey value
// when the connection string is built and BlobServiceClient doesn't support escape sequences.
}

// Connection string built from a URI? e.g., Endpoint=https://{account_name}.blob.core.windows.net;ContainerName=...;
if (builder.TryGetValue("Endpoint", out var endpoint) && endpoint is string)
{
if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var uri))
{
ServiceUri = uri;
}
}
else
{
ConnectionString = endpoint.ToString();
BlobContainerName = containerName.ToString();
// Otherwise preserve the existing connection string
ConnectionString = connectionString;
}
}
}
Original file line number Diff line number Diff line change
@@ -52,16 +52,18 @@ public class AzureStorageBlobsSettings : IConnectionStringSettings

void IConnectionStringSettings.ParseConnectionString(string? connectionString)
{
if (!string.IsNullOrEmpty(connectionString))
if (string.IsNullOrEmpty(connectionString))
{
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
ServiceUri = uri;
}
else
{
ConnectionString = connectionString;
}
return;
}

if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
ServiceUri = uri;
}
else
{
ConnectionString = connectionString;
}
}
}
Original file line number Diff line number Diff line change
@@ -9,22 +9,7 @@ namespace Aspire.Hosting.Azure.Tests;

public class AzureBlobStorageContainerSettingsTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(";")]
[InlineData("Endpoint=https://example.blob.core.windows.net;")]
[InlineData("ContainerName=my-container;")]
[InlineData("Endpoint=https://example.blob.core.windows.net;ExtraParam=value;")]
public void ParseConnectionString_invalid_input(string? connectionString)
{
var settings = new AzureBlobStorageContainerSettings();

((IConnectionStringSettings)settings).ParseConnectionString(connectionString);

Assert.Null(settings.ConnectionString);
Assert.Null(settings.BlobContainerName);
}
private const string EmulatorConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1";

[Fact]
public void ParseConnectionString_invalid_input_results_in_AE()
@@ -40,13 +25,42 @@ public void ParseConnectionString_invalid_input_results_in_AE()
[InlineData("Endpoint=https://example.blob.core.windows.net;ContainerName=my-container;ExtraParam=value")]
[InlineData("endpoint=https://example.blob.core.windows.net;containername=my-container")]
[InlineData("ENDPOINT=https://example.blob.core.windows.net;CONTAINERNAME=my-container")]
public void ParseConnectionString_valid_input(string connectionString)
[InlineData("Endpoint=\"https://example.blob.core.windows.net\";ContainerName=\"my-container\"")]
public void ParseConnectionString_With_ServiceUri(string connectionString)
{
var settings = new AzureBlobStorageContainerSettings();

((IConnectionStringSettings)settings).ParseConnectionString(connectionString);

Assert.Equal("https://example.blob.core.windows.net/", settings.ServiceUri?.ToString());
Assert.Equal("my-container", settings.BlobContainerName);
}

[Theory]
[InlineData($"{EmulatorConnectionString};ContainerName=my-container")]
[InlineData($"{EmulatorConnectionString};ContainerName=\"my-container\"")]
public void ParseConnectionString_With_ConnectionString(string connectionString)
{
var settings = new AzureBlobStorageContainerSettings();

((IConnectionStringSettings)settings).ParseConnectionString(connectionString);

Assert.Contains(EmulatorConnectionString, settings.ConnectionString, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("ContainerName", settings.ConnectionString, StringComparison.OrdinalIgnoreCase);
Assert.Equal("my-container", settings.BlobContainerName);
Assert.Null(settings.ServiceUri);
}

[Theory]
[InlineData($"Endpoint=not-a-uri;ContainerName=my-container")]
public void ParseConnectionString_With_NotAUri(string connectionString)
{
var settings = new AzureBlobStorageContainerSettings();

((IConnectionStringSettings)settings).ParseConnectionString(connectionString);

Assert.Equal("https://example.blob.core.windows.net", settings.ConnectionString);
Assert.True(string.IsNullOrEmpty(settings.ConnectionString));
Assert.Equal("my-container", settings.BlobContainerName);
Assert.Null(settings.ServiceUri);
}
}
Original file line number Diff line number Diff line change
@@ -110,22 +110,29 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForBlobContainersBlocksDepe
[RequiresDocker]
public async Task VerifyAzureStorageEmulatorResource()
{
var blobsResourceName = "BlobConnection";
var blobContainerName = "my-container";

using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
var storage = builder.AddAzureStorage("storage").RunAsEmulator().AddBlobs("BlobConnection");
var blobs = builder.AddAzureStorage("storage").RunAsEmulator().AddBlobs(blobsResourceName);
var container = blobs.AddBlobContainer(blobContainerName);

using var app = builder.Build();
await app.StartAsync();

var hb = Host.CreateApplicationBuilder();
hb.Configuration["ConnectionStrings:BlobConnection"] = await storage.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
hb.AddAzureBlobClient("BlobConnection");
hb.Configuration[$"ConnectionStrings:{blobsResourceName}"] = await blobs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
hb.Configuration[$"ConnectionStrings:{blobContainerName}"] = await container.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
hb.AddAzureBlobClient(blobsResourceName);
hb.AddAzureBlobContainerClient(blobContainerName);

using var host = hb.Build();
await host.StartAsync();

var serviceClient = host.Services.GetRequiredService<BlobServiceClient>();
var blobContainer = (await serviceClient.CreateBlobContainerAsync("container")).Value;
var blobClient = blobContainer.GetBlobClient("testKey");
var blobServiceClient = host.Services.GetRequiredService<BlobServiceClient>();
var blobContainerClient = host.Services.GetRequiredService<BlobContainerClient>();
await blobContainerClient.CreateIfNotExistsAsync(); // For Aspire 9.3 only
Copy link
Member

Choose a reason for hiding this comment

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

Why is this necessary? Aspire should be creating the blob container, right?

Copy link
Member

Choose a reason for hiding this comment

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

Probably for the same reason we had to make this PR in 9.4 to fix some flaky tests. On 9.3 I would see failures without this line.

var blobClient = blobContainerClient.GetBlobClient("testKey");

await blobClient.UploadAsync(BinaryData.FromString("testValue"));

12 changes: 7 additions & 5 deletions tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -231,10 +231,12 @@ public async Task AddBlobContainer_ConnectionString_resolved_expected_RunAsEmula
var blobs = storage.AddBlobs("blob");
var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName);

string? blobConntionString = await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync();
string expected = $"Endpoint=\"{blobConntionString}\";ContainerName={blobContainerName};";
string? blobConnectionString = await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync();
string? blobContainerConnectionString = await ((IResourceWithConnectionString)blobContainer.Resource).GetConnectionStringAsync();

Assert.Equal(expected, await ((IResourceWithConnectionString)blobContainer.Resource).GetConnectionStringAsync());
Assert.NotNull(blobConnectionString);
Assert.Contains(blobConnectionString, blobContainerConnectionString);
Assert.Contains($"ContainerName={blobContainerName}", blobContainerConnectionString);
}

[Fact]
@@ -252,7 +254,7 @@ public async Task AddBlobContainer_ConnectionString_resolved_expected()
var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName);

string? blobsConnectionString = await ((IResourceWithConnectionString)blobs.Resource).GetConnectionStringAsync();
string expected = $"Endpoint=\"{blobsConnectionString}\";ContainerName={blobContainerName};";
string expected = $"Endpoint={blobsConnectionString};ContainerName={blobContainerName}";

Assert.Equal(expected, await ((IResourceWithConnectionString)blobContainer.Resource).GetConnectionStringAsync());
}
@@ -266,7 +268,7 @@ public void AddBlobContainer_ConnectionString_unresolved_expected()
var blobs = storage.AddBlobs("blob");
var blobContainer = blobs.AddBlobContainer(name: "myContainer");

Assert.Equal("Endpoint=\"{storage.outputs.blobEndpoint}\";ContainerName=myContainer;", blobContainer.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("Endpoint={storage.outputs.blobEndpoint};ContainerName=myContainer", blobContainer.Resource.ConnectionStringExpression.ValueExpression);
}

[Fact]
Loading
Oops, something went wrong.