diff --git a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs index 31b51666133..30b794b282f 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs @@ -38,8 +38,16 @@ internal ReferenceExpression GetConnectionString(string? blobContainerName) return ConnectionStringExpression; } + // For non-emulator environments (deployed), return the connection string directly + // which will be used as a ServiceUri with the container name appended by the client + if (!Parent.IsEmulator) + { + return ConnectionStringExpression; + } + + // For emulator environment, create a connection string with the emulator-specific format ReferenceExpressionBuilder builder = new(); - builder.Append($"{Endpoint}=\"{ConnectionStringExpression}\";"); + builder.Append($"{Endpoint}={ConnectionStringExpression};"); if (!string.IsNullOrEmpty(blobContainerName)) { diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs index d74fb692cbb..60c1c76f85b 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs +++ b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs @@ -23,15 +23,39 @@ 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. + // NOTE: if ever these constants 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)) + // First try to parse as a connection string in the format Endpoint=value;ContainerName=value + try { - ConnectionString = endpoint.ToString(); - BlobContainerName = containerName.ToString(); + DbConnectionStringBuilder builder = new() { ConnectionString = connectionString }; + if (builder.TryGetValue(Endpoint, out var endpoint) && builder.TryGetValue(ContainerName, out var containerName)) + { + ConnectionString = endpoint?.ToString(); + BlobContainerName = containerName?.ToString(); + return; // Successfully parsed + } + } + catch (ArgumentException) + { + // If this is not a valid connection string, it might be a direct URL endpoint + // from a deployed environment - we'll handle it in the next step + } + + // Handle the case where connectionString is a direct URL endpoint (for deployed environments) + // In this case, we use it as the ConnectionString directly and expect the BlobContainerName + // to be set separately through configuration + if (Uri.TryCreate(connectionString, UriKind.Absolute, out _)) + { + ConnectionString = connectionString; + // BlobContainerName should be set through configuration + } + else + { + // If we get here, the string is neither a valid connection string nor a valid URI + throw new ArgumentException($"Invalid connection string format. Expected either a connection string with format 'Endpoint=value;ContainerName=value' or a valid URI."); } } } diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs index 1c45c37e8e2..052ef96504e 100644 --- a/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs +++ b/tests/Aspire.Azure.Storage.Blobs.Tests/AzureBlobStorageContainerSettingsTests.cs @@ -12,11 +12,7 @@ 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) + public void ParseConnectionString_null_or_empty_input(string? connectionString) { var settings = new AzureBlobStorageContainerSettings(); @@ -26,15 +22,23 @@ public void ParseConnectionString_invalid_input(string? connectionString) Assert.Null(settings.BlobContainerName); } - [Fact] - public void ParseConnectionString_invalid_input_results_in_AE() + [Theory] + [InlineData(";")] + [InlineData("InvalidConnectionString")] + [InlineData("Endpoint=")] + [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_throws(string connectionString) { var settings = new AzureBlobStorageContainerSettings(); - string connectionString = "InvalidConnectionString"; Assert.Throws(() => ((IConnectionStringSettings)settings).ParseConnectionString(connectionString)); } + // The "partial_input_handled_gracefully" test was removed because these cases + // now throw exceptions in our stricter validation + [Theory] [InlineData("Endpoint=https://example.blob.core.windows.net;ContainerName=my-container")] [InlineData("Endpoint=https://example.blob.core.windows.net;ContainerName=my-container;ExtraParam=value")] @@ -49,4 +53,44 @@ public void ParseConnectionString_valid_input(string connectionString) Assert.Equal("https://example.blob.core.windows.net", settings.ConnectionString); Assert.Equal("my-container", settings.BlobContainerName); } + + [Fact] + public void ParseConnectionString_with_quoted_endpoint() + { + // This test reproduces the issue where the connection string has quotes around the endpoint value + var settings = new AzureBlobStorageContainerSettings(); + string connectionString = "Endpoint=\"https://example.blob.core.windows.net\";ContainerName=my-container"; + + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + + Assert.Equal("https://example.blob.core.windows.net", settings.ConnectionString); + Assert.Equal("my-container", settings.BlobContainerName); + } + + [Fact] + public void ParseConnectionString_with_emulator_format() + { + // Test with emulator connection string format where Endpoint value is itself a connection string + var settings = new AzureBlobStorageContainerSettings(); + string connectionString = "Endpoint=\"DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=key;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;\";ContainerName=foo-container;"; + + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + + Assert.Equal("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=key;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;", settings.ConnectionString); + Assert.Equal("foo-container", settings.BlobContainerName); + } + + [Theory] + [InlineData("https://example.blob.core.windows.net")] + [InlineData("http://localhost:10000/devstoreaccount1")] + public void ParseConnectionString_url_endpoint(string endpoint) + { + var settings = new AzureBlobStorageContainerSettings(); + + // Using a URL directly as a connection string (deployed environment scenario) + ((IConnectionStringSettings)settings).ParseConnectionString(endpoint); + + Assert.Equal(endpoint, settings.ConnectionString); + Assert.Null(settings.BlobContainerName); // Container name should be provided separately + } }