diff --git a/Directory.Packages.props b/Directory.Packages.props
index 06a8e1d77f5..a6f8e12632b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,6 +7,7 @@
true
true
1.0.0
+ 1.0.1
8.0.6
@@ -49,7 +50,7 @@
-
+
@@ -146,7 +147,6 @@
-
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.ApiService/Program.cs
index 00afe533fb1..d136b9fb68e 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.AddAzureQueueClient("queues");
+builder.AddAzureBlobClient("blobs");
builder.AddAzureEventHubProducerClient("myhub");
#if !SKIP_UNSTABLE_EMULATORS
builder.AddAzureServiceBusClient("messaging");
@@ -24,10 +24,16 @@
var app = builder.Build();
+app.MapGet("/", async (HttpClient client) =>
+{
+ var stream = await client.GetStreamAsync("http://funcapp/api/injected-resources");
+ return Results.Stream(stream, "application/json");
+});
+
app.MapGet("/publish/asq", async (QueueServiceClient client, CancellationToken cancellationToken) =>
{
- var queue = client.GetQueueClient("queue");
- await queue.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
+ var queue = client.GetQueueClient("myqueue1");
+
var data = Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello, World!"));
await queue.SendMessageAsync(data, cancellationToken: cancellationToken);
return Results.Ok("Message sent to Azure Storage Queue.");
@@ -41,15 +47,14 @@ static string RandomString(int length)
app.MapGet("/publish/blob", async (BlobServiceClient client, CancellationToken cancellationToken, int length = 20) =>
{
- var container = client.GetBlobContainerClient("blobs");
- await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
+ var container = client.GetBlobContainerClient("myblobcontainer");
var entry = new { Id = Guid.NewGuid(), Text = RandomString(length) };
var blob = container.GetBlobClient(entry.Id.ToString());
await blob.UploadAsync(new BinaryData(entry));
- return Results.Ok("String uploaded to Azure Storage Blobs.");
+ return Results.Ok($"String uploaded to Azure Storage Blobs {container.Uri}.");
});
app.MapGet("/publish/eventhubs", async (EventHubProducerClient client, CancellationToken cancellationToken, int length = 20) =>
@@ -80,12 +85,6 @@ static string RandomString(int length)
});
#endif
-app.MapGet("/", async (HttpClient client) =>
-{
- var stream = await client.GetStreamAsync("http://funcapp/api/injected-resources");
- return Results.Stream(stream, "application/json");
-});
-
app.MapDefaultEndpoints();
app.Run();
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs
index e52a06b7427..14e38800b6a 100644
--- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/Program.cs
@@ -1,9 +1,12 @@
var builder = DistributedApplication.CreateBuilder(args);
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
-var queue = storage.AddQueues("queue");
-var blob = storage.AddBlobs("blob");
-var myBlobContainer = blob.AddBlobContainer("myblobcontainer");
+
+var queues = storage.AddQueues("queues");
+var myQueue = queues.AddQueue("myqueue1");
+
+var blobs = storage.AddBlobs("blobs");
+var myBlobContainer = blobs.AddBlobContainer("myblobcontainer");
var eventHub = builder.AddAzureEventHubs("eventhubs")
.RunAsEmulator()
@@ -22,13 +25,14 @@
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(blob)
- .WithReference(queue);
+ .WithReference(blobs)
+ .WithReference(myBlobContainer).WaitFor(myBlobContainer)
+ .WithReference(queues)
+ .WithReference(myQueue).WaitFor(myQueue);
builder.AddProject("apiservice")
.WithReference(eventHub).WaitFor(eventHub)
@@ -36,8 +40,8 @@
.WithReference(serviceBus).WaitFor(serviceBus)
.WithReference(cosmosDb).WaitFor(cosmosDb)
#endif
- .WithReference(queue)
- .WithReference(blob)
+ .WithReference(queues)
+ .WithReference(blobs)
.WithReference(funcApp);
builder.Build().Run();
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs
index 6c535261cdd..845272ba6a2 100644
--- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureBlobTrigger.cs
@@ -4,16 +4,16 @@
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")]
- public async Task RunAsync([BlobTrigger("blobs/{name}", Connection = "blob")] string triggerString, FunctionContext context)
+ [BlobOutput("test-files/{name}.txt", Connection = "blobs")]
+ public async Task RunAsync([BlobTrigger("myblobcontainer/{name}", Connection = "blobs")] string triggerString, FunctionContext context)
{
var blobName = (string)context.BindingContext.BindingData["name"]!;
- await containerClient.UploadBlobAsync(blobName, new BinaryData(triggerString));
+ _ = await containerClient.GetAccountInfoAsync();
- logger.LogInformation("C# blob trigger function invoked for 'blobs/{source}' with {message}...", blobName, triggerString);
+ logger.LogInformation("C# blob trigger function invoked for 'myblobcontainer/{source}' with {message}...", blobName, triggerString);
return triggerString.ToUpper();
}
}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs
index 0ea035d98c5..035b7d19f5b 100644
--- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyAzureQueueTrigger.cs
@@ -1,14 +1,17 @@
+using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace AzureFunctionsEndToEnd.Functions;
-public class MyAzureQueueTrigger(ILogger logger)
+public class MyAzureQueueTrigger(QueueClient queueClient, ILogger logger)
{
[Function(nameof(MyAzureQueueTrigger))]
- public void Run([QueueTrigger("queue", Connection = "queue")] QueueMessage message)
+ public void Run([QueueTrigger("myqueue1", Connection = "queues")] QueueMessage message)
{
+ _ = queueClient.GetProperties();
+
logger.LogInformation("C# Queue trigger function processed: {Text}", message.MessageText);
}
}
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs
index 83fc3a38893..ea0e99833e1 100644
--- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/MyHttpTrigger.cs
@@ -22,6 +22,7 @@ public class MyHttpTrigger(
#endif
EventHubProducerClient eventHubProducerClient,
QueueServiceClient queueServiceClient,
+ QueueClient queueClient,
BlobServiceClient blobServiceClient,
BlobContainerClient blobContainerClient)
{
@@ -35,6 +36,7 @@ public IResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] Ht
#endif
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected EventHubProducerClient namespace: {eventHubProducerClient.FullyQualifiedNamespace}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected QueueServiceClient URI: {queueServiceClient.Uri}");
+ stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected QueueClient URI: {queueClient.Uri}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobServiceClient URI: {blobServiceClient.Uri}");
stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"Aspire-injected BlobContainerClient URI: {blobContainerClient.Uri}");
return Results.Text(stringBuilder.ToString());
diff --git a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs
index 336749046b4..0dcab6eb03b 100644
--- a/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs
+++ b/playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.Functions/Program.cs
@@ -4,9 +4,13 @@
var builder = FunctionsApplication.CreateBuilder(args);
builder.AddServiceDefaults();
-builder.AddAzureQueueClient("queue");
-builder.AddAzureBlobClient("blob");
+
+builder.AddAzureQueueClient("queues");
+builder.AddAzureQueue("myqueue1");
+
+builder.AddAzureBlobClient("blobs");
builder.AddAzureBlobContainerClient("myblobcontainer");
+
builder.AddAzureEventHubProducerClient("myhub");
#if !SKIP_UNSTABLE_EMULATORS
builder.AddAzureServiceBusClient("messaging");
diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs
index 02f16b8f516..dc4353ac7ba 100644
--- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs
+++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.ApiService/Program.cs
@@ -12,31 +12,61 @@
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("/", (HttpContext context) =>
+{
+ var request = context.Request;
+ var scheme = request.Scheme;
+ var host = request.Host;
+
+ var endpointDataSource = context.RequestServices.GetRequiredService();
+ var urls = endpointDataSource.Endpoints
+ .OfType()
+ .Select(e => $"{scheme}://{host}{e.RoutePattern.RawText}");
+
+ var html = "" +
+ string.Join("", urls.Select(url => $"- {url}
")) +
+ "
";
+
+ context.Response.ContentType = "text/html";
+ return context.Response.WriteAsync(html);
+});
+
+app.MapGet("/blobs", async (BlobServiceClient bsc, [FromKeyedServices("foocontainer")] BlobContainerClient bcc) =>
{
var blobNames = new List();
var blobNameAndContent = Guid.NewGuid().ToString();
- await keyedContainerClient1.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
+ await bcc.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
var directContainerClient = bsc.GetBlobContainerClient(blobContainerName: "test-container-1");
await directContainerClient.UploadBlobAsync(blobNameAndContent, new BinaryData(blobNameAndContent));
await ReadBlobsAsync(directContainerClient, blobNames);
- await ReadBlobsAsync(keyedContainerClient1, blobNames);
-
- var queue = qsc.GetQueueClient("myqueue");
- await queue.CreateIfNotExistsAsync();
- await queue.SendMessageAsync("Hello, world!");
+ await ReadBlobsAsync(bcc, blobNames);
return blobNames;
});
+app.MapGet("/queues", async (QueueServiceClient qsc, [FromKeyedServices("myqueue")] QueueClient qc) =>
+{
+ const string text = "Hello, World!";
+ List messages = [$"Sent: {text}"];
+
+ var queue = qsc.GetQueueClient("my-queue");
+ await queue.SendMessageAsync(text);
+
+ var msg = await qc.ReceiveMessageAsync();
+ messages.Add($"Received: {msg.Value.Body}");
+
+ return messages;
+});
+
app.Run();
static async Task ReadBlobsAsync(BlobContainerClient containerClient, List output)
diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs
index aba1f9e6eea..138c03e87be 100644
--- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs
+++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs
@@ -13,6 +13,7 @@
blobs.AddBlobContainer("mycontainer2", blobContainerName: "test-container-2");
var queues = storage.AddQueues("queues");
+var myqueue = queues.AddQueue("myqueue", queueName: "my-queue");
var storage2 = builder.AddAzureStorage("storage2").RunAsEmulator(container =>
{
@@ -25,7 +26,8 @@
.WithExternalHttpEndpoints()
.WithReference(blobs).WaitFor(blobs)
.WithReference(blobContainer2).WaitFor(blobContainer2)
- .WithReference(queues).WaitFor(queues);
+ .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/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj
index f3a8aced451..67cc268a09e 100644
--- a/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj
+++ b/src/Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.Storage.csproj
@@ -14,7 +14,11 @@
+
+
+
+
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs
new file mode 100644
index 00000000000..dd05fe88413
--- /dev/null
+++ b/src/Aspire.Hosting.Azure.Storage/AzureQueueStorageQueueResource.cs
@@ -0,0 +1,56 @@
+// 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 Aspire.Hosting.Azure;
+using Azure.Provisioning;
+
+namespace Aspire.Hosting;
+
+///
+/// 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 queue 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 8c9235640b1..927be7d95d1 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,43 @@ public static IResourceBuilder AddAzureStorage(this IDistr
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
});
- var blobs = new BlobService("blobs")
- {
- Parent = storageAccount
- };
- infrastructure.Add(blobs);
+ var azureResource = (AzureStorageResource)infrastructure.AspireResource;
- 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 });
+ if (azureResource.BlobContainers.Count > 0)
+ {
+ var blobs = new BlobService("blobs")
+ {
+ Parent = storageAccount
+ };
+ infrastructure.Add(blobs);
- var azureResource = (AzureStorageResource)infrastructure.AspireResource;
+ foreach (var blobContainer in azureResource.BlobContainers)
+ {
+ var cdkBlobContainer = blobContainer.ToProvisioningEntity();
+ cdkBlobContainer.Parent = blobs;
+ infrastructure.Add(cdkBlobContainer);
+ }
+ }
- foreach (var blobContainer in azureResource.BlobContainers)
+ if (azureResource.Queues.Count > 0)
{
- var cdkBlobContainer = blobContainer.ToProvisioningEntity();
- cdkBlobContainer.Parent = blobs;
- infrastructure.Add(cdkBlobContainer);
+ var queues = new QueueService("queues")
+ {
+ Parent = storageAccount
+ };
+ infrastructure.Add(queues);
+ foreach (var queue in azureResource.Queues)
+ {
+ var cdkQueue = queue.ToProvisioningEntity();
+ cdkQueue.Parent = queues;
+ infrastructure.Add(cdkQueue);
+ }
}
+ 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 +151,45 @@ public static IResourceBuilder RunAsEmulator(this IResourc
});
BlobServiceClient? blobServiceClient = null;
+ QueueServiceClient? queueServiceClient = null;
+
builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (@event, ct) =>
{
// The BlobServiceClient is 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.");
+ 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);
+
+ connectionString = 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(connectionString);
});
builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (@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 =>
@@ -284,7 +315,7 @@ public static IResourceBuilder WithApiVersionCheck
///
/// 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 .
public static IResourceBuilder AddBlobs(this IResourceBuilder builder, [ResourceName] string name)
@@ -300,6 +331,8 @@ public static IResourceBuilder AddBlobs(this IResource
connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
});
+ // 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;
@@ -308,13 +341,15 @@ public static IResourceBuilder AddBlobs(this IResource
return blobServiceClient ??= CreateBlobServiceClient(connectionString ?? throw new InvalidOperationException("Connection string is not initialized."));
}, name: healthCheckKey);
- return builder.ApplicationBuilder.AddResource(resource).WithHealthCheck(healthCheckKey);
+ return builder.ApplicationBuilder
+ .AddResource(resource)
+ .WithHealthCheck(healthCheckKey);
}
///
/// 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 .
@@ -343,13 +378,14 @@ public static IResourceBuilder AddBlobContain
name: healthCheckKey);
return builder.ApplicationBuilder
- .AddResource(resource).WithHealthCheck(healthCheckKey);
+ .AddResource(resource)
+ .WithHealthCheck(healthCheckKey);
}
///
/// 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 .
public static IResourceBuilder AddTables(this IResourceBuilder builder, [ResourceName] string name)
@@ -364,7 +400,7 @@ public static IResourceBuilder AddTables(this IResour
///
/// 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 .
public static IResourceBuilder AddQueues(this IResourceBuilder builder, [ResourceName] string name)
@@ -373,7 +409,62 @@ public static IResourceBuilder AddQueues(this IResour
ArgumentException.ThrowIfNullOrEmpty(name);
var resource = new AzureQueueStorageResource(name, builder.Resource);
- return builder.ApplicationBuilder.AddResource(resource);
+
+ string? connectionString = null;
+ builder.ApplicationBuilder.Eventing.Subscribe(resource, async (@event, ct) =>
+ {
+ connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
+ });
+
+ // 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);
+ }
+
+ ///
+ /// Creates a builder for the which can be referenced to get the Azure Storage queue endpoint 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;
+
+ AzureQueueStorageQueueResource resource = new(name, queueName, builder.Resource);
+ builder.Resource.Parent.Queues.Add(resource);
+
+ string? connectionString = null;
+ builder.ApplicationBuilder.Eventing.Subscribe(resource, async (@event, ct) =>
+ {
+ connectionString = await resource.Parent.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
+ });
+
+ 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);
}
private static BlobServiceClient CreateBlobServiceClient(string connectionString)
@@ -388,6 +479,18 @@ private static BlobServiceClient CreateBlobServiceClient(string connectionString
}
}
+ 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.
diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
index e5f581a1a48..bd87fe18c25 100644
--- a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
+++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs
@@ -24,6 +24,7 @@ public class AzureStorageResource(string name, Action new(this, "table");
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/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json b/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
index bfcbb0bba02..9ccad96578c 100644
--- a/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
+++ b/src/Components/Aspire.Azure.AI.OpenAI/ConfigurationSchema.json
@@ -67,10 +67,6 @@
},
"description": "The options to be used to configure logging within the 'System.ClientModel.Primitives.ClientPipeline'."
},
- "EnableDistributedTracing": {
- "type": "boolean",
- "description": "Gets or sets whether distributed tracing should be enabled. If null, this value will be treated as true. The default is null."
- },
"NetworkTimeout": {
"type": "string",
"pattern": "^-?(\\d{1,7}|((\\d{1,7}[\\.:])?(([01]?\\d|2[0-3]):[0-5]?\\d|([01]?\\d|2[0-3]):[0-5]?\\d:[0-5]?\\d)(\\.\\d{1,7})?))$",
diff --git a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs
index cb581a1d1d0..e0cc8651e13 100644
--- a/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs
+++ b/src/Components/Aspire.Azure.Storage.Blobs/AzureBlobStorageContainerSettings.cs
@@ -40,7 +40,7 @@ void IConnectionStringSettings.ParseConnectionString(string? connectionString)
// when the connection string is built and BlobServiceClient doesn't support escape sequences.
}
- // Connection string built from a URI? e.g., Endpoint=https://{account_name}.blob.core.windows.net;ContainerName=...;
+ // Connection string built from a URI? E.g., Endpoint=https://{account_name}.blob.core.windows.net;ContainerName=...;
if (builder.TryGetValue("Endpoint", out var endpoint) && endpoint is string)
{
if (Uri.TryCreate(endpoint.ToString(), UriKind.Absolute, out var uri))
diff --git a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.StorageQueueComponent.cs b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.StorageQueueComponent.cs
new file mode 100644
index 00000000000..e1cea0b14d6
--- /dev/null
+++ b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.StorageQueueComponent.cs
@@ -0,0 +1,74 @@
+// 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 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;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Microsoft.Extensions.Hosting;
+
+public static partial class AspireQueueStorageExtensions
+{
+ 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 QueueServiceClient 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/AspireQueueStorageExtensions.StorageQueuesComponent.cs b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.StorageQueuesComponent.cs
new file mode 100644
index 00000000000..6428f0eafd6
--- /dev/null
+++ b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.StorageQueuesComponent.cs
@@ -0,0 +1,68 @@
+// 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 Azure.Core;
+using Azure.Core.Extensions;
+using Azure.Storage.Queues;
+using HealthChecks.Azure.Storage.Queues;
+using Microsoft.Extensions.Azure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Microsoft.Extensions.Hosting;
+
+partial class AspireQueueStorageExtensions
+{
+ private sealed class StorageQueuesComponent : AzureComponent
+ {
+ protected override IAzureClientBuilder AddClient(
+ AzureClientFactoryBuilder azureFactoryBuilder, AzureStorageQueuesSettings settings, string connectionName,
+ string configurationSectionName)
+ {
+ return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory((options, cred) =>
+ {
+ var connectionString = settings.ConnectionString;
+ if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null)
+ {
+ throw new InvalidOperationException($"A QueueServiceClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a 'ConnectionString' or 'ServiceUri' in the '{configurationSectionName}' configuration section.");
+ }
+
+ return !string.IsNullOrEmpty(connectionString)
+ ? new QueueServiceClient(connectionString, options)
+ : cred is not null
+ ? new QueueServiceClient(settings.ServiceUri, cred, options)
+ : new QueueServiceClient(settings.ServiceUri, options);
+ }, 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(AzureStorageQueuesSettings settings, IConfiguration configuration)
+ {
+ configuration.Bind(settings);
+ }
+
+ protected override IHealthCheck CreateHealthCheck(QueueServiceClient client, AzureStorageQueuesSettings settings)
+ => new AzureQueueStorageHealthCheck(client, new AzureQueueStorageHealthCheckOptions());
+
+ protected override bool GetHealthCheckEnabled(AzureStorageQueuesSettings settings)
+ => !settings.DisableHealthChecks;
+
+ protected override TokenCredential? GetTokenCredential(AzureStorageQueuesSettings settings)
+ => settings.Credential;
+
+ protected override bool GetMetricsEnabled(AzureStorageQueuesSettings settings)
+ => false;
+
+ protected override bool GetTracingEnabled(AzureStorageQueuesSettings settings)
+ => !settings.DisableTracing;
+ }
+}
diff --git a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
index b4cc6bf2c3f..5b0c0ca756c 100644
--- a/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
+++ b/src/Components/Aspire.Azure.Storage.Queues/AspireQueueStorageExtensions.cs
@@ -1,16 +1,10 @@
// 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 Azure.Core;
using Azure.Core.Extensions;
using Azure.Storage.Queues;
-using HealthChecks.Azure.Storage.Queues;
-using Microsoft.Extensions.Azure;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.Hosting;
@@ -18,20 +12,27 @@ namespace Microsoft.Extensions.Hosting;
/// Provides extension methods for registering as a singleton in the services provided by the .
/// Enables retries, corresponding health check, logging and telemetry.
///
-public static class AspireQueueStorageExtensions
+public static partial class AspireQueueStorageExtensions
{
private const string DefaultConfigSectionName = "Aspire:Azure:Storage:Queues";
///
- /// Registers as a singleton in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
+ /// 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 .
+ ///
+ /// 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.
+ ///
+ /// Neither nor is provided.
+ ///
public static void AddAzureQueueClient(
this IHostApplicationBuilder builder,
string connectionName,
@@ -41,19 +42,30 @@ public static void AddAzureQueueClient(
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(connectionName);
- new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
+ new StorageQueuesComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
///
- /// Registers as a singleton for given in the services provided by the .
- /// Enables retries, corresponding health check, logging and telemetry.
+ /// 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 .
+ ///
+ /// 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.
+ ///
+ /// Neither nor is provided.
+ ///
public static void AddKeyedAzureQueueClient(
this IHostApplicationBuilder builder,
string name,
@@ -63,56 +75,71 @@ public static void AddKeyedAzureQueueClient(
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
- new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);
+ new StorageQueuesComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);
}
- private sealed class StorageQueueComponent : AzureComponent
+ ///
+ /// 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)
{
- protected override IAzureClientBuilder AddClient(
- AzureClientFactoryBuilder azureFactoryBuilder, AzureStorageQueuesSettings settings, string connectionName,
- string configurationSectionName)
- {
- return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory((options, cred) =>
- {
- var connectionString = settings.ConnectionString;
- if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null)
- {
- throw new InvalidOperationException($"A QueueServiceClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or specify a 'ConnectionString' or 'ServiceUri' in the '{configurationSectionName}' configuration section.");
- }
-
- return !string.IsNullOrEmpty(connectionString)
- ? new QueueServiceClient(connectionString, options)
- : cred is not null
- ? new QueueServiceClient(settings.ServiceUri, cred, options)
- : new QueueServiceClient(settings.ServiceUri, options);
- }, 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(AzureStorageQueuesSettings settings, IConfiguration configuration)
- {
- configuration.Bind(settings);
- }
-
- protected override IHealthCheck CreateHealthCheck(QueueServiceClient client, AzureStorageQueuesSettings settings)
- => new AzureQueueStorageHealthCheck(client, new AzureQueueStorageHealthCheckOptions());
-
- protected override bool GetHealthCheckEnabled(AzureStorageQueuesSettings settings)
- => !settings.DisableHealthChecks;
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(connectionName);
- protected override TokenCredential? GetTokenCredential(AzureStorageQueuesSettings settings)
- => settings.Credential;
+ new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName, serviceKey: null);
+ }
- protected override bool GetMetricsEnabled(AzureStorageQueuesSettings settings)
- => false;
+ ///
+ /// 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);
- protected override bool GetTracingEnabled(AzureStorageQueuesSettings settings)
- => !settings.DisableTracing;
+ new StorageQueueComponent().AddClient(builder, DefaultConfigSectionName, configureSettings, configureClientBuilder, connectionName: name, serviceKey: name);
}
}
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..2d10ad7d829
--- /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 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;
+ }
+
+ DbConnectionStringBuilder builder = new() { ConnectionString = connectionString };
+
+ if (builder.TryGetValue("QueueName", out var containerName))
+ {
+ QueueName = containerName?.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/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..26d65b29196 100644
--- a/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Azure.Storage.Queues.Tests/ConformanceTests.cs
@@ -24,6 +24,9 @@ public class ConformanceTests : ConformanceTests "Azure.Storage.Queues.QueueClient";
+ // AzureStorageQueuesSettings subclassed by AzureStorageQueueSettings
+ protected override bool CheckOptionClassSealed => false;
+
protected override string[] RequiredLogCategories => new string[]
{
"Azure.Core",
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 61d88b75159..190b127ff6a 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj
+++ b/tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj
@@ -33,6 +33,7 @@
+
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs
index df14ddaaa3a..99e496e4d7c 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageEmulatorFunctionalTests.cs
@@ -1,10 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Aspire.TestUtilities;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
+using Aspire.TestUtilities;
using Azure.Storage.Blobs;
+using Azure.Storage.Queues;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
@@ -106,16 +107,65 @@ 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.AddQueues("queues");
+ var testQueue = queues.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 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 storage = builder.AddAzureStorage("storage").RunAsEmulator();
+ var blobs = storage.AddBlobs(blobsResourceName);
var container = blobs.AddBlobContainer(blobContainerName);
+ var queues = storage.AddQueues(queuesResourceName);
+ var queue = queues.AddQueue(queueName);
using var app = builder.Build();
await app.StartAsync();
@@ -123,8 +173,12 @@ 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.Configuration[$"ConnectionStrings:{queuesResourceName}"] = await queues.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
+ hb.Configuration[$"ConnectionStrings:{queueName}"] = await queue.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
hb.AddAzureBlobClient(blobsResourceName);
hb.AddAzureBlobContainerClient(blobContainerName);
+ hb.AddAzureQueueClient(queuesResourceName);
+ hb.AddAzureQueue(queueName);
using var host = hb.Build();
await host.StartAsync();
@@ -137,6 +191,11 @@ public async Task VerifyAzureStorageEmulatorResource()
await blobClient.UploadAsync(BinaryData.FromString("testValue"));
var downloadResult = (await blobClient.DownloadContentAsync()).Value;
+
+ var queueServiceClient = host.Services.GetRequiredService();
+ var queueClient = host.Services.GetRequiredService();
+ await queueClient.CreateIfNotExistsAsync(); // For Aspire 9.3 only
+
Assert.Equal("testValue", downloadResult.Content.ToString());
}
@@ -179,4 +238,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.AddQueues("queues");
+ var queue = queues.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.AddAzureQueueClient("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 bc0b1a97037..4207a4d4238 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureStorageExtensionsTests.cs
@@ -271,6 +271,111 @@ public void AddBlobContainer_ConnectionString_unresolved_expected()
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.AddQueues("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.AddQueues("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.AddQueues("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.AddQueues("queues");
+ var queue = queues.AddQueue(name: "myqueue", queueName);
+
+ string? conntionString = await ((IResourceWithConnectionString)queues.Resource).GetConnectionStringAsync();
+ string expected = $"{conntionString};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.AddQueues("queues");
+ var queue = queues.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.AddQueues("queues");
+ var queue = queues.AddQueue(name: "myqueue");
+
+ Assert.Equal("Endpoint={storage.outputs.queueEndpoint};QueueName=myqueue", queue.Resource.ConnectionStringExpression.ValueExpression);
+ }
+
[Fact]
public async Task ResourceNamesBicepValid()
{
@@ -280,11 +385,11 @@ public async Task ResourceNamesBicepValid()
var blobs = storage.AddBlobs("myblobs");
var blob = blobs.AddBlobContainer(name: "myContainer", blobContainerName: "my-blob-container");
var queues = storage.AddQueues("myqueues");
+ var queue = queues.AddQueue(name: "myqueue", queueName: "my-queue");
var tables = storage.AddTables("mytables");
var manifest = await AzureManifestUtils.GetManifestWithBicep(storage.Resource);
await Verify(manifest.BicepText, extension: "bicep");
-
}
}
diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishMode.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishMode.verified.bicep
index 0752967f8a5..1a1653ca102 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishMode.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishMode.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/AzureBicepResourceTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep
index e1550b10c65..802c2888572 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.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/AzureBicepResourceTests.AddAzureStorageViaRunMode.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaRunMode.verified.bicep
index 0752967f8a5..1a1653ca102 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaRunMode.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.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/AzureBicepResourceTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep
index e1550b10c65..802c2888572 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureBicepResourceTests.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/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#01.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#01.verified.bicep
index 445128945c3..c70f96f3dd5 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#01.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#01.verified.bicep
@@ -25,11 +25,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.ResourceNamesBicepValid.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep
index c539e1e9812..0b7641690cd 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureStorageExtensionsTests.ResourceNamesBicepValid.verified.bicep
@@ -31,6 +31,16 @@ resource myContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@
parent: blobs
}
+resource 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: queues
+}
+
output blobEndpoint string = storage.properties.primaryEndpoints.blob
output queueEndpoint string = storage.properties.primaryEndpoints.queue
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..e08f824468e 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroup.verified.bicep
@@ -7,11 +7,6 @@ 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
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..545234c9a9e 100644
--- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingStorageAccountWithResourceGroupAndStaticArguments.verified.bicep
@@ -5,11 +5,6 @@ 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
diff --git a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
index 4a1e253143a..b4e3e66cc4a 100644
--- a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
+++ b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs
@@ -91,6 +91,7 @@ await WaitForAllTextAsync(app,
{
"Aspire-injected EventHubProducerClient namespace: localhost",
"Aspire-injected QueueServiceClient URI: http://127.0.0.1:*/devstoreaccount1",
+ "Aspire-injected QueueClient URI: http://127.0.0.1:*/devstoreaccount1/myqueue1",
"Aspire-injected BlobServiceClient URI: http://127.0.0.1:*/devstoreaccount1",
"Aspire-injected BlobContainerClient URI: http://127.0.0.1:*/devstoreaccount1/myblobcontainer"
};