diff --git a/playground/AspireEventHub/EventHubs.AppHost/Program.cs b/playground/AspireEventHub/EventHubs.AppHost/Program.cs index 80e9c3d98c7..997489e6a31 100644 --- a/playground/AspireEventHub/EventHubs.AppHost/Program.cs +++ b/playground/AspireEventHub/EventHubs.AppHost/Program.cs @@ -3,7 +3,7 @@ // required for the event processor client which will use the connectionName to get the connectionString. var blob = builder.AddAzureStorage("ehstorage") .RunAsEmulator() - .AddBlobs("checkpoints"); + .AddBlobService("checkpoints"); var eventHub = builder.AddAzureEventHubs("eventhubns") .RunAsEmulator() diff --git a/playground/AspireEventHub/EventHubsConsumer/Program.cs b/playground/AspireEventHub/EventHubsConsumer/Program.cs index c524c8e94fc..5fc7edfc918 100644 --- a/playground/AspireEventHub/EventHubsConsumer/Program.cs +++ b/playground/AspireEventHub/EventHubsConsumer/Program.cs @@ -18,7 +18,7 @@ else { // required for checkpointing our position in the event stream - builder.AddAzureBlobClient("checkpoints"); + builder.AddAzureBlobServiceClient("checkpoints"); builder.AddAzureEventProcessorClient("eventhubOne"); diff --git a/playground/AzureAppService/AzureAppService.ApiService/Program.cs b/playground/AzureAppService/AzureAppService.ApiService/Program.cs index b0cdbaa85d5..dcb9844eb39 100644 --- a/playground/AzureAppService/AzureAppService.ApiService/Program.cs +++ b/playground/AzureAppService/AzureAppService.ApiService/Program.cs @@ -10,7 +10,7 @@ builder.AddServiceDefaults(); builder.AddCosmosDbContext("account", "db"); -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); var app = builder.Build(); diff --git a/playground/AzureAppService/AzureAppService.AppHost/Program.cs b/playground/AzureAppService/AzureAppService.AppHost/Program.cs index 64d4de5d7da..8315e851f44 100644 --- a/playground/AzureAppService/AzureAppService.AppHost/Program.cs +++ b/playground/AzureAppService/AzureAppService.AppHost/Program.cs @@ -25,7 +25,7 @@ storage.AllowBlobPublicAccess = false; }) .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); -var blobs = storage.AddBlobs("blobs"); +var blobs = storage.AddBlobService("blobs"); // Testing projects builder.AddProject("api") diff --git a/playground/AzureContainerApps/AzureContainerApps.ApiService/Program.cs b/playground/AzureContainerApps/AzureContainerApps.ApiService/Program.cs index 5a0ba1078b6..b4d9adebe1f 100644 --- a/playground/AzureContainerApps/AzureContainerApps.ApiService/Program.cs +++ b/playground/AzureContainerApps/AzureContainerApps.ApiService/Program.cs @@ -11,7 +11,7 @@ builder.AddServiceDefaults(); builder.AddCosmosDbContext("account", "db"); -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); builder.AddRedisClient("cache"); var app = builder.Build(); diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs index 86876845d09..3ab9187d56f 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/Program.cs @@ -31,7 +31,7 @@ // Testing a connection string var storage = builder.AddAzureStorage("storage") .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); -var blobs = storage.AddBlobs("blobs"); +var blobs = storage.AddBlobService("blobs"); // Testing docker files diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs index 00afe533fb1..066941c645c 100644 --- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs +++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs @@ -14,8 +14,8 @@ // Add service defaults & Aspire client integrations. builder.AddServiceDefaults(); -builder.AddAzureQueueClient("queue"); -builder.AddAzureBlobClient("blob"); +builder.AddAzureQueueServiceClient("queue"); +builder.AddAzureBlobServiceClient("blob"); builder.AddAzureEventHubProducerClient("myhub"); #if !SKIP_UNSTABLE_EMULATORS builder.AddAzureServiceBusClient("messaging"); diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs index 82f107270d5..3d8cb28a605 100644 --- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs +++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs @@ -3,9 +3,9 @@ builder.AddAzureContainerAppEnvironment("env"); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); -var queue = storage.AddQueues("queue"); -var blob = storage.AddBlobs("blob"); -var myBlobContainer = blob.AddBlobContainer("myblobcontainer"); +var queue = storage.AddQueueService("queue"); +var blob = storage.AddBlobService("blob"); +var myBlobContainer = storage.AddBlobContainer("myblobcontainer"); var eventHub = builder.AddAzureEventHubs("eventhubs") .RunAsEmulator() @@ -24,11 +24,11 @@ var funcApp = builder.AddAzureFunctionsProject("funcapp") .WithExternalHttpEndpoints() .WithReference(eventHub).WaitFor(eventHub) - .WithReference(myBlobContainer).WaitFor(myBlobContainer) #if !SKIP_UNSTABLE_EMULATORS .WithReference(serviceBus).WaitFor(serviceBus) .WithReference(cosmosDb).WaitFor(cosmosDb) #endif + .WithReference(myBlobContainer).WaitFor(myBlobContainer) .WithReference(blob) .WithReference(queue); diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs index 6c535261cdd..563b2087fd2 100644 --- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs +++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs @@ -4,7 +4,7 @@ namespace AzureFunctionsEndToEnd.Functions; -public class MyAzureBlobTrigger(ILogger logger, BlobContainerClient containerClient) +public class MyAzureBlobTrigger(BlobContainerClient containerClient, ILogger logger) { [Function(nameof(MyAzureBlobTrigger))] [BlobOutput("test-files/{name}.txt", Connection = "blob")] diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs index 0ea035d98c5..c0f85ce0121 100644 --- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs +++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs @@ -8,7 +8,7 @@ public class MyAzureQueueTrigger(ILogger logger) { [Function(nameof(MyAzureQueueTrigger))] public void Run([QueueTrigger("queue", Connection = "queue")] QueueMessage message) - { + { logger.LogInformation("C# Queue trigger function processed: {Text}", message.MessageText); } } diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs index 336749046b4..1fc2b3245b7 100644 --- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs +++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs @@ -4,8 +4,8 @@ var builder = FunctionsApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.AddAzureQueueClient("queue"); -builder.AddAzureBlobClient("blob"); +builder.AddAzureQueueServiceClient("queue"); +builder.AddAzureBlobServiceClient("blob"); builder.AddAzureBlobContainerClient("myblobcontainer"); builder.AddAzureEventHubProducerClient("myhub"); #if !SKIP_UNSTABLE_EMULATORS diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs index 02f16b8f516..13b9d3b9135 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs @@ -8,16 +8,17 @@ builder.AddServiceDefaults(); -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); + builder.AddKeyedAzureBlobContainerClient("foocontainer"); -builder.AddAzureQueueClient("queues"); +builder.AddKeyedAzureQueue("myqueue"); var app = builder.Build(); app.MapDefaultEndpoints(); -app.MapGet("/", async (BlobServiceClient bsc, QueueServiceClient qsc, [FromKeyedServices("foocontainer")] BlobContainerClient keyedContainerClient1) => +app.MapGet("/", async (BlobServiceClient bsc, [FromKeyedServices("myqueue")] QueueClient queue, [FromKeyedServices("foocontainer")] BlobContainerClient keyedContainerClient1) => { var blobNames = new List(); var blobNameAndContent = Guid.NewGuid().ToString(); @@ -30,8 +31,6 @@ await ReadBlobsAsync(directContainerClient, blobNames); await ReadBlobsAsync(keyedContainerClient1, blobNames); - var queue = qsc.GetQueueClient("myqueue"); - await queue.CreateIfNotExistsAsync(); await queue.SendMessageAsync("Hello, world!"); return blobNames; diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs index aba1f9e6eea..5c643a93c99 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs @@ -8,24 +8,24 @@ container.WithDataBindMount(); }); -var blobs = storage.AddBlobs("blobs"); -blobs.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1"); -blobs.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2"); +var blobs = storage.AddBlobService("blobs"); +storage.AddBlobContainer("mycontainer1", blobContainerName: "test-container-1"); +storage.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2"); -var queues = storage.AddQueues("queues"); +var myqueue = storage.AddQueue("myqueue", queueName: "my-queue"); var storage2 = builder.AddAzureStorage("storage2").RunAsEmulator(container => { container.WithDataBindMount(); }); -var blobs2 = storage2.AddBlobs("blobs2"); -var blobContainer2 = blobs2.AddBlobContainer("foocontainer", blobContainerName: "foo-container"); + +var blobContainer2 = storage2.AddBlobContainer("foocontainer", blobContainerName: "foo-container"); builder.AddProject("api") .WithExternalHttpEndpoints() .WithReference(blobs).WaitFor(blobs) .WithReference(blobContainer2).WaitFor(blobContainer2) - .WithReference(queues).WaitFor(queues); + .WithReference(myqueue).WaitFor(myqueue); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/aspire-manifest.json b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/aspire-manifest.json index d26c582b3e8..e6de0e79b0c 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/aspire-manifest.json +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/aspire-manifest.json @@ -17,15 +17,19 @@ "type": "value.v0", "connectionString": "Endpoint={storage.outputs.blobEndpoint};ContainerName=test-container-2" }, - "queues": { + "storage-queues": { "type": "value.v0", "connectionString": "{storage.outputs.queueEndpoint}" }, + "myqueue": { + "type": "value.v0", + "connectionString": "Endpoint={storage.outputs.queueEndpoint};QueueName=my-queue" + }, "storage2": { "type": "azure.bicep.v0", "path": "storage2.module.bicep" }, - "blobs2": { + "storage2-blobs": { "type": "value.v0", "connectionString": "{storage2.outputs.blobEndpoint}" }, @@ -44,7 +48,7 @@ "HTTP_PORTS": "{api.bindings.http.targetPort}", "ConnectionStrings__blobs": "{blobs.connectionString}", "ConnectionStrings__foocontainer": "{foocontainer.connectionString}", - "ConnectionStrings__queues": "{queues.connectionString}" + "ConnectionStrings__myqueue": "{myqueue.connectionString}" }, "bindings": { "http": { diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage.module.bicep b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage.module.bicep index a35c22a7293..c04e655e501 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage.module.bicep +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage.module.bicep @@ -36,6 +36,16 @@ resource mycontainer2 'Microsoft.Storage/storageAccounts/blobServices/containers parent: blobs } +resource storage_queues 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = { + name: 'default' + parent: storage +} + +resource myqueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = { + name: 'my-queue' + parent: storage_queues +} + output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage2.module.bicep b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage2.module.bicep index 32838ffeb1c..b64a4408eb9 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage2.module.bicep +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/storage2.module.bicep @@ -21,14 +21,14 @@ resource storage2 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { +resource storage2_blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { name: 'default' parent: storage2 } resource foocontainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { name: 'foo-container' - parent: blobs + parent: storage2_blobs } output blobEndpoint string = storage2.properties.primaryEndpoints.blob diff --git a/playground/bicep/BicepSample.ApiService/Program.cs b/playground/bicep/BicepSample.ApiService/Program.cs index a040ed6c5b8..900509bc941 100644 --- a/playground/bicep/BicepSample.ApiService/Program.cs +++ b/playground/bicep/BicepSample.ApiService/Program.cs @@ -18,9 +18,9 @@ builder.AddNpgsqlDbContext("db2"); builder.AddAzureCosmosClient("cosmos"); builder.AddRedisClient("redis"); -builder.AddAzureBlobClient("blob"); -builder.AddAzureTableClient("table"); -builder.AddAzureQueueClient("queue"); +builder.AddAzureBlobServiceClient("blob"); +builder.AddAzureTableServiceClient("table"); +builder.AddAzureQueueServiceClient("queue"); builder.AddAzureServiceBusClient("sb"); var app = builder.Build(); diff --git a/playground/bicep/BicepSample.AppHost/Program.cs b/playground/bicep/BicepSample.AppHost/Program.cs index 521539447b8..cb5e355c65a 100644 --- a/playground/bicep/BicepSample.AppHost/Program.cs +++ b/playground/bicep/BicepSample.AppHost/Program.cs @@ -29,9 +29,9 @@ var storage = builder.AddAzureStorage("storage"); // .RunAsEmulator(); -var blobs = storage.AddBlobs("blob"); -var tables = storage.AddTables("table"); -var queues = storage.AddQueues("queue"); +var blobs = storage.AddBlobService("blob"); +var tables = storage.AddTableService("table"); +var queues = storage.AddQueueService("queue"); var sqlServer = builder.AddAzureSqlServer("sql").AddDatabase("db"); diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs index 5477c1e97c6..8766b147f32 100644 --- a/playground/cdk/CdkSample.ApiService/Program.cs +++ b/playground/cdk/CdkSample.ApiService/Program.cs @@ -16,7 +16,7 @@ builder.AddServiceDefaults(); -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); builder.AddSqlServerDbContext("sqldb"); builder.AddAzureKeyVaultClient("mykv"); builder.AddRedisClient("cache"); diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs index 4f49b7a6b43..ddd2ab9ac0b 100644 --- a/playground/cdk/CdkSample.AppHost/Program.cs +++ b/playground/cdk/CdkSample.AppHost/Program.cs @@ -22,7 +22,7 @@ account.Location = locationOverride.AsProvisioningParameter(infrastructure); }); -var blobs = storage.AddBlobs("blobs"); +var blobs = storage.AddBlobService("blobs"); var sqldb = builder.AddAzureSqlServer("sql").AddDatabase("sqldb"); diff --git a/playground/orleans/Orleans.AppHost/Program.cs b/playground/orleans/Orleans.AppHost/Program.cs index a71789225e2..db9b6df13af 100644 --- a/playground/orleans/Orleans.AppHost/Program.cs +++ b/playground/orleans/Orleans.AppHost/Program.cs @@ -1,8 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); -var clusteringTable = storage.AddTables("clustering"); -var grainStorage = storage.AddBlobs("grainstate"); +var clusteringTable = storage.AddTableService("clustering"); +var grainStorage = storage.AddBlobService("grainstate"); var orleans = builder.AddOrleans("my-app") .WithClustering(clusteringTable) diff --git a/playground/orleans/OrleansClient/Program.cs b/playground/orleans/OrleansClient/Program.cs index 5a11264fcc7..8e888cfa4eb 100644 --- a/playground/orleans/OrleansClient/Program.cs +++ b/playground/orleans/OrleansClient/Program.cs @@ -3,7 +3,7 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.AddKeyedAzureTableClient("clustering"); +builder.AddKeyedAzureTableServiceClient("clustering"); builder.UseOrleansClient(); var app = builder.Build(); diff --git a/playground/orleans/OrleansServer/Program.cs b/playground/orleans/OrleansServer/Program.cs index 2cae58bd9c5..cbdd7f5b357 100644 --- a/playground/orleans/OrleansServer/Program.cs +++ b/playground/orleans/OrleansServer/Program.cs @@ -3,8 +3,8 @@ var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.AddKeyedAzureTableClient("clustering"); -builder.AddKeyedAzureBlobClient("grainstate"); +builder.AddKeyedAzureTableServiceClient("clustering"); +builder.AddKeyedAzureBlobServiceClient("grainstate"); builder.UseOrleans(); var app = builder.Build(); diff --git a/src/Aspire.Hosting.Azure.Functions/README.md b/src/Aspire.Hosting.Azure.Functions/README.md index cab05f263ec..6b2ac54beba 100644 --- a/src/Aspire.Hosting.Azure.Functions/README.md +++ b/src/Aspire.Hosting.Azure.Functions/README.md @@ -35,8 +35,8 @@ using Aspire.Hosting.Azure.Functions; var builder = new DistributedApplicationBuilder(); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); -var queue = storage.AddQueues("queue"); -var blob = storage.AddBlobs("blob"); +var queue = storage.AddQueueService("queue"); +var blob = storage.AddBlobService("blob"); builder.AddAzureFunctionsProject("my-functions-project") .WithReference(queue) diff --git a/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj index f3a8aced451..50e98aa7aad 100644 --- a/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj +++ b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs index 0df5914b4c4..48a1023944e 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureBlobStorageResource.cs @@ -68,7 +68,7 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction 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 + // We don't inject the queue resource here since we only want it to // be accessible by the Functions host. target[$"{AzureStorageResource.BlobsConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.BlobEndpoint; } diff --git a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs new file mode 100644 index 00000000000..3706a30e8ec --- /dev/null +++ b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs @@ -0,0 +1,55 @@ +// 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 Azure.Provisioning; + +namespace Aspire.Hosting.Azure; + +/// +/// A resource that represents an Azure Storage Queue. +/// +/// The name of the resource. +/// The name of the queue. +/// The that the resource is stored in. +public class AzureQueueStorageQueueResource(string name, string queueName, AzureQueueStorageResource parent) : Resource(name), + IResourceWithConnectionString, + IResourceWithParent +{ + /// + /// Gets the blob container name. + /// + public string QueueName { get; } = ThrowIfNullOrEmpty(queueName); + + /// + /// Gets the connection string template for the manifest for the Azure Storage Queue resource. + /// + public ReferenceExpression ConnectionStringExpression => Parent.GetConnectionString(QueueName); + + /// + /// Gets the parent of this . + /// + public AzureQueueStorageResource Parent => parent ?? throw new ArgumentNullException(nameof(parent)); + + /// + /// Converts the current instance to a provisioning entity. + /// + /// A instance. + internal global::Azure.Provisioning.Storage.StorageQueue ToProvisioningEntity() + { + global::Azure.Provisioning.Storage.StorageQueue queue = new(Infrastructure.NormalizeBicepIdentifier(Name)) + { + Name = QueueName + }; + + return queue; + } + + private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + ArgumentException.ThrowIfNullOrEmpty(argument, paramName); + return argument; + } +} diff --git a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs index 295a5b5a77b..0465539ac65 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageResource.cs @@ -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; @@ -26,6 +27,29 @@ public class AzureQueueStorageResource(string name, AzureStorageResource storage public ReferenceExpression ConnectionStringExpression => Parent.GetQueueConnectionString(); + internal ReferenceExpression GetConnectionString(string? queueName) + { + if (string.IsNullOrEmpty(queueName)) + { + return ConnectionStringExpression; + } + + ReferenceExpressionBuilder builder = new(); + + if (Parent.IsEmulator) + { + builder.AppendFormatted(ConnectionStringExpression); + } + else + { + builder.Append($"Endpoint={ConnectionStringExpression}"); + } + + builder.Append($";QueueName={queueName}"); + + return builder.Build(); + } + void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary target, string connectionName) { if (Parent.IsEmulator) @@ -42,4 +66,14 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction target[$"{AzureStorageResource.QueuesConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.QueueEndpoint; } } + + /// + /// Converts the current instance to a provisioning entity. + /// + /// A instance. + internal global::Azure.Provisioning.Storage.QueueService ToProvisioningEntity() + { + global::Azure.Provisioning.Storage.QueueService service = new(Infrastructure.NormalizeBicepIdentifier(Name)); + return service; + } } diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs index 70ac922c4dc..9184bd1bae0 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageExtensions.cs @@ -8,6 +8,7 @@ using Azure.Provisioning; using Azure.Provisioning.Storage; using Azure.Storage.Blobs; +using Azure.Storage.Queues; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -71,25 +72,47 @@ public static IResourceBuilder AddAzureStorage(this IDistr Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } } }); - var blobs = new BlobService("blobs") + var azureResource = (AzureStorageResource)infrastructure.AspireResource; + + if (azureResource.BlobStorageResource is not null) { - Parent = storageAccount - }; - infrastructure.Add(blobs); + var blobService = azureResource.BlobStorageResource.ToProvisioningEntity(); + blobService.Parent = storageAccount; + infrastructure.Add(blobService); - infrastructure.Add(new ProvisioningOutput("blobEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.BlobUri }); - infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.QueueUri }); - infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.TableUri }); + foreach (var blobContainer in azureResource.BlobContainers) + { + var cdkBlobContainer = blobContainer.ToProvisioningEntity(); + cdkBlobContainer.Parent = blobService; + infrastructure.Add(cdkBlobContainer); + } + } - var azureResource = (AzureStorageResource)infrastructure.AspireResource; + if (azureResource.QueueStorageResource is not null) + { + var queueService = azureResource.QueueStorageResource.ToProvisioningEntity(); + queueService.Parent = storageAccount; + infrastructure.Add(queueService); + + foreach (var queue in azureResource.Queues) + { + var cdkQueue = queue.ToProvisioningEntity(); + cdkQueue.Parent = queueService; + infrastructure.Add(cdkQueue); + } + } - foreach (var blobContainer in azureResource.BlobContainers) + if (azureResource.TableStorageResource is not null) { - var cdkBlobContainer = blobContainer.ToProvisioningEntity(); - cdkBlobContainer.Parent = blobs; - infrastructure.Add(cdkBlobContainer); + var tableService = azureResource.TableStorageResource.ToProvisioningEntity(); + tableService.Parent = storageAccount; + infrastructure.Add(tableService); } + infrastructure.Add(new ProvisioningOutput("blobEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.BlobUri }); + infrastructure.Add(new ProvisioningOutput("queueEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.QueueUri }); + infrastructure.Add(new ProvisioningOutput("tableEndpoint", typeof(string)) { Value = storageAccount.PrimaryEndpoints.TableUri }); + // We need to output name to externalize role assignments. infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = storageAccount.Name }); }; @@ -132,33 +155,43 @@ public static IResourceBuilder RunAsEmulator(this IResourc }); BlobServiceClient? blobServiceClient = null; + QueueServiceClient? queueServiceClient = null; + builder .OnBeforeResourceStarted(async (storage, @event, ct) => { - // The BlobServiceClient is created before the health check is run. + // The BlobServiceClient and QueueServiceClient are created before the health check is run. // We can't use ConnectionStringAvailableEvent here because the resource doesn't have a connection string, so // we use BeforeResourceStartedEvent - var connectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null."); - blobServiceClient = CreateBlobServiceClient(connectionString); + var blobConnectionString = await builder.Resource.GetBlobConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null."); + blobServiceClient = CreateBlobServiceClient(blobConnectionString); + + var queueConnectionString = await builder.Resource.GetQueueConnectionString().GetValueAsync(ct).ConfigureAwait(false) ?? throw new DistributedApplicationException($"{nameof(ConnectionStringAvailableEvent)} was published for the '{builder.Resource.Name}' resource but the connection string was null."); + queueServiceClient = CreateQueueServiceClient(queueConnectionString); }) .OnResourceReady(async (storage, @event, ct) => { - // The ResourceReadyEvent of a resource is triggered after its health check is healthy. + // The ResourceReadyEvent of a resource is triggered after its health check (AddAzureBlobStorage) is healthy. // This means we can safely use this event to create the blob containers. - if (blobServiceClient is null) - { - throw new InvalidOperationException("BlobServiceClient is not initialized."); - } + _ = blobServiceClient ?? throw new InvalidOperationException($"{nameof(BlobServiceClient)} is not initialized."); + _ = queueServiceClient ?? throw new InvalidOperationException($"{nameof(QueueServiceClient)} is not initialized."); foreach (var container in builder.Resource.BlobContainers) { var blobContainerClient = blobServiceClient.GetBlobContainerClient(container.BlobContainerName); await blobContainerClient.CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false); } + + foreach (var queue in builder.Resource.Queues) + { + var queueClient = queueServiceClient.GetQueueClient(queue.QueueName); + await queueClient.CreateIfNotExistsAsync(cancellationToken: ct).ConfigureAwait(false); + } }); + // Add the "Storage" resource health check. There will be separate health checks for the nested child resources. var healthCheckKey = $"{builder.Resource.Name}_check"; builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage(sp => @@ -280,22 +313,40 @@ public static IResourceBuilder WithApiVersionCheck return Task.CompletedTask; }); } - /// /// Creates a builder for the which can be referenced to get the Azure Storage blob endpoint for the storage account. /// - /// The for / + /// The for . /// The name of the resource. /// An for the . + [Obsolete("Use AddBlobService on IResourceBuilder instead.")] public static IResourceBuilder AddBlobs(this IResourceBuilder builder, [ResourceName] string name) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + return AddBlobService(builder, name); + } + + /// + /// Creates a builder for the which can be referenced to get the Azure Storage blob endpoint for the storage account. + /// + /// The for . + /// The name of the resource. + /// An for the . + public static IResourceBuilder AddBlobService(this IResourceBuilder builder, [ResourceName] string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + name ??= builder.Resource.Name + "-blobs"; + var resource = new AzureBlobStorageResource(name, builder.Resource); + builder.Resource.BlobStorageResource = resource; string? connectionString = null; + // Add the "Blobs" resource health check. This is a separate health check from the "Storage" resource health check. + // Doing it on the storage is not sufficient as the WaitForHealthyAsync doesn't bubble up to the parent resources. var healthCheckKey = $"{resource.Name}_check"; BlobServiceClient? blobServiceClient = null; @@ -316,10 +367,53 @@ public static IResourceBuilder AddBlobs(this IResource /// /// Creates a builder for the which can be referenced to get the Azure Storage blob container endpoint for the storage account. /// - /// The for / + /// The for . /// The name of the resource. /// The name of the blob container. /// An for the . + public static IResourceBuilder AddBlobContainer(this IResourceBuilder builder, [ResourceName] string name, string? blobContainerName = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + blobContainerName ??= name; + + // Create a Blob Service resource implicitly + if (builder.Resource.BlobStorageResource is null) + { + AddBlobService(builder); + } + + AzureBlobStorageContainerResource resource = new(name, blobContainerName, builder.Resource.BlobStorageResource!); + builder.Resource.BlobContainers.Add(resource); + + string? connectionString = null; + + var healthCheckKey = $"{resource.Name}_check"; + + BlobServiceClient? blobServiceClient = null; + builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureBlobStorage( + sp => blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized.")), + optionsFactory: sp => new HealthChecks.Azure.Storage.Blobs.AzureBlobStorageHealthCheckOptions { ContainerName = blobContainerName }, + name: healthCheckKey); + + return builder.ApplicationBuilder + .AddResource(resource) + .WithHealthCheck(healthCheckKey) + .OnConnectionStringAvailable(async (containerResource, @event, ct) => + { + connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + }); + } + + /// + /// Creates a builder for the which can be referenced to get the Azure Storage blob container endpoint for the storage account. + /// + /// The for . + /// The name of the resource. + /// The name of the blob container. + /// An for the . + [Obsolete("Use AddBlobContainer on IResourceBuilder instead.")] public static IResourceBuilder AddBlobContainer(this IResourceBuilder builder, [ResourceName] string name, string? blobContainerName = null) { ArgumentNullException.ThrowIfNull(builder); @@ -351,31 +445,127 @@ public static IResourceBuilder AddBlobContain /// /// Creates a builder for the which can be referenced to get the Azure Storage tables endpoint for the storage account. /// - /// The for / + /// The for . /// The name of the resource. /// An for the . + [Obsolete("Use AddTableService on IResourceBuilder instead.")] public static IResourceBuilder AddTables(this IResourceBuilder builder, [ResourceName] string name) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + return AddTableService(builder, name); + } + + /// + /// Creates a builder for the which can be referenced to get the Azure Storage tables endpoint for the storage account. + /// + /// The for . + /// The name of the resource. + /// An for the . + public static IResourceBuilder AddTableService(this IResourceBuilder builder, [ResourceName] string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + name ??= builder.Resource.Name + "-tables"; + var resource = new AzureTableStorageResource(name, builder.Resource); + builder.Resource.TableStorageResource = resource; + return builder.ApplicationBuilder.AddResource(resource); } /// /// Creates a builder for the which can be referenced to get the Azure Storage queues endpoint for the storage account. /// - /// The for / + /// The for . /// The name of the resource. /// An for the . + [Obsolete("Use AddQueueService on IResourceBuilder instead.")] public static IResourceBuilder AddQueues(this IResourceBuilder builder, [ResourceName] string name) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + return AddQueueService(builder, name); + } + + /// + /// Creates a builder for the which can be referenced to get the Azure Storage queues endpoint for the storage account. + /// + /// The for . + /// The name of the resource. + /// An for the . + public static IResourceBuilder AddQueueService(this IResourceBuilder builder, [ResourceName] string? name = null) + { + ArgumentNullException.ThrowIfNull(builder); + + name ??= builder.Resource.Name + "-queues"; + var resource = new AzureQueueStorageResource(name, builder.Resource); - return builder.ApplicationBuilder.AddResource(resource); + builder.Resource.QueueStorageResource = resource; + + string? connectionString = null; + + // Add the "Queues" resource health check. This is a separate health check from the "Storage" resource health check. + // Doing it on the storage is not sufficient as the WaitForHealthyAsync doesn't bubble up to the parent resources. + var healthCheckKey = $"{resource.Name}_check"; + + QueueServiceClient? queueServiceClient = null; + builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureQueueStorage(sp => + { + return queueServiceClient ??= CreateQueueServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized.")); + }, name: healthCheckKey); + + return builder.ApplicationBuilder + .AddResource(resource) + .WithHealthCheck(healthCheckKey) + .OnConnectionStringAvailable(async (blobs, @event, ct) => + { + connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + }); + } + + /// + /// Creates a builder for the which can be referenced to get the Azure Storage queue for the storage account. + /// + /// The for . + /// The name of the resource. + /// The name of the queue. + /// An for the . + public static IResourceBuilder AddQueue(this IResourceBuilder builder, [ResourceName] string name, string? queueName = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + queueName ??= name; + + // Create a Queue Service resource implicitly + if (builder.Resource.QueueStorageResource is null) + { + AddQueueService(builder); + } + + AzureQueueStorageQueueResource resource = new(name, queueName, builder.Resource.QueueStorageResource!); + builder.Resource.Queues.Add(resource); + + string? connectionString = null; + + var healthCheckKey = $"{resource.Name}_check"; + + QueueServiceClient? queueServiceClient = null; + builder.ApplicationBuilder.Services.AddHealthChecks().AddAzureQueueStorage( + sp => queueServiceClient ??= CreateQueueServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized.")), + optionsFactory: sp => new HealthChecks.Azure.Storage.Queues.AzureQueueStorageHealthCheckOptions { QueueName = queueName }, + name: healthCheckKey); + + return builder.ApplicationBuilder + .AddResource(resource) + .WithHealthCheck(healthCheckKey) + .OnConnectionStringAvailable(async (containerResource, @event, ct) => + { + connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + }); } private static BlobServiceClient CreateBlobServiceClient(string connectionString) @@ -390,6 +580,18 @@ private static BlobServiceClient CreateBlobServiceClient(string connectionString } } + private static QueueServiceClient CreateQueueServiceClient(string connectionString) + { + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + return new QueueServiceClient(uri, new DefaultAzureCredential()); + } + else + { + return new QueueServiceClient(connectionString); + } + } + /// /// Assigns the specified roles to the given resource, granting it the necessary permissions /// on the target Azure Storage account. This replaces the default role assignments for the resource. @@ -405,7 +607,7 @@ private static BlobServiceClient CreateBlobServiceClient(string connectionString /// var builder = DistributedApplication.CreateBuilder(args); /// /// var storage = builder.AddAzureStorage("storage"); - /// var blobs = storage.AddBlobs("blobs"); + /// var blobs = storage.AddBlobService("blobs"); /// /// var api = builder.AddProject<Projects.Api>("api") /// .WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataContributor) diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs index ec9851be6f3..05994fe2e80 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs @@ -23,8 +23,14 @@ public class AzureStorageResource(string name, Action new(this, "queue"); private EndpointReference EmulatorTableEndpoint => new(this, "table"); + internal AzureBlobStorageResource? BlobStorageResource { get; set; } + internal AzureQueueStorageResource? QueueStorageResource { get; set; } + internal AzureTableStorageResource? TableStorageResource { get; set; } + internal List BlobContainers { get; } = []; + internal List Queues { get; } = []; + /// /// Gets the "blobEndpoint" output reference from the bicep template for the Azure Storage resource. /// diff --git a/src/Aspire.Hosting.Azure.Storage/AzureTableStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureTableStorageResource.cs index f3269cd10dc..bf6fa5de553 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureTableStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureTableStorageResource.cs @@ -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; @@ -40,4 +41,14 @@ void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDiction target[$"{AzureStorageResource.TablesConnectionKeyPrefix}__{connectionName}__ServiceUri"] = Parent.TableEndpoint; // Updated for consistency } } + + /// + /// Converts the current instance to a provisioning entity. + /// + /// A instance. + internal global::Azure.Provisioning.Storage.TableService ToProvisioningEntity() + { + global::Azure.Provisioning.Storage.TableService service = new(Infrastructure.NormalizeBicepIdentifier(Name)); + return service; + } } diff --git a/src/Aspire.Hosting.Azure.Storage/README.md b/src/Aspire.Hosting.Azure.Storage/README.md index 23490d2d772..9bfe06fc52f 100644 --- a/src/Aspire.Hosting.Azure.Storage/README.md +++ b/src/Aspire.Hosting.Azure.Storage/README.md @@ -41,7 +41,7 @@ automatically. In the _AppHost.cs_ file of `AppHost`, add a Blob (can use tables or queues also) Storage connection and consume the connection using the following methods: ```csharp -var blobs = builder.AddAzureStorage("storage").AddBlobs("blobs"); +var blobs = builder.AddAzureStorage("storage").AddBlobService("blobs"); var myService = builder.AddProject() .WithReference(blobs); @@ -50,9 +50,59 @@ var myService = builder.AddProject() The `WithReference` method passes that connection information into a connection string named `blobs` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using the client library [Aspire.Azure.Storage.Blobs](https://www.nuget.org/packages/Aspire.Azure.Storage.Blobs): ```csharp -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); ``` +## Creating and using blob containers and queues directly + +You can create and use blob containers and queues directly by adding them to your storage resource. This allows you to provision and reference specific containers or queues for your services. + +### Adding a blob container + +```csharp +var storage = builder.AddAzureStorage("storage"); +var container = storage.AddBlobContainer("my-container"); +``` + +You can then pass the container reference to a project: + +```csharp +builder.AddProject() + .WithReference(container); +``` + +In your service, consume the container using: + +```csharp +builder.AddAzureBlobContainerClient("my-container"); +``` + +This will register a singleton of type `BlobContainerClient`. + +### Adding a queue + +```csharp +var storage = builder.AddAzureStorage("storage"); +var queue = storage.AddQueue("my-queue"); +``` + +Pass the queue reference to a project: + +```csharp +builder.AddProject() + .WithReference(queue); +``` + +In your service, consume the queue using: + +```csharp +builder.AddAzureQueue("my-queue"); +``` + +This will register a singleton of type `QueueClient`. + +This approach allows you to define and use specific blob containers and queues as first-class resources in your Aspire application model. + ## Additional documentation * https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/storage/Azure.Storage.Blobs/README.md diff --git a/src/Aspire.Hosting.Orleans/README.md b/src/Aspire.Hosting.Orleans/README.md index 241ba583c0f..075b0de9df1 100644 --- a/src/Aspire.Hosting.Orleans/README.md +++ b/src/Aspire.Hosting.Orleans/README.md @@ -18,8 +18,8 @@ Then, in the _AppHost.cs_ file of `AppHost`, add a Or resource and consume the c ```csharp var storage = builder.AddAzureStorage("storage").RunAsEmulator(); -var clusteringTable = storage.AddTables("clustering"); -var grainStorage = storage.AddBlobs("grainstate"); +var clusteringTable = storage.AddTableService("clustering"); +var grainStorage = storage.AddBlobService("grainstate"); var orleans = builder.AddOrleans("my-app") .WithClustering(clusteringTable) diff --git a/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs b/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs index 976efd942cb..0a47a555670 100644 --- a/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs +++ b/src/Components/Aspire.Azure.Data.Tables/AspireTablesExtensions.cs @@ -31,7 +31,7 @@ public static class AspireTablesExtensions /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Data:Tables" section. /// Thrown when neither nor is provided. - public static void AddAzureTableClient( + public static void AddAzureTableServiceClient( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, @@ -43,6 +43,20 @@ public static void AddAzureTableClient( new TableServiceComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); } + /// + [Obsolete("Use AddAzureTableServiceClient instead. This method will be removed in a future version.")] + public static void AddAzureTableClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + AddAzureTableServiceClient(builder, connectionName, configureSettings, configureClientBuilder); + } + /// /// Registers as a singleton for given in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. @@ -53,6 +67,20 @@ public static void AddAzureTableClient( /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Data:Tables:{name}" section. /// Thrown when neither nor is provided. + public static void AddKeyedAzureTableServiceClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new TableServiceComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); + } + + /// + [Obsolete("Use AddKeyedAzureTableServiceClient instead. This method will be removed in a future version.")] public static void AddKeyedAzureTableClient( this IHostApplicationBuilder builder, string name, diff --git a/src/Components/Aspire.Azure.Data.Tables/README.md b/src/Components/Aspire.Azure.Data.Tables/README.md index 8d9ed60079d..913ba3c245a 100644 --- a/src/Components/Aspire.Azure.Data.Tables/README.md +++ b/src/Components/Aspire.Azure.Data.Tables/README.md @@ -22,7 +22,7 @@ dotnet add package Aspire.Azure.Data.Tables In the _AppHost.cs_ file of your project, call the `AddAzureTableClient` extension method to register a `TableServiceClient` for use via the dependency injection container. The method takes a connection name parameter. ```csharp -builder.AddAzureTableClient("tables"); +builder.AddAzureTableServiceClient("tables"); ``` You can then retrieve the `TableServiceClient` instance using dependency injection. For example, to retrieve the client from a Web API controller: @@ -44,10 +44,10 @@ The .NET Aspire Azure Table storage library provides multiple options to configu ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureTableClient()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureTableServiceClient()`: ```csharp -builder.AddAzureTableClient("tableConnectionName"); +builder.AddAzureTableServiceClient("tableConnectionName"); ``` And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The Azure Table storage library supports [Microsoft.Extensions.Configuration](ht You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureTableClient("tables", settings => settings.DisableHealthChecks = true); +builder.AddAzureTableServiceClient("tables", settings => settings.DisableHealthChecks = true); ``` You can also setup the [TableClientOptions](https://learn.microsoft.com/dotnet/api/azure.data.tables.tableclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureTableClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureTableClient("tables", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureTableServiceClient("tables", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions @@ -126,7 +126,7 @@ Then, in the _AppHost.cs_ file of `AppHost`, add a Table Storage connection and ```csharp var tables = builder.ExecutionContext.IsPublishMode - ? builder.AddAzureStorage("storage").AddTables("tables") + ? builder.AddAzureStorage("storage").AddTableService("tables") : builder.AddConnectionString("tables"); var myService = builder.AddProject() @@ -136,7 +136,7 @@ var myService = builder.AddProject() The `AddTables` method will add an Azure Storage table resource to the builder. Or `AddConnectionString` can be used to read the connection information from the AppHost's configuration (for example, from "user secrets") under the `ConnectionStrings:tables` config key. The `WithReference` method passes that connection information into a connection string named `tables` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: ```csharp -builder.AddAzureTableClient("tables"); +builder.AddAzureTableServiceClient("tables"); ``` ## Additional documentation diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs b/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs index 0669e3d23ea..7806808889e 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs +++ b/src/Components/Aspire.Azure.Storage.Blobs/AspireBlobStorageExtensions.cs @@ -33,7 +33,7 @@ public static partial class AspireBlobStorageExtensions /// /// Neither nor is provided. /// - public static void AddAzureBlobClient( + public static void AddAzureBlobServiceClient( this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, @@ -45,6 +45,20 @@ public static void AddAzureBlobClient( new BlobStorageComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); } + /// + [Obsolete("Use AddAzureBlobServiceClient instead. This method will be removed in a future version.")] + public static void AddAzureBlobClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + AddAzureBlobServiceClient(builder, connectionName, configureSettings, configureClientBuilder); + } + /// /// Registers as a singleton for given in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. @@ -65,7 +79,7 @@ public static void AddAzureBlobClient( /// /// Neither nor is provided. /// - public static void AddKeyedAzureBlobClient( + public static void AddKeyedAzureBlobServiceClient( this IHostApplicationBuilder builder, string name, Action? configureSettings = null, @@ -77,6 +91,20 @@ public static void AddKeyedAzureBlobClient( new BlobStorageComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); } + /// + [Obsolete("Use AddKeyedAzureBlobServiceClient instead. This method will be removed in a future version.")] + public static void AddKeyedAzureBlobClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + AddKeyedAzureBlobServiceClient(builder, name, configureSettings, configureClientBuilder); + } + /// /// Registers as a singleton in the services provided by the . /// Enables retries, corresponding health check, logging and telemetry. diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs index cb581a1d1d0..2727d85333c 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs +++ b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs @@ -27,7 +27,7 @@ void IConnectionStringSettings.ParseConnectionString(string? connectionString) return; } - DbConnectionStringBuilder builder = new() { ConnectionString = connectionString }; + var builder = new DbConnectionStringBuilder() { ConnectionString = connectionString }; if (builder.TryGetValue("ContainerName", out var containerName)) { @@ -54,4 +54,4 @@ void IConnectionStringSettings.ParseConnectionString(string? connectionString) ConnectionString = connectionString; } } -} +} \ No newline at end of file diff --git a/src/Components/Aspire.Azure.Storage.Blobs/README.md b/src/Components/Aspire.Azure.Storage.Blobs/README.md index c35061881d1..b561242ff22 100644 --- a/src/Components/Aspire.Azure.Storage.Blobs/README.md +++ b/src/Components/Aspire.Azure.Storage.Blobs/README.md @@ -22,7 +22,7 @@ dotnet add package Aspire.Azure.Storage.Blobs In the _AppHost.cs_ file of your project, call the `AddAzureBlobClient` extension method to register a `BlobServiceClient` for use via the dependency injection container. The method takes a connection name parameter. ```csharp -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); ``` You can then retrieve the `BlobServiceClient` instance using dependency injection. For example, to retrieve the client from a Web API controller: @@ -44,10 +44,10 @@ The .NET Aspire Azure Storage Blobs library provides multiple options to configu ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureBlobClient()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureBlobServiceClient()`: ```csharp -builder.AddAzureBlobClient("blobsConnectionName"); +builder.AddAzureBlobServiceClient("blobsConnectionName"); ``` And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The .NET Aspire Azure Storage Blobs library supports [Microsoft.Extensions.Confi You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureBlobClient("blobs", settings => settings.HealthChecks = false); +builder.AddAzureBlobServiceClient("blobs", settings => settings.HealthChecks = false); ``` You can also setup the [BlobClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.blobs.blobclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureBlobClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureBlobClient("blobs", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureBlobServiceClient("blobs", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions @@ -126,7 +126,7 @@ Then, in the _AppHost.cs_ file of `AppHost`, add a Blob Storage connection and c ```csharp var blobs = builder.ExecutionContext.IsPublishMode - ? builder.AddAzureStorage("storage").AddBlobs("blobs") + ? builder.AddAzureStorage("storage").AddBlobService("blobs") : builder.AddConnectionString("blobs"); var myService = builder.AddProject() @@ -136,7 +136,7 @@ var myService = builder.AddProject() The `AddBlobs` method adds an Azure Storage blob resource to the builder. Or `AddConnectionString` method can be used be used to read connection information from the AppHost's configuration (for example, from "user secrets") under the `ConnectionStrings:blobs` config key. The `WithReference` method passes that connection information into a connection string named `blobs` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: ```csharp -builder.AddAzureBlobClient("blobs"); +builder.AddAzureBlobServiceClient("blobs"); ``` ## Additional documentation diff --git a/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj b/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj index cdcdc602d5b..dbb63dbb6c2 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj +++ b/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs index b4cc6bf2c3f..a6c2789af8d 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs +++ b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs @@ -6,6 +6,7 @@ using Azure.Core; using Azure.Core.Extensions; using Azure.Storage.Queues; +using Azure.Storage.Queues.Specialized; using HealthChecks.Azure.Storage.Queues; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -32,6 +33,29 @@ public static class AspireQueueStorageExtensions /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Storage:Queues" section. /// Thrown when neither nor is provided. + public static void AddAzureQueueServiceClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new StorageQueuesComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Storage:Queues" section. + /// Thrown when neither nor is provided. + [Obsolete("Use AddAzureQueueServiceClient instead.")] public static void AddAzureQueueClient( this IHostApplicationBuilder builder, string connectionName, @@ -41,7 +65,7 @@ public static void AddAzureQueueClient( ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(connectionName); - new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + AddAzureQueueServiceClient(builder, connectionName, configureSettings, configureClientBuilder); } /// @@ -54,6 +78,29 @@ public static void AddAzureQueueClient( /// An optional method that can be used for customizing the . /// Reads the configuration from "Aspire:Azure:Storage:Queues:{name}" section. /// Thrown when neither nor is provided. + public static void AddKeyedAzureQueueServiceClient( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + new StorageQueuesComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Azure:Storage:Queues:{name}" section. + /// Thrown when neither nor is provided. + [Obsolete("Use AddKeyedAzureQueueServiceClient instead.")] public static void AddKeyedAzureQueueClient( this IHostApplicationBuilder builder, string name, @@ -62,11 +109,76 @@ public static void AddKeyedAzureQueueClient( { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + + AddKeyedAzureQueueServiceClient(builder, name, configureSettings, configureClientBuilder); + } + + /// + /// Registers as a singleton in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// + /// An optional method that can be used for customizing the . + /// It's invoked after the settings are read from the configuration. + /// + /// + /// An optional method that can be used for customizing the . + /// + /// Reads the configuration from "Aspire:Azure:Storage:Queues:{name}" section. + /// + /// Neither nor is provided. + /// - or - + /// is not provided in the configuration section. + /// + public static void AddAzureQueue( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton in the services provided by the . + /// Enables retries, corresponding health check, logging and telemetry. + /// + /// The to read config from and add services to. + /// + /// The name of the component, which is used as the of the service and also to retrieve + /// the connection string from the ConnectionStrings configuration section. + /// + /// + /// An optional method that can be used for customizing the . + /// It's invoked after the settings are read from the configuration. + /// + /// + /// An optional method that can be used for customizing the . + /// + /// Reads the configuration from "Aspire:Azure:Storage:Queues:{name}" section. + /// + /// Neither nor is provided. + /// - or - + /// is not provided in the configuration section. + /// + public static void AddKeyedAzureQueue( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action>? configureClientBuilder = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name); } - private sealed class StorageQueueComponent : AzureComponent + private sealed class StorageQueuesComponent : AzureComponent { protected override IAzureClientBuilder AddClient( AzureClientFactoryBuilder azureFactoryBuilder, AzureStorageQueuesSettings settings, string connectionName, @@ -115,4 +227,59 @@ protected override bool GetMetricsEnabled(AzureStorageQueuesSettings settings) protected override bool GetTracingEnabled(AzureStorageQueuesSettings settings) => !settings.DisableTracing; } + + private sealed partial class StorageQueueComponent : AzureComponent + { + protected override IAzureClientBuilder AddClient( + AzureClientFactoryBuilder azureFactoryBuilder, AzureStorageQueueSettings settings, string connectionName, string configurationSectionName) + { + return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory((options, cred) => + { + if (string.IsNullOrEmpty(settings.QueueName)) + { + throw new InvalidOperationException($"The connection string '{connectionName}' does not exist or is missing the queue name."); + } + + var connectionString = settings.ConnectionString; + if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null) + { + throw new InvalidOperationException($"A QueueClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a 'ConnectionString' or 'ServiceUri' in the '{configurationSectionName}' configuration section."); + } + + var queueServiceClient = !string.IsNullOrEmpty(connectionString) ? new QueueServiceClient(connectionString, options) : + cred is not null ? new QueueServiceClient(settings.ServiceUri, cred, options) : + new QueueServiceClient(settings.ServiceUri, options); + + var client = queueServiceClient.GetQueueClient(settings.QueueName); + return client; + }, requiresCredential: false); + } + + protected override void BindClientOptionsToConfiguration(IAzureClientBuilder clientBuilder, IConfiguration configuration) + { +#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works + clientBuilder.ConfigureOptions(options => configuration.Bind(options)); +#pragma warning restore IDE0200 + } + + protected override void BindSettingsToConfiguration(AzureStorageQueueSettings settings, IConfiguration configuration) + { + configuration.Bind(settings); + } + + protected override IHealthCheck CreateHealthCheck(QueueClient client, AzureStorageQueueSettings settings) + => new AzureQueueStorageHealthCheck(client.GetParentQueueServiceClient(), new AzureQueueStorageHealthCheckOptions { QueueName = client.Name }); + + protected override bool GetHealthCheckEnabled(AzureStorageQueueSettings settings) + => !settings.DisableHealthChecks; + + protected override TokenCredential? GetTokenCredential(AzureStorageQueueSettings settings) + => settings.Credential; + + protected override bool GetMetricsEnabled(AzureStorageQueueSettings settings) + => false; + + protected override bool GetTracingEnabled(AzureStorageQueueSettings settings) + => !settings.DisableTracing; + } } diff --git a/src/Components/Aspire.Azure.Storage.Queues/AssemblyInfo.cs b/src/Components/Aspire.Azure.Storage.Queues/AssemblyInfo.cs index 8b05276196f..0003ad5f726 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/AssemblyInfo.cs +++ b/src/Components/Aspire.Azure.Storage.Queues/AssemblyInfo.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; using Aspire; using Aspire.Azure.Storage.Queues; using Azure.Storage.Queues; @@ -12,3 +13,5 @@ "Azure", "Azure.Core", "Azure.Identity")] + +[assembly: InternalsVisibleTo("Aspire.Azure.Storage.Queues.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004b86c4cb78549b34bab61a3b1800e23bfeb5b3ec390074041536a7e3cbd97f5f04cf0f857155a8928eaa29ebfd11cfbbad3ba70efea7bda3226c6a8d370a4cd303f714486b6ebc225985a638471e6ef571cc92a4613c00b8fa65d61ccee0cbe5f36330c9a01f4183559f1bef24cc2917c6d913e3a541333a1d05d9bed22b38cb")] diff --git a/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueueSettings.cs b/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueueSettings.cs new file mode 100644 index 00000000000..562f0c776a7 --- /dev/null +++ b/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueueSettings.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// 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.Queues; + +/// +/// Provides the client configuration settings for connecting to an Azure Storage queue. +/// +public sealed partial class AzureStorageQueueSettings : AzureStorageQueuesSettings, IConnectionStringSettings +{ + [GeneratedRegex(@"(?i)QueueName\s*=\s*([^;]+);?", RegexOptions.IgnoreCase)] + private static partial Regex QueueNameRegex(); + + /// + /// Gets or sets the name of the blob container. + /// + public string? QueueName { get; set; } + + void IConnectionStringSettings.ParseConnectionString(string? connectionString) + { + if (string.IsNullOrEmpty(connectionString)) + { + return; + } + + var builder = new DbConnectionStringBuilder() { ConnectionString = connectionString }; + + if (builder.TryGetValue("QueueName", out var queueName)) + { + QueueName = queueName?.ToString(); + + // Remove the QueueName property from the connection string as QueueServiceClient would fail to parse it. + connectionString = QueueNameRegex().Replace(connectionString, ""); + + // NB: we can't remove QueueName by using the DbConnectionStringBuilder as it would escape the AccountKey value + // when the connection string is built and QueueServiceClient doesn't support escape sequences. + } + + // Connection string built from a URI? E.g., Endpoint=https://{account_name}.queue.core.windows.net;QueueName=...; + if (builder.TryGetValue("Endpoint", out var endpoint) && endpoint is string) + { + if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var uri)) + { + ServiceUri = uri; + } + } + else + { + // Otherwise preserve the existing connection string + ConnectionString = connectionString; + } + } +} diff --git a/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueuesSettings.cs b/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueuesSettings.cs index acedbf8b329..b0b1d041c38 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueuesSettings.cs +++ b/src/Components/Aspire.Azure.Storage.Queues/AzureStorageQueuesSettings.cs @@ -9,7 +9,7 @@ namespace Aspire.Azure.Storage.Queues; /// /// Provides the client configuration settings for connecting to Azure Storage Queues. /// -public sealed class AzureStorageQueuesSettings : IConnectionStringSettings +public class AzureStorageQueuesSettings : IConnectionStringSettings { /// /// Gets or sets the connection string used to connect to the blob service. @@ -52,16 +52,18 @@ public sealed class AzureStorageQueuesSettings : 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/src/Components/Aspire.Azure.Storage.Queues/README.md b/src/Components/Aspire.Azure.Storage.Queues/README.md index a0abb3e5022..4f352401639 100644 --- a/src/Components/Aspire.Azure.Storage.Queues/README.md +++ b/src/Components/Aspire.Azure.Storage.Queues/README.md @@ -22,7 +22,7 @@ dotnet add package Aspire.Azure.Storage.Queues In the _AppHost.cs_ file of your project, call the `AddAzureQueueClient` extension method to register a `QueueServiceClient` for use via the dependency injection container. The method takes a connection name parameter. ```csharp -builder.AddAzureQueueClient("queue"); +builder.AddAzureQueueServiceClient("queue"); ``` You can then retrieve the `QueueServiceClient` instance using dependency injection. For example, to retrieve the client from a Web API controller: @@ -44,10 +44,10 @@ The .NET Aspire Azure Storage Queues library provides multiple options to config ### Use a connection string -When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureQueueClient()`: +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureQueueServiceClient()`: ```csharp -builder.AddAzureQueueClient("queueConnectionName"); +builder.AddAzureQueueServiceClient("queueConnectionName"); ``` And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: @@ -105,13 +105,13 @@ The .NET Aspire Azure Storage Queues library supports [Microsoft.Extensions.Conf You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp -builder.AddAzureQueueClient("queue", settings => settings.DisableHealthChecks = true); +builder.AddAzureQueueServiceClient("queue", settings => settings.DisableHealthChecks = true); ``` You can also setup the [QueueClientOptions](https://learn.microsoft.com/dotnet/api/azure.storage.queues.queueclientoptions) using the optional `Action> configureClientBuilder` parameter of the `AddAzureQueueClient` method. For example, to set the first part of "User-Agent" headers for all requests issues by this client: ```csharp -builder.AddAzureQueueClient("queue", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); +builder.AddAzureQueueServiceClient("queue", configureClientBuilder: clientBuilder => clientBuilder.ConfigureOptions(options => options.Diagnostics.ApplicationId = "myapp")); ``` ## AppHost extensions @@ -126,7 +126,7 @@ Then, in the _AppHost.cs_ file of `AppHost`, add a Storage Queue connection and ```csharp var queue = builder.ExecutionContext.IsPublishMode - ? builder.AddAzureStorage("storage").AddQueues("queue") + ? builder.AddAzureStorage("storage").AddQueueService("queue") : builder.AddConnectionString("queue"); var myService = builder.AddProject() @@ -136,7 +136,7 @@ var myService = builder.AddProject() The `AddQueues` method adds an Azure Storage queue to the builder. Or `AddConnectionString` can be used to read connection information from the AppHost's configuration (for example, from "user secrets") under the `ConnectionStrings:queue` config key. The `WithReference` method passes that connection information into a connection string named `queue` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: ```csharp -builder.AddAzureQueueClient("queue"); +builder.AddAzureQueueServiceClient("queue"); ``` ## Additional documentation diff --git a/tests/Aspire.Azure.Data.Tables.Tests/AspireTablesExtensionsTests.cs b/tests/Aspire.Azure.Data.Tables.Tests/AspireTablesExtensionsTests.cs index d897d0aad21..5c0f265ac61 100644 --- a/tests/Aspire.Azure.Data.Tables.Tests/AspireTablesExtensionsTests.cs +++ b/tests/Aspire.Azure.Data.Tables.Tests/AspireTablesExtensionsTests.cs @@ -25,11 +25,11 @@ public void ReadsFromConnectionStringsCorrectly(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureTableClient("tables"); + builder.AddKeyedAzureTableServiceClient("tables"); } else { - builder.AddAzureTableClient("tables"); + builder.AddAzureTableServiceClient("tables"); } using var host = builder.Build(); @@ -52,11 +52,11 @@ public void ConnectionStringCanBeSetInCode(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureTableClient("tables", settings => settings.ConnectionString = ConnectionString); + builder.AddKeyedAzureTableServiceClient("tables", settings => settings.ConnectionString = ConnectionString); } else { - builder.AddAzureTableClient("tables", settings => settings.ConnectionString = ConnectionString); + builder.AddAzureTableServiceClient("tables", settings => settings.ConnectionString = ConnectionString); } using var host = builder.Build(); @@ -82,11 +82,11 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureTableClient("tables"); + builder.AddKeyedAzureTableServiceClient("tables"); } else { - builder.AddAzureTableClient("tables"); + builder.AddAzureTableServiceClient("tables"); } using var host = builder.Build(); @@ -110,11 +110,11 @@ public void ServiceUriWorksInConnectionStrings(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureTableClient("tables"); + builder.AddKeyedAzureTableServiceClient("tables"); } else { - builder.AddAzureTableClient("tables"); + builder.AddAzureTableServiceClient("tables"); } using var host = builder.Build(); @@ -135,9 +135,9 @@ public void CanAddMultipleKeyedServices() new KeyValuePair("ConnectionStrings:tables3", "AccountName=account3;AccountKey=fake") ]); - builder.AddAzureTableClient("tables1"); - builder.AddKeyedAzureTableClient("tables2"); - builder.AddKeyedAzureTableClient("tables3"); + builder.AddAzureTableServiceClient("tables1"); + builder.AddKeyedAzureTableServiceClient("tables2"); + builder.AddKeyedAzureTableServiceClient("tables3"); using var host = builder.Build(); diff --git a/tests/Aspire.Azure.Data.Tables.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Data.Tables.Tests/ConformanceTests.cs index e351c3825c3..e9fe84e3695 100644 --- a/tests/Aspire.Azure.Data.Tables.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Data.Tables.Tests/ConformanceTests.cs @@ -77,11 +77,11 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureTableClient("tables", ConfigureCredentials); + builder.AddAzureTableServiceClient("tables", ConfigureCredentials); } else { - builder.AddKeyedAzureTableClient(key, ConfigureCredentials); + builder.AddKeyedAzureTableServiceClient(key, ConfigureCredentials); } void ConfigureCredentials(AzureDataTablesSettings settings) diff --git a/tests/Aspire.Azure.Data.Tables.Tests/DataTablesPublicApiTests.cs b/tests/Aspire.Azure.Data.Tables.Tests/DataTablesPublicApiTests.cs index d5f088bf9b5..e2ee233b264 100644 --- a/tests/Aspire.Azure.Data.Tables.Tests/DataTablesPublicApiTests.cs +++ b/tests/Aspire.Azure.Data.Tables.Tests/DataTablesPublicApiTests.cs @@ -14,7 +14,7 @@ public void AddAzureTableClientShouldThrowWhenBuilderIsNull() IHostApplicationBuilder builder = null!; const string connectionName = "tables"; - var action = () => builder.AddAzureTableClient(connectionName); + var action = () => builder.AddAzureTableServiceClient(connectionName); var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -28,7 +28,7 @@ public void AddAzureTableClientShouldThrowWhenConnectionNameIsNullOrEmpty(bool i var builder = Host.CreateEmptyApplicationBuilder(null); var connectionName = isNull ? null! : string.Empty; - var action = () => builder.AddAzureTableClient(connectionName); + var action = () => builder.AddAzureTableServiceClient(connectionName); var exception = isNull ? Assert.Throws(action) @@ -42,7 +42,7 @@ public void AddKeyedAzureTableClientShouldThrowWhenBuilderIsNull() IHostApplicationBuilder builder = null!; const string name = "tables"; - var action = () => builder.AddKeyedAzureTableClient(name); + var action = () => builder.AddKeyedAzureTableServiceClient(name); var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -56,7 +56,7 @@ public void AddKeyedAzureTableClientShouldThrowWhenNameIsNullOrEmpty(bool isNull var builder = Host.CreateEmptyApplicationBuilder(null); var name = isNull ? null! : string.Empty; - var action = () => builder.AddKeyedAzureTableClient(name); + var action = () => builder.AddKeyedAzureTableServiceClient(name); var exception = isNull ? Assert.Throws(action) diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/AspireBlobStorageExtensionsTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/AspireBlobStorageExtensionsTests.cs index d1ee2f6cdc6..34bfe089c15 100644 --- a/tests/Aspire.Azure.Storage.Blobs.Tests/AspireBlobStorageExtensionsTests.cs +++ b/tests/Aspire.Azure.Storage.Blobs.Tests/AspireBlobStorageExtensionsTests.cs @@ -25,11 +25,11 @@ public void ReadsFromConnectionStringsCorrectly(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureBlobClient("blob"); + builder.AddKeyedAzureBlobServiceClient("blob"); } else { - builder.AddAzureBlobClient("blob"); + builder.AddAzureBlobServiceClient("blob"); } using var host = builder.Build(); @@ -52,11 +52,11 @@ public void ConnectionStringCanBeSetInCode(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureBlobClient("blob", settings => settings.ConnectionString = ConnectionString); + builder.AddKeyedAzureBlobServiceClient("blob", settings => settings.ConnectionString = ConnectionString); } else { - builder.AddAzureBlobClient("blob", settings => settings.ConnectionString = ConnectionString); + builder.AddAzureBlobServiceClient("blob", settings => settings.ConnectionString = ConnectionString); } using var host = builder.Build(); @@ -82,11 +82,11 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureBlobClient("blob"); + builder.AddKeyedAzureBlobServiceClient("blob"); } else { - builder.AddAzureBlobClient("blob"); + builder.AddAzureBlobServiceClient("blob"); } using var host = builder.Build(); @@ -110,11 +110,11 @@ public void ServiceUriWorksInConnectionStrings(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureBlobClient("blob"); + builder.AddKeyedAzureBlobServiceClient("blob"); } else { - builder.AddAzureBlobClient("blob"); + builder.AddAzureBlobServiceClient("blob"); } using var host = builder.Build(); @@ -135,9 +135,9 @@ public void CanAddMultipleKeyedServices() new KeyValuePair("ConnectionStrings:blob3", "https://aspirestoragetests3.blob.core.windows.net/") ]); - builder.AddAzureBlobClient("blob1"); - builder.AddKeyedAzureBlobClient("blob2"); - builder.AddKeyedAzureBlobClient("blob3"); + builder.AddAzureBlobServiceClient("blob1"); + builder.AddKeyedAzureBlobServiceClient("blob2"); + builder.AddKeyedAzureBlobServiceClient("blob3"); using var host = builder.Build(); diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs index a53c74a5541..67640d50805 100644 --- a/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Storage.Blobs.Tests/ConformanceTests.cs @@ -81,11 +81,11 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureBlobClient("blob", ConfigureCredentials); + builder.AddAzureBlobServiceClient("blob", ConfigureCredentials); } else { - builder.AddKeyedAzureBlobClient(key, ConfigureCredentials); + builder.AddKeyedAzureBlobServiceClient(key, ConfigureCredentials); } void ConfigureCredentials(AzureStorageBlobsSettings settings) diff --git a/tests/Aspire.Azure.Storage.Blobs.Tests/StorageBlobsPublicApiTests.cs b/tests/Aspire.Azure.Storage.Blobs.Tests/StorageBlobsPublicApiTests.cs index a53b4f2b1a7..199148b8d39 100644 --- a/tests/Aspire.Azure.Storage.Blobs.Tests/StorageBlobsPublicApiTests.cs +++ b/tests/Aspire.Azure.Storage.Blobs.Tests/StorageBlobsPublicApiTests.cs @@ -14,7 +14,7 @@ public void AddAzureBlobClientShouldThrowWhenBuilderIsNull() IHostApplicationBuilder builder = null!; const string connectionName = "blobs"; - var action = () => builder.AddAzureBlobClient(connectionName); + var action = () => builder.AddAzureBlobServiceClient(connectionName); var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -28,7 +28,7 @@ public void AddAzureBlobClientShouldThrowWhenConnectionNameIsNullOrEmpty(bool is var builder = Host.CreateEmptyApplicationBuilder(null); var connectionName = isNull ? null! : string.Empty; - var action = () => builder.AddAzureBlobClient(connectionName); + var action = () => builder.AddAzureBlobServiceClient(connectionName); var exception = isNull ? Assert.Throws(action) @@ -42,7 +42,7 @@ public void AddKeyedAzureBlobClientShouldThrowWhenBuilderIsNull() IHostApplicationBuilder builder = null!; const string name = "blobs"; - var action = () => builder.AddKeyedAzureBlobClient(name); + var action = () => builder.AddKeyedAzureBlobServiceClient(name); var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -56,7 +56,7 @@ public void AddKeyedAzureBlobClientShouldThrowWhenNameIsNullOrEmpty(bool isNull) var builder = Host.CreateEmptyApplicationBuilder(null); var name = isNull ? null! : string.Empty; - var action = () => builder.AddKeyedAzureBlobClient(name); + var action = () => builder.AddKeyedAzureBlobServiceClient(name); var exception = isNull ? Assert.Throws(action) diff --git a/tests/Aspire.Azure.Storage.Queues.Tests/AspireQueueStorageExtensionsTests.cs b/tests/Aspire.Azure.Storage.Queues.Tests/AspireQueueStorageExtensionsTests.cs index 8eb595dbb45..c9c373d7cd2 100644 --- a/tests/Aspire.Azure.Storage.Queues.Tests/AspireQueueStorageExtensionsTests.cs +++ b/tests/Aspire.Azure.Storage.Queues.Tests/AspireQueueStorageExtensionsTests.cs @@ -25,11 +25,11 @@ public void ReadsFromConnectionStringsCorrectly(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureQueueClient("queue"); + builder.AddKeyedAzureQueueServiceClient("queue"); } else { - builder.AddAzureQueueClient("queue"); + builder.AddAzureQueueServiceClient("queue"); } using var host = builder.Build(); @@ -52,11 +52,11 @@ public void ConnectionStringCanBeSetInCode(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureQueueClient("queue", settings => settings.ConnectionString = ConnectionString); + builder.AddKeyedAzureQueueServiceClient("queue", settings => settings.ConnectionString = ConnectionString); } else { - builder.AddAzureQueueClient("queue", settings => settings.ConnectionString = ConnectionString); + builder.AddAzureQueueServiceClient("queue", settings => settings.ConnectionString = ConnectionString); } using var host = builder.Build(); @@ -82,11 +82,11 @@ public void ConnectionNameWinsOverConfigSection(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureQueueClient("queue"); + builder.AddKeyedAzureQueueServiceClient("queue"); } else { - builder.AddAzureQueueClient("queue"); + builder.AddAzureQueueServiceClient("queue"); } using var host = builder.Build(); @@ -110,11 +110,11 @@ public void ServiceUriWorksInConnectionStrings(bool useKeyed) if (useKeyed) { - builder.AddKeyedAzureQueueClient("queue"); + builder.AddKeyedAzureQueueServiceClient("queue"); } else { - builder.AddAzureQueueClient("queue"); + builder.AddAzureQueueServiceClient("queue"); } using var host = builder.Build(); @@ -135,9 +135,9 @@ public void CanAddMultipleKeyedServices() new KeyValuePair("ConnectionStrings:queue3", "https://aspirestoragetests3.queue.core.windows.net") ]); - builder.AddAzureQueueClient("queue1"); - builder.AddKeyedAzureQueueClient("queue2"); - builder.AddKeyedAzureQueueClient("queue3"); + builder.AddAzureQueueServiceClient("queue1"); + builder.AddKeyedAzureQueueServiceClient("queue2"); + builder.AddKeyedAzureQueueServiceClient("queue3"); using var host = builder.Build(); diff --git a/tests/Aspire.Azure.Storage.Queues.Tests/AzureStorageQueueSettingsTests.cs b/tests/Aspire.Azure.Storage.Queues.Tests/AzureStorageQueueSettingsTests.cs new file mode 100644 index 00000000000..c90a10aa9db --- /dev/null +++ b/tests/Aspire.Azure.Storage.Queues.Tests/AzureStorageQueueSettingsTests.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Azure.Common; +using Aspire.Azure.Storage.Queues; +using Xunit; + +namespace Aspire.Hosting.Azure.Tests; + +public class AzureStorageQueueSettingsTests +{ + private const string EmulatorConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;QueueEndpoint=http://127.0.0.1:10000/devstoreaccount1"; + + [Fact] + public void ParseConnectionString_invalid_input_results_in_AE() + { + var settings = new AzureStorageQueueSettings(); + string connectionString = "InvalidConnectionString"; + + Assert.Throws(() => ((IConnectionStringSettings)settings).ParseConnectionString(connectionString)); + } + + [Theory] + [InlineData("Endpoint=https://example.queueName.core.windows.net;QueueName=my-queue")] + [InlineData("Endpoint=https://example.queueName.core.windows.net;QueueName=my-queue;ExtraParam=value")] + [InlineData("endpoint=https://example.queueName.core.windows.net;queuename=my-queue")] + [InlineData("ENDPOINT=https://example.queueName.core.windows.net;QUEUENAME=my-queue")] + [InlineData("Endpoint=\"https://example.queueName.core.windows.net\";QueueName=\"my-queue\"")] + public void ParseConnectionString_With_ServiceUri(string connectionString) + { + var settings = new AzureStorageQueueSettings(); + + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + + Assert.Equal("https://example.queuename.core.windows.net/", settings.ServiceUri?.ToString()); + Assert.Equal("my-queue", settings.QueueName); + } + + [Theory] + [InlineData($"{EmulatorConnectionString};QueueName=my-queue")] + [InlineData($"{EmulatorConnectionString};QueueName=\"my-queue\"")] + public void ParseConnectionString_With_ConnectionString(string connectionString) + { + var settings = new AzureStorageQueueSettings(); + + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + + Assert.Contains(EmulatorConnectionString, settings.ConnectionString, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("QueueName", settings.ConnectionString, StringComparison.OrdinalIgnoreCase); + Assert.Equal("my-queue", settings.QueueName); + Assert.Null(settings.ServiceUri); + } + + [Theory] + [InlineData($"Endpoint=not-a-uri;QueueName=my-queue")] + public void ParseConnectionString_With_NotAUri(string connectionString) + { + var settings = new AzureStorageQueueSettings(); + + ((IConnectionStringSettings)settings).ParseConnectionString(connectionString); + + Assert.True(string.IsNullOrEmpty(settings.ConnectionString)); + Assert.Equal("my-queue", settings.QueueName); + Assert.Null(settings.ServiceUri); + } +} diff --git a/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs b/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs index 006803a8083..dd122233e9f 100644 --- a/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs +++ b/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs @@ -24,6 +24,8 @@ public class ConformanceTests : ConformanceTests "Azure.Storage.Queues.QueueClient"; + protected override bool CheckOptionClassSealed => false; + protected override string[] RequiredLogCategories => new string[] { "Azure.Core", @@ -80,11 +82,11 @@ protected override void RegisterComponent(HostApplicationBuilder builder, Action { if (key is null) { - builder.AddAzureQueueClient("queue", ConfigureCredentials); + builder.AddAzureQueueServiceClient("queue", ConfigureCredentials); } else { - builder.AddKeyedAzureQueueClient(key, ConfigureCredentials); + builder.AddKeyedAzureQueueServiceClient(key, ConfigureCredentials); } void ConfigureCredentials(AzureStorageQueuesSettings settings) diff --git a/tests/Aspire.Azure.Storage.Queues.Tests/StorageQueuesPublicApiTests.cs b/tests/Aspire.Azure.Storage.Queues.Tests/StorageQueuesPublicApiTests.cs index ff87f2030db..958c2bec5b6 100644 --- a/tests/Aspire.Azure.Storage.Queues.Tests/StorageQueuesPublicApiTests.cs +++ b/tests/Aspire.Azure.Storage.Queues.Tests/StorageQueuesPublicApiTests.cs @@ -8,13 +8,71 @@ namespace Aspire.Azure.Storage.Queues.Tests; public class StorageQueuesPublicApiTests { + [Fact] + public void AddAzureQueueServiceClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + const string connectionName = "queue"; + + var action = () => builder.AddAzureQueueServiceClient(connectionName); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddAzureQueueServiceClientShouldThrowWhenConnectionNameIsNullOrEmpty(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + var connectionName = isNull ? null! : string.Empty; + + var action = () => builder.AddAzureQueueServiceClient(connectionName); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(connectionName), exception.ParamName); + } + + [Fact] + public void AddKeyedAzureQueueServiceClientShouldThrowWhenBuilderIsNull() + { + IHostApplicationBuilder builder = null!; + const string name = "queue"; + + var action = () => builder.AddKeyedAzureQueueServiceClient(name); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddKeyedAzureQueueServiceClientShouldThrowWhenNameIsNullOrEmpty(bool isNull) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + var name = isNull ? null! : string.Empty; + + var action = () => builder.AddKeyedAzureQueueServiceClient(name); + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(name), exception.ParamName); + } + [Fact] public void AddAzureQueueClientShouldThrowWhenBuilderIsNull() { IHostApplicationBuilder builder = null!; const string connectionName = "queue"; - var action = () => builder.AddAzureQueueClient(connectionName); +#pragma warning disable CS0618 // Type or member is obsolete + var action = () => builder.AddAzureQueueServiceClient(connectionName); +#pragma warning restore CS0618 // Type or member is obsolete var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -28,7 +86,9 @@ public void AddAzureQueueClientShouldThrowWhenConnectionNameIsNullOrEmpty(bool i var builder = Host.CreateEmptyApplicationBuilder(null); var connectionName = isNull ? null! : string.Empty; - var action = () => builder.AddAzureQueueClient(connectionName); +#pragma warning disable CS0618 // Type or member is obsolete + var action = () => builder.AddAzureQueueServiceClient(connectionName); +#pragma warning restore CS0618 // Type or member is obsolete var exception = isNull ? Assert.Throws(action) @@ -42,7 +102,9 @@ public void AddKeyedAzureQueueClientShouldThrowWhenBuilderIsNull() IHostApplicationBuilder builder = null!; const string name = "queue"; - var action = () => builder.AddKeyedAzureQueueClient(name); +#pragma warning disable CS0618 // Type or member is obsolete + var action = () => builder.AddKeyedAzureQueueServiceClient(name); +#pragma warning restore CS0618 // Type or member is obsolete var exception = Assert.Throws(action); Assert.Equal(nameof(builder), exception.ParamName); @@ -56,7 +118,9 @@ public void AddKeyedAzureQueueClientShouldThrowWhenNameIsNullOrEmpty(bool isNull var builder = Host.CreateEmptyApplicationBuilder(null); var name = isNull ? null! : string.Empty; - var action = () => builder.AddKeyedAzureQueueClient(name); +#pragma warning disable CS0618 // Type or member is obsolete + var action = () => builder.AddKeyedAzureQueueServiceClient(name); +#pragma warning restore CS0618 // Type or member is obsolete var exception = isNull ? Assert.Throws(action) diff --git a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj index 79baf939b6c..e2b27e25f64 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj +++ b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj @@ -38,6 +38,7 @@ + diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index eb37c7dd26e..b89f01f072b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -275,7 +275,7 @@ public async Task ProjectWithManyReferenceTypes() var rawCs = builder.AddConnectionString("cs"); - var blob = builder.AddAzureStorage("storage").AddBlobs("blobs"); + var blob = builder.AddAzureStorage("storage").AddBlobService("blobs"); // Secret parameters (_ isn't supported and will be replaced by -) var secretValue = builder.AddParameter("value0", "x", secret: true); @@ -352,7 +352,7 @@ public async Task ProjectWithManyReferenceTypesAndContainerAppEnvironment() var rawCs = builder.AddConnectionString("cs"); - var blob = builder.AddAzureStorage("storage").AddBlobs("blobs"); + var blob = builder.AddAzureStorage("storage").AddBlobService("blobs"); // Secret parameters (_ isn't supported and will be replaced by -) var secretValue = builder.AddParameter("value0", "x", secret: true); @@ -924,7 +924,7 @@ public async Task RoleAssignmentsWithAsExisting() var storage = builder.AddAzureStorage("storage") .PublishAsExisting(storageName, storageRG); - var blobs = storage.AddBlobs("blobs"); + var blobs = storage.AddBlobService("blobs"); builder.AddProject("api", launchProfileName: null) .WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDataReader); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs index 15a1cf50f42..1dfaa99c244 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEnvironmentResourceTests.cs @@ -109,7 +109,7 @@ public async Task PublishAsync_GeneratesMainBicep_WithSnapshots() }; c.Add(output); }) - .AddBlobs("blobs"); + .AddBlobService("blobs"); builder.AddAzureInfrastructure("mod", infra => { }) .WithParameter("pgdb", pgdb.Resource.ConnectionStringExpression); builder.AddContainer("myapp", "mcr.microsoft.com/dotnet/aspnet:8.0") @@ -152,7 +152,7 @@ public async Task AzurePublishingContext_CapturesParametersAndOutputsCorrectly_W }; c.Add(output); }) - .AddBlobs("blobs"); + .AddBlobService("blobs"); builder.AddProject("fe", launchProfileName: null) .WithEnvironment("BLOB_CONTAINER_URL", $"{blobs}/container") .WithReference(cosmos); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs index eb6f0e50c24..91896ddb9c4 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs @@ -52,7 +52,7 @@ public async Task AppliesDefaultRoleAssignmentsInRunModeIfReferenced(bool addCon } var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("blobs"); + var blobs = storage.AddBlobService("blobs"); var api = builder.AddProject("api", launchProfileName: null) .WithReference(blobs); @@ -93,7 +93,7 @@ public async Task AppliesRoleAssignmentsInRunMode(DistributedApplicationOperatio builder.AddAzureContainerAppEnvironment("env"); var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("blobs"); + var blobs = storage.AddBlobService("blobs"); var api = builder.AddProject("api", launchProfileName: null) .WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDelegator, StorageBuiltInRole.StorageBlobDataReader) @@ -162,7 +162,7 @@ public async Task FindsAzureReferencesFromArguments() builder.AddAzureContainerAppEnvironment("env"); var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("blobs"); + var blobs = storage.AddBlobService("blobs"); // the project doesn't WithReference or WithRoleAssignments, so it should get the default role assignments var api = builder.AddProject("api", launchProfileName: null) diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs index df14ddaaa3a..8b9b331d76f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; +using Azure.Storage.Queues; namespace Aspire.Hosting.Azure.Tests; @@ -30,9 +31,9 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForBlobsBlocksDependentReso .RunAsEmulator() .WithHealthCheck("blocking_check"); - var blobs = storage.AddBlobs("blobs"); - var queues = storage.AddQueues("queues"); - var tables = storage.AddTables("tables"); + var blobs = storage.AddBlobService("blobs"); + var queues = storage.AddQueueService("queues"); + var tables = storage.AddTableService("tables"); var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WaitFor(blobs) @@ -79,8 +80,7 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForBlobContainersBlocksDepe .RunAsEmulator() .WithHealthCheck("blocking_check"); - var blobs = storage.AddBlobs("blobs"); - var blobContainer = blobs.AddBlobContainer("testblobcontainer"); + var blobContainer = storage.AddBlobContainer("testblobcontainer"); var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WaitFor(blobContainer); @@ -106,16 +106,68 @@ public async Task VerifyWaitForOnAzureStorageEmulatorForBlobContainersBlocksDepe await app.StopAsync(); } + [Fact] + [RequiresDocker] + public async Task VerifyWaitForOnAzureStorageEmulatorForQueueBlocksDependentResources() + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var healthCheckTcs = new TaskCompletionSource(); + builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () => + { + return healthCheckTcs.Task; + }); + + var storage = builder.AddAzureStorage("resource") + .RunAsEmulator() + .WithHealthCheck("blocking_check"); + + var queues = storage.AddQueueService("queues"); + var testQueue = storage.AddQueue("testqueue"); + + var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") + .WaitFor(testQueue); + + using var app = builder.Build(); + + var pendingStart = app.StartAsync(cts.Token); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceAsync(storage.Resource.Name, KnownResourceStates.Running, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token); + + healthCheckTcs.SetResult(HealthCheckResult.Healthy()); + + await rns.WaitForResourceHealthyAsync(testQueue.Resource.Name, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await pendingStart; + + await app.StopAsync(); + } + [Fact] [RequiresDocker] public async Task VerifyAzureStorageEmulatorResource() { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + var blobsResourceName = "BlobConnection"; var blobContainerName = "my-container"; + var queuesResourceName = "QueuesConnection"; + var queueName = "my-queue"; using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); - var blobs = builder.AddAzureStorage("storage").RunAsEmulator().AddBlobs(blobsResourceName); - var container = blobs.AddBlobContainer(blobContainerName); + var storage = builder.AddAzureStorage("storage").RunAsEmulator(); + var blobs = storage.AddBlobService(blobsResourceName); + var container = storage.AddBlobContainer(blobContainerName); + + var queues = storage.AddQueueService(queuesResourceName); + var queue = storage.AddQueue(queueName); using var app = builder.Build(); await app.StartAsync(); @@ -123,21 +175,34 @@ public async Task VerifyAzureStorageEmulatorResource() var hb = Host.CreateApplicationBuilder(); 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.Configuration[$"ConnectionStrings:{queuesResourceName}"] = await queues.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + hb.Configuration[$"ConnectionStrings:{queueName}"] = await queue.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + + hb.AddAzureBlobServiceClient(blobsResourceName); hb.AddAzureBlobContainerClient(blobContainerName); + hb.AddAzureQueueServiceClient(queuesResourceName); + hb.AddAzureQueue(queueName); using var host = hb.Build(); await host.StartAsync(); - var blobServiceClient = host.Services.GetRequiredService(); + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceHealthyAsync(storage.Resource.Name, cts.Token); + var blobContainerClient = host.Services.GetRequiredService(); - await blobContainerClient.CreateIfNotExistsAsync(); // For Aspire 9.3 only var blobClient = blobContainerClient.GetBlobClient("testKey"); + var queueClient = host.Services.GetRequiredService(); await blobClient.UploadAsync(BinaryData.FromString("testValue")); var downloadResult = (await blobClient.DownloadContentAsync()).Value; Assert.Equal("testValue", downloadResult.Content.ToString()); + + await queueClient.SendMessageAsync("Hello, World!"); + var peekedMessages = await queueClient.PeekMessagesAsync(1); + Assert.Single(peekedMessages.Value); + Assert.Equal("Hello, World!", peekedMessages.Value[0].MessageText); } [Fact] @@ -149,8 +214,8 @@ public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created() using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); - var blobs = storage.AddBlobs("BlobConnection"); - var blobContainer = blobs.AddBlobContainer("testblobcontainer"); + var blobs = storage.AddBlobService("BlobConnection"); + var blobContainer = storage.AddBlobContainer("testblobcontainer"); using var app = builder.Build(); await app.StartAsync(); @@ -160,7 +225,7 @@ public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created() var hb = Host.CreateApplicationBuilder(); hb.Configuration["ConnectionStrings:BlobConnection"] = await blobs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); - hb.AddAzureBlobClient("BlobConnection"); + hb.AddAzureBlobServiceClient("BlobConnection"); using var host = hb.Build(); await host.StartAsync(); @@ -179,4 +244,42 @@ public async Task VerifyAzureStorageEmulator_blobcontainer_auto_created() var downloadResult = (await blobClient.DownloadContentAsync()).Value; Assert.Equal(blobNameAndContent, downloadResult.Content.ToString()); } + + [Fact] + [RequiresDocker] + public async Task VerifyAzureStorageEmulator_queue_auto_created() + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + + using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper); + var storage = builder.AddAzureStorage("storage").RunAsEmulator(); + var queues = storage.AddQueueService("queues"); + var queue = storage.AddQueue("testqueue"); + + using var app = builder.Build(); + await app.StartAsync(); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceHealthyAsync(queue.Resource.Name, cancellationToken: cts.Token); + + var hb = Host.CreateApplicationBuilder(); + hb.Configuration["ConnectionStrings:QueueConnection"] = await queues.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None); + hb.AddAzureQueueServiceClient("QueueConnection"); + + using var host = hb.Build(); + await host.StartAsync(); + + var serviceClient = host.Services.GetRequiredService(); + var queueClient = serviceClient.GetQueueClient("testqueue"); + + var exists = await queueClient.ExistsAsync(); + Assert.True(exists, "Queue should exist after starting the application."); + + var blobNameAndContent = Guid.NewGuid().ToString(); + var response = await queueClient.SendMessageAsync(blobNameAndContent); + + var peekMessage = await queueClient.PeekMessageAsync(); + + Assert.Equal(blobNameAndContent, peekMessage.Value.Body.ToString()); + } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs index 048da1740be..54ba4145f9f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs @@ -184,7 +184,7 @@ public async Task AddBlobs_ConnectionString_resolved_expected_RunAsEmulator() Assert.True(storage.Resource.IsContainer()); - var blobs = storage.AddBlobs("blob"); + var blobs = storage.AddBlobService("blob"); Assert.Equal(expected, await ((IResourceWithConnectionString)blobs.Resource).ConnectionStringExpression.GetValueAsync(default)); } @@ -200,7 +200,7 @@ public async Task AddBlobs_ConnectionString_resolved_expected() var storage = builder.AddAzureStorage("storage"); storage.Resource.Outputs["blobEndpoint"] = blobsConnectionString; - var blobs = storage.AddBlobs("blob"); + var blobs = storage.AddBlobService("blob"); Assert.Equal(blobsConnectionString, await ((IResourceWithConnectionString)blobs.Resource).ConnectionStringExpression.GetValueAsync(default)); } @@ -211,7 +211,7 @@ public void AddBlobs_ConnectionString_unresolved_expected() using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("blob"); + var blobs = storage.AddBlobService("blob"); Assert.Equal("{storage.outputs.blobEndpoint}", blobs.Resource.ConnectionStringExpression.ValueExpression); } @@ -232,8 +232,8 @@ public async Task AddBlobContainer_ConnectionString_resolved_expected_RunAsEmula Assert.True(storage.Resource.IsContainer()); - var blobs = storage.AddBlobs("blob"); - var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName); + var blobs = storage.AddBlobService("blob"); + var blobContainer = storage.AddBlobContainer(name: "myContainer", blobContainerName); string? blobConnectionString = await ((IResourceWithConnectionString)blobs.Resource).ConnectionStringExpression.GetValueAsync(default); string? blobContainerConnectionString = await ((IResourceWithConnectionString)blobContainer.Resource).ConnectionStringExpression.GetValueAsync(default); @@ -254,8 +254,8 @@ public async Task AddBlobContainer_ConnectionString_resolved_expected() var storage = builder.AddAzureStorage("storage"); storage.Resource.Outputs["blobEndpoint"] = "https://myblob"; - var blobs = storage.AddBlobs("blob"); - var blobContainer = blobs.AddBlobContainer(name: "myContainer", blobContainerName); + var blobs = storage.AddBlobService("blob"); + var blobContainer = storage.AddBlobContainer(name: "myContainer", blobContainerName); string? blobsConnectionString = await ((IResourceWithConnectionString)blobs.Resource).ConnectionStringExpression.GetValueAsync(default); string expected = $"Endpoint={blobsConnectionString};ContainerName={blobContainerName}"; @@ -269,22 +269,126 @@ public void AddBlobContainer_ConnectionString_unresolved_expected() using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("blob"); - var blobContainer = blobs.AddBlobContainer(name: "myContainer"); + var blobContainer = storage.AddBlobContainer(name: "myContainer"); Assert.Equal("Endpoint={storage.outputs.blobEndpoint};ContainerName=myContainer", blobContainer.Resource.ConnectionStringExpression.ValueExpression); } + [Fact] + public async Task AddQueues_ConnectionString_resolved_expected_RunAsEmulator() + { + const string expected = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage").RunAsEmulator(e => + { + e.WithEndpoint("blob", e => e.AllocatedEndpoint = new(e, "localhost", 10000)); + e.WithEndpoint("queue", e => e.AllocatedEndpoint = new(e, "localhost", 10001)); + e.WithEndpoint("table", e => e.AllocatedEndpoint = new(e, "localhost", 10002)); + }); + + Assert.True(storage.Resource.IsContainer()); + + var queues = storage.AddQueueService("queues"); + + Assert.Equal(expected, await ((IResourceWithConnectionString)queues.Resource).GetConnectionStringAsync()); + } + + [Fact] + public async Task AddQueues_ConnectionString_resolved_expected() + { + const string connectionString = "https://myblob"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var storagesku = builder.AddParameter("storagesku"); + var storage = builder.AddAzureStorage("storage"); + storage.Resource.Outputs["queueEndpoint"] = connectionString; + + var queues = storage.AddQueueService("queues"); + + Assert.Equal(connectionString, await ((IResourceWithConnectionString)queues.Resource).GetConnectionStringAsync()); + } + + [Fact] + public void AddQueues_ConnectionString_unresolved_expected() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage"); + var queues = storage.AddQueueService("queues"); + + Assert.Equal("{storage.outputs.queueEndpoint}", queues.Resource.ConnectionStringExpression.ValueExpression); + } + + [Fact] + public async Task AddQueue_ConnectionString_resolved_expected_RunAsEmulator() + { + const string queueName = "my-queue"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage").RunAsEmulator(e => + { + e.WithEndpoint("blob", e => e.AllocatedEndpoint = new(e, "localhost", 10000)); + e.WithEndpoint("queue", e => e.AllocatedEndpoint = new(e, "localhost", 10001)); + e.WithEndpoint("table", e => e.AllocatedEndpoint = new(e, "localhost", 10002)); + }); + + Assert.True(storage.Resource.IsContainer()); + + var queues = storage.AddQueueService("queues"); + var queue = storage.AddQueue(name: "myqueue", queueName); + + string? connectionString = await ((IResourceWithConnectionString)queues.Resource).GetConnectionStringAsync(); + string expected = $"{connectionString};QueueName={queueName}"; + + Assert.Equal(expected, await ((IResourceWithConnectionString)queue.Resource).GetConnectionStringAsync()); + } + + [Fact] + public async Task AddQueue_ConnectionString_resolved_expected() + { + const string queueName = "my-queue"; + + using var builder = TestDistributedApplicationBuilder.Create(); + + var storagesku = builder.AddParameter("storagesku"); + var storage = builder.AddAzureStorage("storage"); + storage.Resource.Outputs["queueEndpoint"] = "https://myqueue"; + + var queues = storage.AddQueueService("queues"); + var queue = storage.AddQueue(name: "myqueue", queueName); + + string? connectionString = await ((IResourceWithConnectionString)queues.Resource).GetConnectionStringAsync(); + string expected = $"Endpoint={connectionString};QueueName={queueName}"; + + Assert.Equal(expected, await ((IResourceWithConnectionString)queue.Resource).GetConnectionStringAsync()); + } + + [Fact] + public void AddQueue_ConnectionString_unresolved_expected() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage"); + var queues = storage.AddQueueService("queues"); + var queue = storage.AddQueue(name: "myqueue"); + + Assert.Equal("Endpoint={storage.outputs.queueEndpoint};QueueName=myqueue", queue.Resource.ConnectionStringExpression.ValueExpression); + } + [Fact] public async Task ResourceNamesBicepValid() { using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage"); - var blobs = storage.AddBlobs("myblobs"); - var blob = blobs.AddBlobContainer(name: "myContainer", blobContainerName: "my-blob-container"); - var queues = storage.AddQueues("myqueues"); - var tables = storage.AddTables("mytables"); + var blob = storage.AddBlobContainer(name: "myContainer", blobContainerName: "my-blob-container"); + var queues = storage.AddQueueService("myqueues"); + var queue = storage.AddQueue(name: "myqueue", queueName: "my-queue"); + var tables = storage.AddTableService("mytables"); var manifest = await AzureManifestUtils.GetManifestWithBicep(storage.Resource); @@ -305,9 +409,9 @@ public async Task AddAzureStorageEmulator() Assert.True(storage.Resource.IsContainer()); - var blob = storage.AddBlobs("blob"); - var queue = storage.AddQueues("queue"); - var table = storage.AddTables("table"); + var blob = storage.AddBlobService("blob"); + var queue = storage.AddQueueService("queue"); + var table = storage.AddTableService("table"); EndpointReference GetEndpointReference(string name, int port) => new(storage.Resource, new EndpointAnnotation(ProtocolType.Tcp, name: name, targetPort: port)); @@ -369,7 +473,7 @@ public async Task AddAzureStorageViaRunMode() await Verify(storageManifest.BicepText, extension: "bicep"); // Check blob resource. - var blob = storage.AddBlobs("blob"); + var blob = storage.AddBlobService("blob"); var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource; @@ -384,7 +488,7 @@ public async Task AddAzureStorageViaRunMode() Assert.Equal(expectedBlobManifest, blobManifest.ToString()); // Check queue resource. - var queue = storage.AddQueues("queue"); + var queue = storage.AddQueueService("queue"); var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource; @@ -399,7 +503,7 @@ public async Task AddAzureStorageViaRunMode() Assert.Equal(expectedQueueManifest, queueManifest.ToString()); // Check table resource. - var table = storage.AddTables("table"); + var table = storage.AddTableService("table"); var connectionStringTableResource = (IResourceWithConnectionString)table.Resource; @@ -454,7 +558,7 @@ public async Task AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultF await Verify(storageManifest.BicepText, extension: "bicep"); // Check blob resource. - var blob = storage.AddBlobs("blob"); + var blob = storage.AddBlobService("blob"); var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource; @@ -469,7 +573,7 @@ public async Task AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultF Assert.Equal(expectedBlobManifest, blobManifest.ToString()); // Check queue resource. - var queue = storage.AddQueues("queue"); + var queue = storage.AddQueueService("queue"); var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource; @@ -484,7 +588,7 @@ public async Task AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultF Assert.Equal(expectedQueueManifest, queueManifest.ToString()); // Check table resource. - var table = storage.AddTables("table"); + var table = storage.AddTableService("table"); var connectionStringTableResource = (IResourceWithConnectionString)table.Resource; @@ -515,9 +619,9 @@ public async Task AddAzureStorageViaPublishMode() }; }); - var blob = storage.AddBlobs("blob"); - var queue = storage.AddQueues("queue"); - var table = storage.AddTables("table"); + var blob = storage.AddBlobService("blob"); + var queue = storage.AddQueueService("queue"); + var table = storage.AddTableService("table"); storage.Resource.Outputs["blobEndpoint"] = "https://myblob"; storage.Resource.Outputs["queueEndpoint"] = "https://myqueue"; @@ -674,7 +778,7 @@ public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverrid await Verify(storageManifest.BicepText, extension: "bicep"); // Check blob resource. - var blob = storage.AddBlobs("blob"); + var blob = storage.AddBlobService("blob"); var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource; @@ -689,7 +793,7 @@ public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverrid Assert.Equal(expectedBlobManifest, blobManifest.ToString()); // Check queue resource. - var queue = storage.AddQueues("queue"); + var queue = storage.AddQueueService("queue"); var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource; @@ -704,7 +808,7 @@ public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverrid Assert.Equal(expectedQueueManifest, queueManifest.ToString()); // Check table resource. - var table = storage.AddTables("table"); + var table = storage.AddTableService("table"); var connectionStringTableResource = (IResourceWithConnectionString)table.Resource; @@ -718,4 +822,66 @@ public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverrid var tableManifest = await ManifestUtils.GetManifest(table.Resource); Assert.Equal(expectedTableManifest, tableManifest.ToString()); } + + [Fact] + public void AddBlobService_Default_Name() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage"); + var blobService = storage.AddBlobService(); + + Assert.Equal("storage-blobs", blobService.Resource.Name); + } + + [Fact] + public void AddQueueService_Default_Name() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage"); + var queueService = storage.AddQueueService(); + + Assert.Equal("storage-queues", queueService.Resource.Name); + } + + [Fact] + public void AddTableService_Default_Name() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var storage = builder.AddAzureStorage("storage"); + var tableService = storage.AddTableService(); + + Assert.Equal("storage-tables", tableService.Resource.Name); + } + + [Fact] + public async Task AddMultipleStorageServiceGeneratesSingleResource() + { + // Ensures the bicep file doesn't contain duplicate service resources + // and blobs/queues are associated to the last created one. + + using var builder = TestDistributedApplicationBuilder.Create(); + var storage = builder.AddAzureStorage("storage"); + + var blobService1 = storage.AddBlobService("blobService1"); + var container1 = storage.AddBlobContainer(name: "container1"); + + var blobService2 = storage.AddBlobService("blobService2"); + var container2 = storage.AddBlobContainer(name: "container2"); + + var queueService1 = storage.AddQueueService("queueService1"); + var queue1 = storage.AddQueue(name: "queue1"); + + var queueService2 = storage.AddQueueService("queueService2"); + var queue2 = storage.AddQueue(name: "queue2"); + + var tableService1 = storage.AddTableService("tableService1"); + var tableService2 = storage.AddTableService("tableService2"); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(storage.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } } diff --git a/tests/Aspire.Hosting.Azure.Tests/ResourceWithAzureFunctionsConfigTests.cs b/tests/Aspire.Hosting.Azure.Tests/ResourceWithAzureFunctionsConfigTests.cs index aa81e70eb43..75df63fa771 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ResourceWithAzureFunctionsConfigTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ResourceWithAzureFunctionsConfigTests.cs @@ -25,7 +25,7 @@ public void AzureBlobStorageResource_ImplementsIResourceWithAzureFunctionsConfig // Arrange using var builder = TestDistributedApplicationBuilder.Create(); var storageResource = builder.AddAzureStorage("storage"); - var blobResource = storageResource.AddBlobs("blobs").Resource; + var blobResource = storageResource.AddBlobService("blobs").Resource; // Act & Assert Assert.IsAssignableFrom(blobResource); @@ -37,7 +37,7 @@ public void AzureQueueStorageResource_ImplementsIResourceWithAzureFunctionsConfi // Arrange using var builder = TestDistributedApplicationBuilder.Create(); var storageResource = builder.AddAzureStorage("storage"); - var queueResource = storageResource.AddQueues("queues").Resource; + var queueResource = storageResource.AddQueueService("queues").Resource; // Act & Assert Assert.IsAssignableFrom(queueResource); @@ -145,7 +145,7 @@ public void AzureBlobStorage_AppliesCorrectConfigurationFormat() // Arrange using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); - var blobResource = storage.AddBlobs("blobs").Resource; + var blobResource = storage.AddBlobService("blobs").Resource; var target = new Dictionary(); // Act @@ -162,7 +162,7 @@ public void AzureTableStorage_AppliesCorrectConfigurationFormat() // Arrange using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); - var tableResource = storage.AddTables("tables").Resource; + var tableResource = storage.AddTableService("tables").Resource; var target = new Dictionary(); // Act @@ -179,7 +179,7 @@ public void AzureQueueStorage_AppliesCorrectConfigurationFormat() // Arrange using var builder = TestDistributedApplicationBuilder.Create(); var storage = builder.AddAzureStorage("storage").RunAsEmulator(); - var queueResource = storage.AddQueues("queues").Resource; + var queueResource = storage.AddQueueService("queues").Resource; var target = new Dictionary(); // Act diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishMode.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishMode.verified.bicep index ad170c30359..e8de364e8b1 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishMode.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishMode.verified.bicep @@ -23,11 +23,20 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { +resource blob 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { name: 'default' parent: storage } +resource queue 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = { + name: 'default' + parent: storage +} + +resource table 'Microsoft.Storage/storageAccounts/tableServices@2024-01-01' = { + parent: storage +} + output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep index d30a60e9983..659f6897a70 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: storage -} - output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunMode.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunMode.verified.bicep index ad170c30359..7213020ee33 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunMode.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunMode.verified.bicep @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: storage -} - output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep index d30a60e9983..659f6897a70 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep @@ -23,11 +23,6 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: storage -} - output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddMultipleStorageServiceGeneratesSingleResource.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddMultipleStorageServiceGeneratesSingleResource.verified.bicep new file mode 100644 index 00000000000..2d1fce79577 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.AddMultipleStorageServiceGeneratesSingleResource.verified.bicep @@ -0,0 +1,64 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('storage${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'storage' + } +} + +resource blobService2 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { + name: 'default' + parent: storage +} + +resource container1 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { + name: 'container1' + parent: blobService2 +} + +resource container2 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { + name: 'container2' + parent: blobService2 +} + +resource queueService2 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = { + name: 'default' + parent: storage +} + +resource queue1 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = { + name: 'queue1' + parent: queueService2 +} + +resource queue2 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = { + name: 'queue2' + parent: queueService2 +} + +resource tableService2 'Microsoft.Storage/storageAccounts/tableServices@2024-01-01' = { + parent: storage +} + +output blobEndpoint string = storage.properties.primaryEndpoints.blob + +output queueEndpoint string = storage.properties.primaryEndpoints.queue + +output tableEndpoint string = storage.properties.primaryEndpoints.table + +output name string = storage.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep index c539e1e9812..0a48160a069 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep @@ -21,14 +21,28 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { } } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { +resource storage_blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { name: 'default' parent: storage } resource myContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { name: 'my-blob-container' - parent: blobs + parent: storage_blobs +} + +resource myqueues 'Microsoft.Storage/storageAccounts/queueServices@2024-01-01' = { + name: 'default' + parent: storage +} + +resource myqueue 'Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01' = { + name: 'my-queue' + parent: myqueues +} + +resource mytables 'Microsoft.Storage/storageAccounts/tableServices@2024-01-01' = { + parent: storage } output blobEndpoint string = storage.properties.primaryEndpoints.blob @@ -37,4 +51,4 @@ output queueEndpoint string = storage.properties.primaryEndpoints.queue output tableEndpoint string = storage.properties.primaryEndpoints.table -output name string = storage.name \ No newline at end of file +output name string = storage.name diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep index 19e5fbea54e..37fa28a486e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep @@ -7,15 +7,10 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { name: existingResourceName } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: storage -} - output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue output tableEndpoint string = storage.properties.primaryEndpoints.table -output name string = existingResourceName \ No newline at end of file +output name string = existingResourceName diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep index ce8d49e094b..bdb7e689d41 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep @@ -5,15 +5,10 @@ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { name: 'existingResourcename' } -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: storage -} - output blobEndpoint string = storage.properties.primaryEndpoints.blob output queueEndpoint string = storage.properties.primaryEndpoints.queue output tableEndpoint string = storage.properties.primaryEndpoints.table -output name string = storage.name \ No newline at end of file +output name string = storage.name