Skip to content
Merged
Show file tree
Hide file tree
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
Expand Up @@ -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>
Expand All @@ -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();
}

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

namespace Aspire.Azure.Storage.Blobs;
Expand All @@ -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>
Expand All @@ -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
Expand Up @@ -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
Expand Up @@ -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()
Expand All @@ -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
Expand Up @@ -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"));

Expand Down
12 changes: 7 additions & 5 deletions tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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());
}
Expand All @@ -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]
Expand Down
Loading