Skip to content

Commit 9bbf81b

Browse files
feat: Add Aspire dashboard module (#1194)
Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com>
1 parent 0ba7b9c commit 9bbf81b

19 files changed

Lines changed: 413 additions & 3 deletions

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
<PackageVersion Include="Npgsql" Version="6.0.11"/>
8585
<PackageVersion Include="OllamaSharp" Version="5.1.13"/>
8686
<PackageVersion Include="OpenSearch.Client" Version="1.8.0"/>
87+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.16.0"/>
8788
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.7.0"/>
8889
<PackageVersion Include="Qdrant.Client" Version="1.13.0"/>
8990
<PackageVersion Include="RabbitMQ.Client" Version="6.4.0"/>

Testcontainers.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ActiveMq", "
2525
EndProject
2626
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ArangoDb", "src\Testcontainers.ArangoDb\Testcontainers.ArangoDb.csproj", "{AB9C1563-07C7-4685-BACD-BB1FF64B3611}"
2727
EndProject
28+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.AspireDashboard", "src\Testcontainers.AspireDashboard\Testcontainers.AspireDashboard.csproj", "{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}"
29+
EndProject
2830
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Azurite", "src\Testcontainers.Azurite\Testcontainers.Azurite.csproj", "{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}"
2931
EndProject
3032
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery", "src\Testcontainers.BigQuery\Testcontainers.BigQuery.csproj", "{A9FF9C7F-BBA0-4B44-90B7-48A60F9E00F3}"
@@ -153,6 +155,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ActiveMq.Tes
153155
EndProject
154156
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ArangoDb.Tests", "tests\Testcontainers.ArangoDb.Tests\Testcontainers.ArangoDb.Tests.csproj", "{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}"
155157
EndProject
158+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.AspireDashboard.Tests", "tests\Testcontainers.AspireDashboard.Tests\Testcontainers.AspireDashboard.Tests.csproj", "{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}"
159+
EndProject
156160
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Azurite.Tests", "tests\Testcontainers.Azurite.Tests\Testcontainers.Azurite.Tests.csproj", "{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}"
157161
EndProject
158162
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery.Tests", "tests\Testcontainers.BigQuery.Tests\Testcontainers.BigQuery.Tests.csproj", "{03E60673-078A-4508-99AD-8537CE6F78F1}"
@@ -307,6 +311,10 @@ Global
307311
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Debug|Any CPU.Build.0 = Debug|Any CPU
308312
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Release|Any CPU.ActiveCfg = Release|Any CPU
309313
{AB9C1563-07C7-4685-BACD-BB1FF64B3611}.Release|Any CPU.Build.0 = Release|Any CPU
314+
{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
315+
{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
316+
{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
317+
{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A}.Release|Any CPU.Build.0 = Release|Any CPU
310318
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
311319
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
312320
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -563,6 +571,10 @@ Global
563571
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Debug|Any CPU.Build.0 = Debug|Any CPU
564572
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Release|Any CPU.ActiveCfg = Release|Any CPU
565573
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C}.Release|Any CPU.Build.0 = Release|Any CPU
574+
{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
575+
{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Debug|Any CPU.Build.0 = Debug|Any CPU
576+
{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Release|Any CPU.ActiveCfg = Release|Any CPU
577+
{82F79BC0-5E4A-4380-82C5-8B6E3CF05958}.Release|Any CPU.Build.0 = Release|Any CPU
566578
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
567579
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
568580
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -851,6 +863,7 @@ Global
851863
{78E879DE-E97B-4E19-B71B-628ED9FA7F7C} = {AF017206-CE20-4DDF-8301-CAC68CED1BE6}
852864
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
853865
{AB9C1563-07C7-4685-BACD-BB1FF64B3611} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
866+
{9B8A4BDE-1D9C-48A0-A64A-C33FBC701E0A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
854867
{3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
855868
{A9FF9C7F-BBA0-4B44-90B7-48A60F9E00F3} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
856869
{302EC1E0-AE75-4E99-A6BF-524F35338BC8} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -915,6 +928,7 @@ Global
915928
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
916929
{AB93C67F-0A53-4525-AE6C-29B065820ABE} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
917930
{8E1E0A6D-EEBB-4455-B8E8-A55AF9B2062C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
931+
{82F79BC0-5E4A-4380-82C5-8B6E3CF05958} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
918932
{B272FDDE-5E01-425D-B9E1-10FF883DDAAA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
919933
{03E60673-078A-4508-99AD-8537CE6F78F1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
920934
{2E7B92E3-8526-4706-90F3-00F0F5C47C37} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}

docs/modules/aspire-dashboard.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Aspire Dashboard
2+
3+
The [Aspire dashboard](https://learn.microsoft.com/dotnet/aspire/fundamentals/dashboard/overview) is a standalone tool for viewing the logs, traces, and metrics that your apps emit over the OpenTelemetry Protocol (OTLP). It provides a web interface to explore telemetry during local development.
4+
5+
Add the following dependency to your project file:
6+
7+
```shell title="NuGet"
8+
dotnet add package Testcontainers.AspireDashboard
9+
```
10+
11+
You can start an Aspire Dashboard container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method.
12+
13+
=== "Usage Example"
14+
```csharp
15+
--8<-- "tests/Testcontainers.AspireDashboard.Tests/AspireDashboardContainerTest.cs:UseAspireDashboardContainer"
16+
```
17+
18+
The test example exports a trace to the dashboard over OTLP and queries the dashboard's telemetry API to confirm that the span was ingested. Use `GetOtlpGrpcAddress()` or `GetOtlpHttpAddress()` to configure the OpenTelemetry exporter in your .NET service, and `GetDashboardAddress()` to open the dashboard's web interface.
19+
20+
The test example uses the following NuGet dependencies:
21+
22+
=== "Package References"
23+
```xml
24+
--8<-- "tests/Testcontainers.AspireDashboard.Tests/Testcontainers.AspireDashboard.Tests.csproj:PackageReferences"
25+
```
26+
27+
To execute the tests, use the command `dotnet test` from a terminal.
28+
29+
--8<-- "docs/modules/_call_out_test_projects.txt"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ nav:
5656
- modules/activemq.md # Apache
5757
- modules/cassandra.md # Apache
5858
- modules/pulsar.md # Apache
59+
- modules/aspire-dashboard.md
5960
- modules/eventhubs.md # Azure
6061
- modules/servicebus.md # Azure
6162
- modules/clickhouse.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
root = true
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
namespace Testcontainers.AspireDashboard;
2+
3+
/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
4+
[PublicAPI]
5+
public sealed class AspireDashboardBuilder : ContainerBuilder<AspireDashboardBuilder, AspireDashboardContainer, AspireDashboardConfiguration>
6+
{
7+
[Obsolete("This constant is obsolete and will be removed in the future. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
8+
public const string AspireDashboardImage = "mcr.microsoft.com/dotnet/aspire-dashboard:13";
9+
10+
public const ushort AspireDashboardHttpPort = 18888;
11+
12+
public const ushort AspireDashboardOtlpGrpcPort = 18889;
13+
14+
public const ushort AspireDashboardOtlpHttpPort = 18890;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="AspireDashboardBuilder" /> class.
18+
/// </summary>
19+
[Obsolete("This parameterless constructor is obsolete and will be removed. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
20+
[ExcludeFromCodeCoverage]
21+
public AspireDashboardBuilder()
22+
: this(AspireDashboardImage)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="AspireDashboardBuilder" /> class.
28+
/// </summary>
29+
/// <param name="image">
30+
/// The full Docker image name, including the image repository and tag
31+
/// (e.g., <c>mcr.microsoft.com/dotnet/aspire-dashboard:13</c>).
32+
/// </param>
33+
/// <remarks>
34+
/// Docker image tags available at <see href="https://hub.docker.com/r/microsoft/dotnet-aspire-dashboard" />.
35+
/// </remarks>
36+
public AspireDashboardBuilder(string image)
37+
: this(new DockerImage(image))
38+
{
39+
}
40+
41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="AspireDashboardBuilder" /> class.
43+
/// </summary>
44+
/// <param name="image">
45+
/// An <see cref="IImage" /> instance that specifies the Docker image to be used
46+
/// for the container builder configuration.
47+
/// </param>
48+
/// <remarks>
49+
/// Docker image tags available at <see href="https://hub.docker.com/r/microsoft/dotnet-aspire-dashboard" />.
50+
/// </remarks>
51+
public AspireDashboardBuilder(IImage image)
52+
: this(new AspireDashboardConfiguration())
53+
{
54+
DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
55+
}
56+
57+
/// <summary>
58+
/// Initializes a new instance of the <see cref="AspireDashboardBuilder" /> class.
59+
/// </summary>
60+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
61+
private AspireDashboardBuilder(AspireDashboardConfiguration resourceConfiguration)
62+
: base(resourceConfiguration)
63+
{
64+
DockerResourceConfiguration = resourceConfiguration;
65+
}
66+
67+
/// <inheritdoc />
68+
protected override AspireDashboardConfiguration DockerResourceConfiguration { get; }
69+
70+
/// <inheritdoc />
71+
public override AspireDashboardContainer Build()
72+
{
73+
Validate();
74+
return new AspireDashboardContainer(DockerResourceConfiguration);
75+
}
76+
77+
/// <inheritdoc />
78+
protected override AspireDashboardBuilder Init()
79+
{
80+
return base.Init()
81+
.WithImage(AspireDashboardImage)
82+
.WithPortBinding(AspireDashboardHttpPort, true)
83+
.WithPortBinding(AspireDashboardOtlpGrpcPort, true)
84+
.WithPortBinding(AspireDashboardOtlpHttpPort, true)
85+
.WithEnvironment("DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS", "true")
86+
.WithConnectionStringProvider(new AspireDashboardConnectionStringProvider())
87+
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
88+
request.ForPort(AspireDashboardHttpPort)));
89+
}
90+
91+
/// <inheritdoc />
92+
protected override AspireDashboardBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
93+
{
94+
return Merge(DockerResourceConfiguration, new AspireDashboardConfiguration(resourceConfiguration));
95+
}
96+
97+
/// <inheritdoc />
98+
protected override AspireDashboardBuilder Clone(IContainerConfiguration resourceConfiguration)
99+
{
100+
return Merge(DockerResourceConfiguration, new AspireDashboardConfiguration(resourceConfiguration));
101+
}
102+
103+
/// <inheritdoc />
104+
protected override AspireDashboardBuilder Merge(AspireDashboardConfiguration oldValue, AspireDashboardConfiguration newValue)
105+
{
106+
return new AspireDashboardBuilder(new AspireDashboardConfiguration(oldValue, newValue));
107+
}
108+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Testcontainers.AspireDashboard;
2+
3+
/// <inheritdoc cref="ContainerConfiguration" />
4+
[PublicAPI]
5+
public sealed class AspireDashboardConfiguration : ContainerConfiguration
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="AspireDashboardConfiguration" /> class.
9+
/// </summary>
10+
public AspireDashboardConfiguration()
11+
{
12+
}
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="AspireDashboardConfiguration" /> class.
16+
/// </summary>
17+
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
18+
public AspireDashboardConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
19+
: base(resourceConfiguration)
20+
{
21+
}
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="AspireDashboardConfiguration" /> class.
25+
/// </summary>
26+
/// <param name="resourceConfiguration">The container configuration.</param>
27+
public AspireDashboardConfiguration(IContainerConfiguration resourceConfiguration)
28+
: base(resourceConfiguration)
29+
{
30+
}
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="AspireDashboardConfiguration" /> class.
34+
/// </summary>
35+
/// <param name="oldValue">The old Aspire dashboard configuration.</param>
36+
/// <param name="newValue">The new Aspire dashboard configuration.</param>
37+
public AspireDashboardConfiguration(AspireDashboardConfiguration oldValue, AspireDashboardConfiguration newValue)
38+
: base(oldValue, newValue)
39+
{
40+
}
41+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Testcontainers.AspireDashboard;
2+
3+
/// <summary>
4+
/// Provides the Aspire dashboard connection string.
5+
/// </summary>
6+
internal sealed class AspireDashboardConnectionStringProvider : ContainerConnectionStringProvider<AspireDashboardContainer, AspireDashboardConfiguration>
7+
{
8+
/// <inheritdoc />
9+
protected override string GetHostConnectionString()
10+
{
11+
return Container.GetConnectionString();
12+
}
13+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace Testcontainers.AspireDashboard;
2+
3+
/// <inheritdoc cref="DockerContainer" />
4+
[PublicAPI]
5+
public sealed class AspireDashboardContainer : DockerContainer
6+
{
7+
/// <summary>
8+
/// Initializes a new instance of the <see cref="AspireDashboardContainer" /> class.
9+
/// </summary>
10+
/// <param name="configuration">The container configuration.</param>
11+
public AspireDashboardContainer(AspireDashboardConfiguration configuration)
12+
: base(configuration)
13+
{
14+
}
15+
16+
/// <summary>
17+
/// Gets the Aspire dashboard address.
18+
/// </summary>
19+
/// <returns>The Aspire dashboard address.</returns>
20+
public string GetDashboardAddress()
21+
{
22+
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardHttpPort)).ToString();
23+
}
24+
25+
/// <summary>
26+
/// Gets the OTLP gRPC endpoint address.
27+
/// </summary>
28+
/// <returns>The OTLP gRPC endpoint address.</returns>
29+
public string GetOtlpGrpcAddress()
30+
{
31+
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardOtlpGrpcPort)).ToString();
32+
}
33+
34+
/// <summary>
35+
/// Gets the OTLP HTTP endpoint address.
36+
/// </summary>
37+
/// <returns>The OTLP HTTP endpoint address.</returns>
38+
public string GetOtlpHttpAddress()
39+
{
40+
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AspireDashboardBuilder.AspireDashboardOtlpHttpPort)).ToString();
41+
}
42+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
8+
</ItemGroup>
9+
<ItemGroup>
10+
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
11+
</ItemGroup>
12+
</Project>

0 commit comments

Comments
 (0)