diff --git a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs index 31b51666133..0df5914b4c4 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs @@ -16,10 +16,6 @@ public class AzureBlobStorageResource(string name, AzureStorageResource storage) IResourceWithParent, 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); - /// /// Gets the parent AzureStorageResource of this AzureBlobStorageResource. /// @@ -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(); } diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs index d74fb692cbb..cb581a1d1d0 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs +++ b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs @@ -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; /// public sealed partial class AzureBlobStorageContainerSettings : AzureStorageBlobsSettings, IConnectionStringSettings { + [GeneratedRegex(@"(?i)ContainerName\s*=\s*([^;]+);?", RegexOptions.IgnoreCase)] + private static partial Regex ContainerNameRegex(); + /// /// Gets or sets the name of the blob container. /// @@ -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; } } } diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AzureStorageBlobsSettings.cs b/src/Components/Aspire.Azure.Storage.Blobs/AzureStorageBlobsSettings.cs index d1b56a420b7..953a78acd9d 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/AzureStorageBlobsSettings.cs +++ b/src/Components/Aspire.Azure.Storage.Blobs/AzureStorageBlobsSettings.cs @@ -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; } } } diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs index 1c45c37e8e2..b2765670b09 100644 --- a/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs +++ b/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs @@ -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); } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs index 2252418382b..4478879987e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs @@ -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(); - var blobContainer = (await serviceClient.CreateBlobContainerAsync("container")).Value; - var blobClient = blobContainer.GetBlobClient("testKey"); + var blobServiceClient = host.Services.GetRequiredService(); + var blobContainerClient = host.Services.GetRequiredService(); + await blobContainerClient.CreateIfNotExistsAsync(); // For Aspire 9.3 only + var blobClient = blobContainerClient.GetBlobClient("testKey"); await blobClient.UploadAsync(BinaryData.FromString("testValue")); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs index 099d744fc42..e74aeb75d68 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs @@ -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]