From c4f7ab7e82f2f1158acccfdeffe1176583d59e05 Mon Sep 17 00:00:00 2001 From: Odonno Date: Fri, 9 Jan 2026 17:55:22 +0100 Subject: [PATCH 1/5] feat: create AzureStorageExplorer hosting integration --- CommunityToolkit.Aspire.slnx | 4 + ...kit.Aspire.Azure.Extensions.AppHost.csproj | 16 +++ .../Program.cs | 16 +++ .../Properties/launchSettings.json | 31 ++++++ .../appsettings.json | 9 ++ ...ureBlobStorageResourceBuilderExtensions.cs | 38 ++++++++ ...reQueueStorageResourceBuilderExtensions.cs | 38 ++++++++ .../AzureStorageExplorerBuilderExtensions.cs | 97 +++++++++++++++++++ .../AzureStorageExplorerContainerImageTags.cs | 14 +++ .../AzureStorageExplorerResource.cs | 38 ++++++++ ...reTableStorageResourceBuilderExtensions.cs | 38 ++++++++ ...kit.Aspire.Hosting.Azure.Extensions.csproj | 13 +++ .../README.md | 39 ++++++++ 13 files changed, 391 insertions(+) create mode 100644 examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/CommunityToolkit.Aspire.Azure.Extensions.AppHost.csproj create mode 100644 examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Program.cs create mode 100644 examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/Properties/launchSettings.json create mode 100644 examples/azure-ext/CommunityToolkit.Aspire.Azure.Extensions.AppHost/appsettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/CommunityToolkit.Aspire.Hosting.Azure.Extensions.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/README.md diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index de6a29fef..44a56d60c 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -8,6 +8,9 @@ + + + @@ -184,6 +187,7 @@ + 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..631011ec7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs @@ -0,0 +1,38 @@ +// 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 . + /// The name of the resource. + /// A reference to the . + public static IResourceBuilder WithAzureStorageExplorer( + this IResourceBuilder blobs, + [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(); + } + + 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..1c3f452cb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs @@ -0,0 +1,38 @@ +// 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 . + /// The name of the resource. + /// A reference to the . + public static IResourceBuilder WithAzureStorageExplorer( + this IResourceBuilder queues, + [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(); + } + + 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..1d9988223 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs @@ -0,0 +1,97 @@ +// 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; + + /// + /// 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 . + /// + /// + /// 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"); + /// + /// var azureStorageExplorer = builder + /// .AddAzureStorageExplorer("explorer") + /// .WithAzurite() + /// .WithBlobs(blobs); + /// + /// builder.Build().Run(); + /// + /// + /// + 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.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..485a74852 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs @@ -0,0 +1,38 @@ +// 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. +/// +public class AzureStorageExplorerResource : ContainerResource +{ + internal const string PrimaryEndpointName = "http"; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + public AzureStorageExplorerResource( + [ResourceName] string name + ) : base(name) + { + PrimaryEndpoint = new(this, PrimaryEndpointName); + } + + /// + /// Gets the primary endpoint for the Azure Storage Explorer instance. + /// + public EndpointReference PrimaryEndpoint { get; } + + /// + /// 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); +} \ No newline at end of file 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..b66a2f714 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs @@ -0,0 +1,38 @@ +// 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 . + /// The name of the resource. + /// A reference to the . + public static IResourceBuilder WithAzureStorageExplorer( + this IResourceBuilder tables, + [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(); + } + + 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 From c12d0e4bf74d7bbe8aa51a13bc84ea5dbb5e5265 Mon Sep 17 00:00:00 2001 From: Odonno Date: Tue, 13 Jan 2026 20:57:16 +0100 Subject: [PATCH 2/5] test: add tests for AzureStorageExplorer hosting integration --- .github/workflows/tests.yaml | 1 + CommunityToolkit.Aspire.slnx | 1 + ...ureBlobStorageResourceBuilderExtensions.cs | 27 +++- ...reQueueStorageResourceBuilderExtensions.cs | 25 ++++ .../AzureStorageExplorerBuilderExtensions.cs | 53 +++---- .../AzureStorageExplorerResource.cs | 20 +-- ...reTableStorageResourceBuilderExtensions.cs | 25 ++++ .../AppHostTests.cs | 19 +++ ...pire.Hosting.Azure.Extensions.Tests.csproj | 8 ++ .../ResourceCreationTests.cs | 135 ++++++++++++++++++ 10 files changed, 269 insertions(+), 45 deletions(-) create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/ResourceCreationTests.cs 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 44a56d60c..3f21a87d7 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -246,6 +246,7 @@ + diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs index 631011ec7..44f3c31f1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureBlobStorageResourceBuilderExtensions.cs @@ -15,10 +15,33 @@ 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 ) { @@ -32,7 +55,9 @@ public static IResourceBuilder WithAzureStorageExplore { 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 index 1c3f452cb..9fd690cb8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureQueueStorageResourceBuilderExtensions.cs @@ -15,10 +15,33 @@ 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 ) { @@ -32,6 +55,8 @@ public static IResourceBuilder WithAzureStorageExplor { builder.WithAzurite(); } + + configureContainer?.Invoke(builder); return queues; } diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs index 1d9988223..50855c160 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerBuilderExtensions.cs @@ -11,6 +11,22 @@ namespace Aspire.Hosting; 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. @@ -20,31 +36,6 @@ public static class AzureStorageExplorerBuilderExtensions /// The name of the resource. /// The host port for the Azure Storage Explorer instance. /// 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"); - /// - /// var azureStorageExplorer = builder - /// .AddAzureStorageExplorer("explorer") - /// .WithAzurite() - /// .WithBlobs(blobs); - /// - /// builder.Build().Run(); - /// - /// - /// internal static IResourceBuilder AddAzureStorageExplorer( this IDistributedApplicationBuilder builder, [ResourceName] string name, @@ -88,10 +79,12 @@ IResourceBuilder resourceBuilder { ArgumentNullException.ThrowIfNull(builder); - return builder.WithEnvironment(e => - { - e.EnvironmentVariables["AZURE_STORAGE_CONNECTIONSTRING"] = - new ConnectionStringReference(resourceBuilder.Resource, optional: false); - }); + 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/AzureStorageExplorerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs index 485a74852..b46933244 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureStorageExplorerResource.cs @@ -6,25 +6,17 @@ namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents an Azure Storage Explorer container. /// -public class AzureStorageExplorerResource : ContainerResource +/// The name of the resource. +public class AzureStorageExplorerResource([ResourceName] string name) : ContainerResource(name) { internal const string PrimaryEndpointName = "http"; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the resource. - public AzureStorageExplorerResource( - [ResourceName] string name - ) : base(name) - { - PrimaryEndpoint = new(this, PrimaryEndpointName); - } + + private EndpointReference? _primaryEndpoint; /// /// Gets the primary endpoint for the Azure Storage Explorer instance. /// - public EndpointReference PrimaryEndpoint { get; } + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); /// /// Gets the host endpoint reference for this resource. @@ -35,4 +27,4 @@ [ResourceName] string name /// Gets the port endpoint reference for this resource. /// public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port); -} \ No newline at end of file +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs index b66a2f714..17ec3a282 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Extensions/AzureTableStorageResourceBuilderExtensions.cs @@ -15,10 +15,33 @@ 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 ) { @@ -32,6 +55,8 @@ public static IResourceBuilder WithAzureStorageExplor { builder.WithAzurite(); } + + configureContainer?.Invoke(builder); return tables; } 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..4edca2331 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs @@ -0,0 +1,19 @@ +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests; + +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 From 3fe264f538c3dd742bb3fd3e45af68048ef2f8e8 Mon Sep 17 00:00:00 2001 From: Odonno Date: Tue, 13 Jan 2026 22:15:32 +0100 Subject: [PATCH 3/5] docs: update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 086143bdf02b33f9a6231028c6428fa7663d9f35 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 16 Jan 2026 11:59:21 +1100 Subject: [PATCH 4/5] Update tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs --- .../AppHostTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs index 4edca2331..3c92045cc 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs @@ -2,6 +2,7 @@ namespace CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests; +[RequiresDocker] public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> { [Fact] From dc9feaa591a2bfd430022f6e403f8efbc995a2dc Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 16 Jan 2026 12:06:30 +1100 Subject: [PATCH 5/5] Apply suggestions from code review --- .../AppHostTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs index 3c92045cc..96fb0c6d0 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests/AppHostTests.cs @@ -1,3 +1,4 @@ +using Aspire.Components.Common.Tests; using CommunityToolkit.Aspire.Testing; namespace CommunityToolkit.Aspire.Hosting.Azure.Extensions.Tests;