diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index c511bb0d0..dd2cee376 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -25,6 +25,7 @@ jobs:
Hosting.Azure.Dapr.Redis.Tests,
Hosting.Azure.Dapr.Tests,
Hosting.Azure.DataApiBuilder.Tests,
+ Hosting.Azure.Extensions.Tests,
Hosting.Bun.Tests,
Hosting.Dapr.Tests,
Hosting.DbGate.Tests,
diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx
index de6a29fef..3f21a87d7 100644
--- a/CommunityToolkit.Aspire.slnx
+++ b/CommunityToolkit.Aspire.slnx
@@ -8,6 +8,9 @@
+
+
+
@@ -184,6 +187,7 @@
+
@@ -242,6 +246,7 @@
+
diff --git a/README.md b/README.md
index 977386955..2804746d9 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ This repository contains the source code for the Aspire Community Toolkit, a col
| - **Learn More**: [`Hosting.SurrealDb`][surrealdb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.SurrealDb][surrealdb-shields]][surrealdb-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.SurrealDb][surrealdb-shields-preview]][surrealdb-nuget-preview] | An Aspire hosting integration leveraging the [SurrealDB](https://surrealdb.com/) container. |
| - **Learn More**: [`SurrealDb`][surrealdb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.SurrealDb][surrealdb-client-shields]][surrealdb-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.SurrealDb][surrealdb-client-shields-preview]][surrealdb-client-nuget-preview] | An Aspire client integration for the [SurrealDB](https://github.com/surrealdb/surrealdb.net/) package. |
| - **Learn More**: [`Hosting.Elasticsearch.Extensions`][elasticsearch-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions][elasticsearch-ext-shields]][elasticsearch-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions][elasticsearch-ext-shields-preview]][elasticsearch-ext-nuget-preview] | An integration that contains some additional extensions for hosting Elasticsearch container. |
+| - **Learn More**: [`Hosting.Azure.Extensions`][azure-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Azure.Extensions][azure-ext-shields]][azure-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Extensions][azure-ext-shields-preview]][azure-ext-nuget-preview] | An integration that contains some additional extensions for hosting Azure container. |
## 🙌 Getting Started
@@ -280,3 +281,8 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[elasticsearch-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions/
[elasticsearch-ext-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions?label=nuget%20(preview)
[elasticsearch-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions/absoluteLatest
+[azure-ext-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-azure-extensions
+[azure-ext-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Azure.Extensions
+[azure-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Azure.Extensions/
+[azure-ext-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Azure.Extensions?label=nuget%20(preview)
+[azure-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Azure.Extensions/absoluteLatest
diff --git a/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/CommunityToolkit.Aspire.Azure.Extensions.AppHost.csproj b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/CommunityToolkit.Aspire.Azure.Extensions.AppHost.csproj
new file mode 100644
index 000000000..36e400b5c
--- /dev/null
+++ b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/CommunityToolkit.Aspire.Azure.Extensions.AppHost.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Program.cs b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Program.cs
new file mode 100644
index 000000000..8c2c47ecf
--- /dev/null
+++ b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Program.cs
@@ -0,0 +1,16 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var storage = builder.AddAzureStorage("azure-storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithArgs("--disableProductStyleUrl")
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002)
+ .WithDataVolume("storage");
+ });
+
+var blobs = storage.AddBlobs("blobs").WithAzureStorageExplorer();
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Properties/launchSettings.json b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..dd6a59f53
--- /dev/null
+++ b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17160;http://localhost:15192",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21259",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23002",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22192"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15192",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19033",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18007",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20079"
+ }
+ }
+ }
+}
diff --git a/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/appsettings.json b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/appsettings.json
new file mode 100644
index 000000000..31c092aa4
--- /dev/null
+++ b/examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs
new file mode 100644
index 000000000..44f3c31f1
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Azure Storage Explorer resources to the application model.
+///
+public static class AzureBlobStorageResourceBuilderExtensions
+{
+ ///
+ /// Adds an Azure Storage Explorer instance to a Blob storage resource.
+ ///
+ /// The builder for the .
+ /// Configuration callback for Azure Storage Explorer container resource.
+ /// The name of the resource.
+ /// A reference to the .
+ ///
+ ///
+ /// Add an Azure Storage Explorer container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var storage = builder.AddAzureStorage("storage")
+ /// .RunAsEmulator(azurite =>
+ /// {
+ /// azurite
+ /// .WithBlobPort(27000)
+ /// .WithQueuePort(27001)
+ /// .WithTablePort(27002);
+ /// });
+ /// var blobs = storage.AddBlobs("blobs")
+ /// .WithAzureStorageExplorer();
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithAzureStorageExplorer(
+ this IResourceBuilder blobs,
+ Action>? configureContainer = null,
+ [ResourceName] string? name = null
+ )
+ {
+ string resourceNname = name ?? $"{blobs.Resource.Name}-explorer";
+ var builder = blobs.ApplicationBuilder
+ .AddAzureStorageExplorer(resourceNname)
+ .WithStorageResource(blobs)
+ .WithParentRelationship(blobs);
+
+ if (blobs.Resource.Parent.IsEmulator)
+ {
+ builder.WithAzurite();
+ }
+
+ configureContainer?.Invoke(builder);
+
+ return blobs;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs
new file mode 100644
index 000000000..9fd690cb8
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Azure Storage Explorer resources to the application model.
+///
+public static class AzureQueueStorageResourceBuilderExtensions
+{
+ ///
+ /// Adds an Azure Storage Explorer instance to a Queue storage resource.
+ ///
+ /// The builder for the .
+ /// Configuration callback for Azure Storage Explorer container resource.
+ /// The name of the resource.
+ /// A reference to the .
+ ///
+ ///
+ /// Add an Azure Storage Explorer container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var storage = builder.AddAzureStorage("storage")
+ /// .RunAsEmulator(azurite =>
+ /// {
+ /// azurite
+ /// .WithBlobPort(27000)
+ /// .WithQueuePort(27001)
+ /// .WithTablePort(27002);
+ /// });
+ /// var queues = storage.AddQueues("queues")
+ /// .WithAzureStorageExplorer();
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithAzureStorageExplorer(
+ this IResourceBuilder queues,
+ Action>? configureContainer = null,
+ [ResourceName] string? name = null
+ )
+ {
+ string resourceNname = name ?? $"{queues.Resource.Name}-explorer";
+ var builder = queues.ApplicationBuilder
+ .AddAzureStorageExplorer(resourceNname)
+ .WithStorageResource(queues)
+ .WithParentRelationship(queues);
+
+ if (queues.Resource.Parent.IsEmulator)
+ {
+ builder.WithAzurite();
+ }
+
+ configureContainer?.Invoke(builder);
+
+ return queues;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs
new file mode 100644
index 000000000..50855c160
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Azure Storage Explorer resources to the application model.
+///
+public static class AzureStorageExplorerBuilderExtensions
+{
+ private const int AzureStorageExplorerPort = 8080;
+
+ ///
+ /// Configures the host port that the Azure Storage Explorer resource is exposed on instead of using randomly assigned port.
+ ///
+ /// The resource builder for Azure Storage Explorer.
+ /// The port to bind on the host. If is used random port will be assigned.
+ /// The resource builder for Azure Storage Explorer.
+ public static IResourceBuilder WithHostPort(this IResourceBuilder builder, int? port)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.WithEndpoint(AzureStorageExplorerResource.PrimaryEndpointName, endpoint =>
+ {
+ endpoint.Port = port;
+ });
+ }
+
+ ///
+ /// Adds an Azure Storage Explorer resource to the application model.
+ /// The default image is and the tag is .
+ ///
+ /// The .
+ /// The name of the resource.
+ /// The host port for the Azure Storage Explorer instance.
+ /// A reference to the .
+ internal static IResourceBuilder AddAzureStorageExplorer(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ int? port = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ var explorer = new AzureStorageExplorerResource(name);
+
+ return builder.AddResource(explorer)
+ .WithHttpEndpoint(
+ port: port,
+ targetPort: AzureStorageExplorerPort,
+ name: AzureStorageExplorerResource.PrimaryEndpointName
+ )
+ .WithImage(AzureStorageExplorerContainerImageTags.Image, AzureStorageExplorerContainerImageTags.Tag)
+ .WithImageRegistry(AzureStorageExplorerContainerImageTags.Registry)
+ .ExcludeFromManifest();
+ }
+
+ ///
+ /// Allows Azure Storage Explorer to work with the Azure Emulator (Azurite).
+ ///
+ /// The builder for the .
+ /// A reference to the .
+ internal static IResourceBuilder WithAzurite(
+ this IResourceBuilder builder
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.WithEnvironment("AZURITE", "true");
+ }
+
+ internal static IResourceBuilder WithStorageResource(
+ this IResourceBuilder builder,
+ IResourceBuilder resourceBuilder
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder
+ .WaitFor(resourceBuilder)
+ .WithEnvironment(e =>
+ {
+ e.EnvironmentVariables["AZURE_STORAGE_CONNECTIONSTRING"] =
+ new ConnectionStringReference(resourceBuilder.Resource, optional: false);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerContainerImageTags.cs
new file mode 100644
index 000000000..baa3e1d1c
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerContainerImageTags.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting;
+
+internal sealed class AzureStorageExplorerContainerImageTags
+{
+ /// docker.io
+ public const string Registry = "docker.io";
+ /// sebagomez/azurestorageexplorer
+ public const string Image = "sebagomez/azurestorageexplorer";
+ /// 3.1.0
+ public const string Tag = "3.1.0";
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs
new file mode 100644
index 000000000..b46933244
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents an Azure Storage Explorer container.
+///
+/// The name of the resource.
+public class AzureStorageExplorerResource([ResourceName] string name) : ContainerResource(name)
+{
+ internal const string PrimaryEndpointName = "http";
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the Azure Storage Explorer instance.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the host endpoint reference for this resource.
+ ///
+ public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);
+
+ ///
+ /// Gets the port endpoint reference for this resource.
+ ///
+ public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs
new file mode 100644
index 000000000..17ec3a282
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Azure Storage Explorer resources to the application model.
+///
+public static class AzureTableStorageResourceBuilderExtensions
+{
+ ///
+ /// Adds an Azure Storage Explorer instance to a Table storage resource.
+ ///
+ /// The builder for the .
+ /// Configuration callback for Azure Storage Explorer container resource.
+ /// The name of the resource.
+ /// A reference to the .
+ ///
+ ///
+ /// Add an Azure Storage Explorer container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var storage = builder.AddAzureStorage("storage")
+ /// .RunAsEmulator(azurite =>
+ /// {
+ /// azurite
+ /// .WithBlobPort(27000)
+ /// .WithQueuePort(27001)
+ /// .WithTablePort(27002);
+ /// });
+ /// var tables = storage.AddTables("tables")
+ /// .WithAzureStorageExplorer();
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithAzureStorageExplorer(
+ this IResourceBuilder tables,
+ Action>? configureContainer = null,
+ [ResourceName] string? name = null
+ )
+ {
+ string resourceNname = name ?? $"{tables.Resource.Name}-explorer";
+ var builder = tables.ApplicationBuilder
+ .AddAzureStorageExplorer(resourceNname)
+ .WithStorageResource(tables)
+ .WithParentRelationship(tables);
+
+ if (tables.Resource.Parent.IsEmulator)
+ {
+ builder.WithAzurite();
+ }
+
+ configureContainer?.Invoke(builder);
+
+ return tables;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/CommunityToolkit.Aspire.Hosting.Azure.Extensions.csproj b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/CommunityToolkit.Aspire.Hosting.Azure.Extensions.csproj
new file mode 100644
index 000000000..46ea1cbee
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/CommunityToolkit.Aspire.Hosting.Azure.Extensions.csproj
@@ -0,0 +1,13 @@
+
+
+
+ hosting azure extensions
+ Azure extensions support for .NET Aspire.
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/README.md
new file mode 100644
index 000000000..72b9dba0a
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/README.md
@@ -0,0 +1,39 @@
+# CommunityToolkit.Aspire.Hosting.Azure.Extensions library
+
+This integration contains extensions for the [Azure Storage hosting package](https://nuget.org/packages/Aspire.Hosting.Azure.Storage) for .NET Aspire.
+
+The integration provides support for running [Azure Storage Explorer](https://github.com/sebagomez/azurestorageexplorer) to interact with the Azure Storage resource: blobs, queues and tables.
+
+## Getting Started
+
+### Install the package
+
+In your AppHost project, install the package using the following command:
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.Azure.Extensions
+```
+
+### Example usage
+
+Then, in the _Program.cs_ file of `AppHost`, define an Azure Storage resource, then call `AddAzureStorageExplorer`:
+
+```csharp
+var storage = builder.AddAzureStorage("storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002);
+ });
+var blobs = storage.AddBlobs("blobs").WithAzureStorageExplorer();
+```
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-azure-extensions
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs
new file mode 100644
index 000000000..96fb0c6d0
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs
@@ -0,0 +1,21 @@
+using Aspire.Components.Common.Tests;
+using CommunityToolkit.Aspire.Testing;
+
+namespace CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests;
+
+[RequiresDocker]
+public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture>
+{
+ [Fact]
+ public async Task ResourceStartsAndRespondsOk()
+ {
+ const string resourceName = "blobs-explorer";
+
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(5));
+
+ using var httpClient = fixture.CreateHttpClient(resourceName);
+ using var response = await httpClient.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests.csproj
new file mode 100644
index 000000000..ce32482de
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests.csproj
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/ResourceCreationTests.cs
new file mode 100644
index 000000000..f1898ca6f
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/ResourceCreationTests.cs
@@ -0,0 +1,135 @@
+using Aspire.Hosting;
+
+namespace CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests;
+
+public class ResourceCreationTests
+{
+ [Fact]
+ public async Task WithAzureStorageExplorerAddsAnnotations()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+
+ var azureStorageResourceBuilder = builder.AddAzureStorage("storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002);
+ });
+ var blobsResourceBuilder = azureStorageResourceBuilder.AddBlobs("blobs").WithAzureStorageExplorer();
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var azureStorageExplorerResource = appModel.Resources.OfType().SingleOrDefault();
+
+ Assert.NotNull(azureStorageExplorerResource);
+
+ Assert.Equal("blobs-explorer", azureStorageExplorerResource.Name);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ var envs = await azureStorageExplorerResource.GetEnvironmentVariableValuesAsync();
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ Assert.NotEmpty(envs);
+
+ Assert.Collection(envs,
+ item =>
+ {
+ Assert.Equal("AZURE_STORAGE_CONNECTIONSTRING", item.Key);
+ Assert.Equal("DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://storage.dev.internal:10000/devstoreaccount1;", item.Value);
+ },
+ item =>
+ {
+ Assert.Equal("AZURITE", item.Key);
+ Assert.Equal("true", item.Value);
+ });
+ }
+
+ [Fact]
+ public void MultipleWithAzureStorageExplorerCallsAddsOneAzureStorageExplorerResourceForEachResource()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+
+ var azureStorageResourceBuilder = builder.AddAzureStorage("storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002);
+ });
+ var blobsResourceBuilder = azureStorageResourceBuilder.AddBlobs("blobs").WithAzureStorageExplorer();
+ var queuesResourceBuilder = azureStorageResourceBuilder.AddBlobs("queues").WithAzureStorageExplorer();
+ var tablesResourceBuilder = azureStorageResourceBuilder.AddBlobs("tables").WithAzureStorageExplorer();
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var azureStorageExplorerResources = appModel.Resources.OfType().ToList();
+
+ Assert.Equal(3, azureStorageExplorerResources.Count);
+ Assert.Equal("blobs-explorer", azureStorageExplorerResources[0].Name);
+ Assert.Equal("queues-explorer", azureStorageExplorerResources[1].Name);
+ Assert.Equal("tables-explorer", azureStorageExplorerResources[2].Name);
+ }
+
+ [Fact]
+ public void WithAzureStorageExplorerShouldChangeAzureStorageExplorerHostPort()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+
+ var azureStorageResourceBuilder = builder.AddAzureStorage("storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002);
+ });
+ var blobsResourceBuilder = azureStorageResourceBuilder
+ .AddBlobs("blobs")
+ .WithAzureStorageExplorer(c => c.WithHostPort(8068));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var azureStorageExplorerResource = appModel.Resources.OfType().SingleOrDefault();
+ Assert.NotNull(azureStorageExplorerResource);
+
+ var primaryEndpoint = azureStorageExplorerResource.Annotations.OfType().Single();
+ Assert.Equal(8068, primaryEndpoint.Port);
+ }
+
+ [Fact]
+ public void WithAzureStorageExplorerShouldChangeAzureStorageExplorerContainerImageTag()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+
+ var azureStorageResourceBuilder = builder.AddAzureStorage("storage")
+ .RunAsEmulator(azurite =>
+ {
+ azurite
+ .WithBlobPort(27000)
+ .WithQueuePort(27001)
+ .WithTablePort(27002);
+ });
+ var blobsResourceBuilder = azureStorageResourceBuilder
+ .AddBlobs("blobs")
+ .WithAzureStorageExplorer(c => c.WithImageTag("manualTag"));
+
+ using var app = builder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var azureStorageExplorerResource = appModel.Resources.OfType().SingleOrDefault();
+ Assert.NotNull(azureStorageExplorerResource);
+
+ var containerImageAnnotation = azureStorageExplorerResource.Annotations.OfType().Single();
+ Assert.Equal("manualTag", containerImageAnnotation.Tag);
+ }
+}
\ No newline at end of file