diff --git a/Testcontainers.sln b/Testcontainers.sln
index e8c10a811..5dc5dde7b 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ActiveMq", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ArangoDb", "src\Testcontainers.ArangoDb\Testcontainers.ArangoDb.csproj", "{AB9C1563-07C7-4685-BACD-BB1FF64B3611}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.AspireDashboard", "src\Testcontainers.AspireDashboard\Testcontainers.AspireDashboard.csproj", "{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Azurite", "src\Testcontainers.Azurite\Testcontainers.Azurite.csproj", "{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery", "src\Testcontainers.BigQuery\Testcontainers.BigQuery.csproj", "{A9FF9C7F-BBA0-4B44-90B7-48A60F9E00F3}"
@@ -105,6 +107,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ActiveMq.Tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ArangoDb.Tests", "tests\Testcontainers.ArangoDb.Tests\Testcontainers.ArangoDb.Tests.csproj", "{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.AspireDashboard.Tests", "tests\Testcontainers.AspireDashboard.Tests\Testcontainers.AspireDashboard.Tests.csproj", "{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Azurite.Tests", "tests\Testcontainers.Azurite.Tests\Testcontainers.Azurite.Tests.csproj", "{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery.Tests", "tests\Testcontainers.BigQuery.Tests\Testcontainers.BigQuery.Tests.csproj", "{03E60673-078A-4508-99AD-8537CE6F78F1}"
@@ -216,6 +220,10 @@ Global
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Release|Any CPU.Build.0 = Release|Any CPU
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -392,6 +400,10 @@ Global
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Release|Any CPU.Build.0 = Release|Any CPU
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -584,6 +596,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{AB9C1563-07C7-4685-BACD-BB1FF64B3611} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{A9FF9C7F-BBA0-4B44-90B7-48A60F9E00F3} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{302EC1E0-AE75-4E99-A6BF-524F35338BC8} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -628,6 +641,7 @@ Global
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{AB93C67F-0A53-4525-AE6C-29B065820ABE} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {82F79BC0-5E4A-4380-82C5-8B6E3CF05958} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{03E60673-078A-4508-99AD-8537CE6F78F1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{2E7B92E3-8526-4706-90F3-00F0F5C47C37} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
diff --git a/docs/modules/aspire-dashboard.md b/docs/modules/aspire-dashboard.md
new file mode 100644
index 000000000..087352cb3
--- /dev/null
+++ b/docs/modules/aspire-dashboard.md
@@ -0,0 +1,46 @@
+# Aspire Dashboard
+
+## Configuration
+
+The Aspire Dashboard can be configured in the following ways:
+
+- Use `AllowAnonymous(true)` to allow anonymous access to the dashboard.
+- Use `AllowUnsecuredTransport` to allow unsecured transport, such as HTTP.
+- Aspire Dashboard usually runs on port 18888. You can change the port by using the `WithPortBinding`
+
+```csharp
+public sealed class AspireDashboardContainerTest : IAsyncLifetime
+{
+ private readonly AspireDashboardContainer _container = new AspireDashboardBuilder()
+ .AllowAnonymous(true)
+ .AllowUnsecuredTransport(false)
+ .WithPortBinding(
+ AspireDashboardBuilder.AspireDashboardFrontendPort,
+ AspireDashboardBuilder.AspireDashboardFrontendPort
+ )
+ .Build();
+
+ public Task InitializeAsync()
+ {
+ return _container.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _container.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ public async Task GetDashboardReturnsHttpStatusCodeOk()
+ {
+ using var httpClient = new HttpClient();
+ var address = new Uri(_container.GetDashboardUrl());
+ httpClient.BaseAddress = address;
+
+ using var response = await httpClient.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+}
+```
+[Aspire Dashboard on Microsoft Artifact Registry](https://mcr.microsoft.com/en-us/product/dotnet/aspire-dashboard/tags)
diff --git a/mkdocs.yml b/mkdocs.yml
index 8c43c04de..f6a5069af 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -47,3 +47,4 @@ nav:
- modules/postgres.md
- modules/pulsar.md
- modules/rabbitmq.md
+ - modules/aspire-dashboard.md
diff --git a/src/Testcontainers.AspireDashboard/.editorconfig b/src/Testcontainers.AspireDashboard/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.AspireDashboard/AspireDashboardBuilder.cs b/src/Testcontainers.AspireDashboard/AspireDashboardBuilder.cs
new file mode 100644
index 000000000..bac21cfc3
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/AspireDashboardBuilder.cs
@@ -0,0 +1,118 @@
+namespace Testcontainers.AspireDashboard;
+
+///
+[PublicAPI]
+public sealed class AspireDashboardBuilder
+ : ContainerBuilder<
+ AspireDashboardBuilder,
+ AspireDashboardContainer,
+ AspireDashboardConfiguration
+ >
+{
+ // https://mcr.microsoft.com/en-us/product/dotnet/aspire-dashboard/tags
+ public const string AspireDashboardImage = "mcr.microsoft.com/dotnet/aspire-dashboard:8.1.0";
+
+ public const ushort AspireDashboardFrontendPort = 18888;
+
+ public const ushort AspireDashboardOtlpPort = 18889;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AspireDashboardBuilder()
+ : this(new AspireDashboardConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private AspireDashboardBuilder(AspireDashboardConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override AspireDashboardConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Configures the dashboard to accept anonymous access.
+ ///
+ /// A value indicating whether anonymous access is allowed.
+ /// A configured instance of .
+ public AspireDashboardBuilder AllowAnonymous(bool allowed)
+ {
+ return Merge(DockerResourceConfiguration, new AspireDashboardConfiguration())
+ .WithEnvironment(
+ "DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS",
+ allowed.ToString().ToLowerInvariant()
+ );
+ }
+
+ ///
+ /// Configures the dashboard to allow unsecured transport.
+ ///
+ /// A value indicating whether unsecured transport is allowed.
+ /// A configured instance of .
+ public AspireDashboardBuilder AllowUnsecuredTransport(bool allowed)
+ {
+ return Merge(DockerResourceConfiguration, new AspireDashboardConfiguration())
+ .WithEnvironment(
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT",
+ allowed.ToString().ToLowerInvariant()
+ );
+ }
+
+ ///
+ public override AspireDashboardContainer Build()
+ {
+ Validate();
+ return new AspireDashboardContainer(DockerResourceConfiguration);
+ }
+
+ ///
+ protected override AspireDashboardBuilder Init()
+ {
+ return base.Init()
+ .WithImage(AspireDashboardImage)
+ .WithPortBinding(AspireDashboardFrontendPort, true)
+ .WithPortBinding(AspireDashboardOtlpPort, true)
+ .AllowAnonymous(true)
+ .WithWaitStrategy(
+ Wait.ForUnixContainer()
+ .UntilHttpRequestIsSucceeded(r => r.ForPort(AspireDashboardFrontendPort))
+ );
+ }
+
+ ///
+ protected override AspireDashboardBuilder Clone(
+ IResourceConfiguration resourceConfiguration
+ )
+ {
+ return Merge(
+ DockerResourceConfiguration,
+ new AspireDashboardConfiguration(resourceConfiguration)
+ );
+ }
+
+ ///
+ protected override AspireDashboardBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(
+ DockerResourceConfiguration,
+ new AspireDashboardConfiguration(resourceConfiguration)
+ );
+ }
+
+ ///
+ protected override AspireDashboardBuilder Merge(
+ AspireDashboardConfiguration oldValue,
+ AspireDashboardConfiguration newValue
+ )
+ {
+ return new AspireDashboardBuilder(new AspireDashboardConfiguration(oldValue, newValue));
+ }
+}
diff --git a/src/Testcontainers.AspireDashboard/AspireDashboardConfiguration.cs b/src/Testcontainers.AspireDashboard/AspireDashboardConfiguration.cs
new file mode 100644
index 000000000..91e162357
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/AspireDashboardConfiguration.cs
@@ -0,0 +1,54 @@
+namespace Testcontainers.AspireDashboard;
+
+///
+[PublicAPI]
+public sealed class AspireDashboardConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AspireDashboardConfiguration() { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public AspireDashboardConfiguration(
+ IResourceConfiguration resourceConfiguration
+ )
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public AspireDashboardConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public AspireDashboardConfiguration(AspireDashboardConfiguration resourceConfiguration)
+ : this(new AspireDashboardConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public AspireDashboardConfiguration(
+ AspireDashboardConfiguration oldValue,
+ AspireDashboardConfiguration newValue
+ )
+ : base(oldValue, newValue) { }
+}
diff --git a/src/Testcontainers.AspireDashboard/AspireDashboardContainer.cs b/src/Testcontainers.AspireDashboard/AspireDashboardContainer.cs
new file mode 100644
index 000000000..cc9cd6e36
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/AspireDashboardContainer.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.Logging;
+
+namespace Testcontainers.AspireDashboard;
+
+///
+[PublicAPI]
+public sealed class AspireDashboardContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public AspireDashboardContainer(AspireDashboardConfiguration configuration)
+ : base(configuration)
+ {
+ Started += (_, _) => Logger.LogInformation("AspireDashboard container is ready!");
+ Logger.LogInformation(
+ "Dashboard available at {Url}.",
+ new UriBuilder(
+ Uri.UriSchemeHttp,
+ Hostname,
+ GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardFrontendPort)
+ )
+ );
+ Logger.LogInformation(
+ "OTLP endpoint available at {Url}.",
+ new UriBuilder(
+ Uri.UriSchemeHttp,
+ Hostname,
+ GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardOtlpPort)
+ )
+ );
+ }
+
+ ///
+ /// Gets the AspireDashboard URL.
+ ///
+ /// The AspireDashboard URL.
+ public string GetDashboardUrl()
+ {
+ var endpoint = new UriBuilder(
+ Uri.UriSchemeHttp,
+ Hostname,
+ GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardFrontendPort)
+ );
+
+ return endpoint.ToString();
+ }
+
+ ///
+ /// Gets the AspireDashboard OTLP endpoint URL.
+ ///
+ /// The AspireDashboard OTLP endpoint URL.
+ public string GetOtlpEndpointUrl()
+ {
+ var endpoint = new UriBuilder(
+ Uri.UriSchemeHttp,
+ Hostname,
+ GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardOtlpPort)
+ );
+
+ return endpoint.ToString();
+ }
+}
diff --git a/src/Testcontainers.AspireDashboard/Testcontainers.AspireDashboard.csproj b/src/Testcontainers.AspireDashboard/Testcontainers.AspireDashboard.csproj
new file mode 100644
index 000000000..8b2ed72c6
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/Testcontainers.AspireDashboard.csproj
@@ -0,0 +1,12 @@
+
+
+ net6.0;net8.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.AspireDashboard/Usings.cs b/src/Testcontainers.AspireDashboard/Usings.cs
new file mode 100644
index 000000000..8642a70f5
--- /dev/null
+++ b/src/Testcontainers.AspireDashboard/Usings.cs
@@ -0,0 +1,11 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+global using System.Net.Http;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.AspireDashboard.Tests/.editorconfig b/tests/Testcontainers.AspireDashboard.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.AspireDashboard.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.AspireDashboard.Tests/AspireDashboardContainerTest.cs b/tests/Testcontainers.AspireDashboard.Tests/AspireDashboardContainerTest.cs
new file mode 100644
index 000000000..13f0c53fa
--- /dev/null
+++ b/tests/Testcontainers.AspireDashboard.Tests/AspireDashboardContainerTest.cs
@@ -0,0 +1,39 @@
+namespace Testcontainers.AspireDashboard;
+
+public sealed class AspireDashboardContainerTest : IAsyncLifetime
+{
+ private readonly AspireDashboardContainer _container = new AspireDashboardBuilder()
+ .AllowAnonymous(true)
+ .AllowUnsecuredTransport(false)
+ .WithPortBinding(
+ AspireDashboardBuilder.AspireDashboardFrontendPort,
+ AspireDashboardBuilder.AspireDashboardFrontendPort
+ )
+ .Build();
+
+ public Task InitializeAsync()
+ {
+ return _container.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _container.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ public async Task GetDashboardReturnsHttpStatusCodeOk()
+ {
+ // Given
+ using var httpClient = new HttpClient();
+
+ var address = new Uri(_container.GetDashboardUrl());
+ httpClient.BaseAddress = address;
+
+ // When
+ using var response = await httpClient.GetAsync("/");
+
+ // Then
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+}
diff --git a/tests/Testcontainers.AspireDashboard.Tests/Testcontainers.AspireDashboard.Tests.csproj b/tests/Testcontainers.AspireDashboard.Tests/Testcontainers.AspireDashboard.Tests.csproj
new file mode 100644
index 000000000..dd7b9ac30
--- /dev/null
+++ b/tests/Testcontainers.AspireDashboard.Tests/Testcontainers.AspireDashboard.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+ net8.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.AspireDashboard.Tests/Usings.cs b/tests/Testcontainers.AspireDashboard.Tests/Usings.cs
new file mode 100644
index 000000000..dc15b5753
--- /dev/null
+++ b/tests/Testcontainers.AspireDashboard.Tests/Usings.cs
@@ -0,0 +1,6 @@
+global using System;
+global using System.Net;
+global using System.Net.Http;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Commons;
+global using Xunit;
\ No newline at end of file