diff --git a/src/Components/Aspire.Azure.Data.Tables/Aspire.Azure.Data.Tables.csproj b/src/Components/Aspire.Azure.Data.Tables/Aspire.Azure.Data.Tables.csproj
index dcfac7f332..620fa1658c 100644
--- a/src/Components/Aspire.Azure.Data.Tables/Aspire.Azure.Data.Tables.csproj
+++ b/src/Components/Aspire.Azure.Data.Tables/Aspire.Azure.Data.Tables.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/Components/Aspire.Azure.Messaging.ServiceBus/Aspire.Azure.Messaging.ServiceBus.csproj b/src/Components/Aspire.Azure.Messaging.ServiceBus/Aspire.Azure.Messaging.ServiceBus.csproj
index 6038621b58..b3a5e175b3 100644
--- a/src/Components/Aspire.Azure.Messaging.ServiceBus/Aspire.Azure.Messaging.ServiceBus.csproj
+++ b/src/Components/Aspire.Azure.Messaging.ServiceBus/Aspire.Azure.Messaging.ServiceBus.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj
index c93ea4b011..a5e4d0236d 100644
--- a/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj
+++ b/src/Components/Aspire.Azure.Security.KeyVault/Aspire.Azure.Security.KeyVault.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/Components/Aspire.Azure.Storage.Blobs/Aspire.Azure.Storage.Blobs.csproj b/src/Components/Aspire.Azure.Storage.Blobs/Aspire.Azure.Storage.Blobs.csproj
index 6a5bd83ee0..8c08bf0828 100644
--- a/src/Components/Aspire.Azure.Storage.Blobs/Aspire.Azure.Storage.Blobs.csproj
+++ b/src/Components/Aspire.Azure.Storage.Blobs/Aspire.Azure.Storage.Blobs.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj b/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj
index 67dd112f02..a3aec66cbd 100644
--- a/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj
+++ b/src/Components/Aspire.Azure.Storage.Queues/Aspire.Azure.Storage.Queues.csproj
@@ -13,6 +13,7 @@
+
diff --git a/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj b/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj
index d2b9334132..f4e428e5c1 100644
--- a/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj
+++ b/src/Components/Aspire.Microsoft.Data.SqlClient/Aspire.Microsoft.Data.SqlClient.csproj
@@ -8,6 +8,10 @@
$(SharedDir)SQL_256x.png
+
+
+
+
diff --git a/src/Components/Aspire.Microsoft.Data.SqlClient/AspireSqlServerSqlClientExtensions.cs b/src/Components/Aspire.Microsoft.Data.SqlClient/AspireSqlServerSqlClientExtensions.cs
index 6747d0bf66..2078db120a 100644
--- a/src/Components/Aspire.Microsoft.Data.SqlClient/AspireSqlServerSqlClientExtensions.cs
+++ b/src/Components/Aspire.Microsoft.Data.SqlClient/AspireSqlServerSqlClientExtensions.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Aspire;
using Aspire.Microsoft.Data.SqlClient;
using HealthChecks.SqlServer;
using Microsoft.Data.SqlClient;
@@ -106,16 +107,15 @@ string GetConnectionString()
if (settings.HealthChecks)
{
- builder.Services.AddHealthChecks()
- .Add(new HealthCheckRegistration(
- serviceKey is null ? "SqlServer" : $"SqlServer_{connectionName}",
- sp => new SqlServerHealthCheck(new SqlServerHealthCheckOptions()
- {
- ConnectionString = settings.ConnectionString ?? string.Empty
- }),
- failureStatus: default,
- tags: default,
- timeout: default));
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? "SqlServer" : $"SqlServer_{connectionName}",
+ sp => new SqlServerHealthCheck(new SqlServerHealthCheckOptions()
+ {
+ ConnectionString = settings.ConnectionString ?? string.Empty
+ }),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
}
}
}
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/Aspire.Microsoft.EntityFrameworkCore.SqlServer.csproj b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/Aspire.Microsoft.EntityFrameworkCore.SqlServer.csproj
index 6a824eb86c..cdd0312c57 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/Aspire.Microsoft.EntityFrameworkCore.SqlServer.csproj
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/Aspire.Microsoft.EntityFrameworkCore.SqlServer.csproj
@@ -8,6 +8,10 @@
$(SharedDir)SQL_256x.png
+
+
+
+
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs
index 1300e8f7c1..13a79ba45b 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.SqlServer/AspireSqlServerEFCoreSqlClientExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using Aspire;
using Aspire.Microsoft.EntityFrameworkCore.SqlServer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -90,7 +91,9 @@ public static class AspireSqlServerEFCoreSqlClientExtensions
if (settings.HealthChecks)
{
- builder.Services.AddHealthChecks().AddDbContextCheck();
+ builder.TryAddHealthCheck(
+ name: typeof(TContext).Name,
+ static hcBuilder => hcBuilder.AddDbContextCheck());
}
void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder)
diff --git a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.csproj b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.csproj
index 47f0494300..60c2d899d1 100644
--- a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.csproj
+++ b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs
index 65008bf65d..2cdede5872 100644
--- a/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs
+++ b/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using Aspire;
using Aspire.Npgsql.EntityFrameworkCore.PostgreSQL;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -95,7 +96,9 @@ public static partial class AspireEFPostgreSqlExtensions
if (settings.HealthChecks)
{
// calling MapHealthChecks is the responsibility of the app, not Component
- builder.Services.AddHealthChecks().AddDbContextCheck();
+ builder.TryAddHealthCheck(
+ name: typeof(TContext).Name,
+ static hcBuilder => hcBuilder.AddDbContextCheck());
}
if (settings.Tracing)
diff --git a/src/Components/Aspire.Npgsql/Aspire.Npgsql.csproj b/src/Components/Aspire.Npgsql/Aspire.Npgsql.csproj
index 9bdb3682db..09e5b19a39 100644
--- a/src/Components/Aspire.Npgsql/Aspire.Npgsql.csproj
+++ b/src/Components/Aspire.Npgsql/Aspire.Npgsql.csproj
@@ -8,6 +8,10 @@
$(SharedDir)PostgreSQL_logo.3colors.540x557.png
+
+
+
+
diff --git a/src/Components/Aspire.Npgsql/AspirePostgreSqlNpgsqlExtensions.cs b/src/Components/Aspire.Npgsql/AspirePostgreSqlNpgsqlExtensions.cs
index 666cb5e84f..ad46bd49df 100644
--- a/src/Components/Aspire.Npgsql/AspirePostgreSqlNpgsqlExtensions.cs
+++ b/src/Components/Aspire.Npgsql/AspirePostgreSqlNpgsqlExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Data.Common;
+using Aspire;
using Aspire.Npgsql;
using HealthChecks.NpgSql;
using Microsoft.Extensions.Configuration;
@@ -71,18 +72,17 @@ private static void AddNpgsqlDataSource(IHostApplicationBuilder builder, string
// https://www.npgsql.org/doc/connection-string-parameters.html#pooling
if (settings.HealthChecks)
{
- builder.Services.AddHealthChecks()
- .Add(new HealthCheckRegistration(
- serviceKey is null ? "PostgreSql" : $"PostgreSql_{connectionName}",
- sp => new NpgSqlHealthCheck(new NpgSqlHealthCheckOptions()
- {
- DataSource = serviceKey is null
- ? sp.GetRequiredService()
- : sp.GetRequiredKeyedService(serviceKey)
- }),
- failureStatus: default,
- tags: default,
- timeout: default));
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? "PostgreSql" : $"PostgreSql_{connectionName}",
+ sp => new NpgSqlHealthCheck(new NpgSqlHealthCheckOptions()
+ {
+ DataSource = serviceKey is null
+ ? sp.GetRequiredService()
+ : sp.GetRequiredKeyedService(serviceKey)
+ }),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
}
if (settings.Tracing)
diff --git a/src/Components/Aspire.RabbitMQ.Client/Aspire.RabbitMQ.Client.csproj b/src/Components/Aspire.RabbitMQ.Client/Aspire.RabbitMQ.Client.csproj
index 61c3ee1f21..eda37b282c 100644
--- a/src/Components/Aspire.RabbitMQ.Client/Aspire.RabbitMQ.Client.csproj
+++ b/src/Components/Aspire.RabbitMQ.Client/Aspire.RabbitMQ.Client.csproj
@@ -8,6 +8,10 @@
$(NoWarn);SYSLIB1100;SYSLIB1101
+
+
+
+
diff --git a/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs b/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
index 8dee478c60..ec228fe460 100644
--- a/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
+++ b/src/Components/Aspire.RabbitMQ.Client/AspireRabbitMQExtensions.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Net.Sockets;
+using Aspire;
using Aspire.RabbitMQ.Client;
using HealthChecks.RabbitMQ;
using Microsoft.Extensions.Configuration;
@@ -119,9 +120,7 @@ IConnectionFactory CreateConnectionFactory(IServiceProvider sp)
if (settings.HealthChecks)
{
- var hcBuilder = builder.Services.AddHealthChecks();
-
- hcBuilder.Add(new HealthCheckRegistration(
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
serviceKey is null ? "RabbitMQ.Client" : $"RabbitMQ.Client_{connectionName}",
sp =>
{
diff --git a/src/Components/Aspire.StackExchange.Redis/Aspire.StackExchange.Redis.csproj b/src/Components/Aspire.StackExchange.Redis/Aspire.StackExchange.Redis.csproj
index 14528857cb..ec1648cdac 100644
--- a/src/Components/Aspire.StackExchange.Redis/Aspire.StackExchange.Redis.csproj
+++ b/src/Components/Aspire.StackExchange.Redis/Aspire.StackExchange.Redis.csproj
@@ -9,6 +9,10 @@
$(NoWarn);SYSLIB1100;SYSLIB1101
+
+
+
+
diff --git a/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs b/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
index 22e0c91ab6..40f0422779 100644
--- a/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
+++ b/src/Components/Aspire.StackExchange.Redis/AspireRedisExtensions.cs
@@ -4,6 +4,7 @@
global using System.Net.Security; // needed to work around https://github.com/dotnet/runtime/issues/94065
using System.Text;
+using Aspire;
using Aspire.StackExchange.Redis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -108,13 +109,16 @@ private static void AddRedis(IHostApplicationBuilder builder, string configurati
if (settings.HealthChecks)
{
- builder.Services.AddHealthChecks()
- .AddRedis(
+ var healthCheckName = serviceKey is null ? "StackExchange.Redis" : $"StackExchange.Redis_{connectionName}";
+
+ builder.TryAddHealthCheck(
+ healthCheckName,
+ hcBuilder => hcBuilder.AddRedis(
// The connection factory tries to open the connection and throws when it fails.
// That is why we don't invoke it here, but capture the state (in a closure)
// and let the health check invoke it and handle the exception (if any).
connectionMultiplexerFactory: sp => serviceKey is null ? sp.GetRequiredService() : sp.GetRequiredKeyedService(serviceKey),
- name: serviceKey is null ? "StackExchange.Redis" : $"StackExchange.Redis_{connectionName}");
+ healthCheckName));
}
static TextWriter? CreateLogger(IServiceProvider serviceProvider)
diff --git a/src/Components/Common/AzureComponent.cs b/src/Components/Common/AzureComponent.cs
index 5e4434e864..b37b824c1f 100644
--- a/src/Components/Common/AzureComponent.cs
+++ b/src/Components/Common/AzureComponent.cs
@@ -103,23 +103,22 @@ internal void AddClient(
{
string namePrefix = $"Azure_{typeof(TClient).Name}";
- builder.Services.AddHealthChecks()
- .Add(new HealthCheckRegistration(
- serviceKey is null ? namePrefix : $"{namePrefix}_{serviceKey}",
- serviceProvider =>
- {
- // From https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/:
- // "The main rule of Azure SDK client lifetime management is: treat clients as singletons".
- // So it's fine to root the client via the health check.
- TClient client = serviceKey is null
- ? serviceProvider.GetRequiredService()
- : serviceProvider.GetRequiredKeyedService(serviceKey);
-
- return CreateHealthCheck(client, settings);
- },
- failureStatus: default,
- tags: default,
- timeout: default));
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? namePrefix : $"{namePrefix}_{serviceKey}",
+ serviceProvider =>
+ {
+ // From https://devblogs.microsoft.com/azure-sdk/lifetime-management-and-thread-safety-guarantees-of-azure-sdk-net-clients/:
+ // "The main rule of Azure SDK client lifetime management is: treat clients as singletons".
+ // So it's fine to root the client via the health check.
+ TClient client = serviceKey is null
+ ? serviceProvider.GetRequiredService()
+ : serviceProvider.GetRequiredKeyedService(serviceKey);
+
+ return CreateHealthCheck(client, settings);
+ },
+ failureStatus: default,
+ tags: default,
+ timeout: default));
}
if (GetTracingEnabled(settings))
diff --git a/src/Components/Common/HealthChecksExtensions.cs b/src/Components/Common/HealthChecksExtensions.cs
new file mode 100644
index 0000000000..29e4762df2
--- /dev/null
+++ b/src/Components/Common/HealthChecksExtensions.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace Aspire;
+
+internal static class HealthChecksExtensions
+{
+ ///
+ /// Adds a HealthCheckRegistration if one hasn't already been added to the builder.
+ ///
+ public static void TryAddHealthCheck(this IHostApplicationBuilder builder, HealthCheckRegistration healthCheckRegistration)
+ {
+ builder.TryAddHealthCheck(healthCheckRegistration.Name, hcBuilder => hcBuilder.Add(healthCheckRegistration));
+ }
+
+ ///
+ /// Invokes the action if the given hasn't already been added to the builder.
+ ///
+ public static void TryAddHealthCheck(this IHostApplicationBuilder builder, string name, Action addHealthCheck)
+ {
+ var healthCheckKey = $"Aspire.HealthChecks.{name}";
+ if (!builder.Properties.ContainsKey(healthCheckKey))
+ {
+ builder.Properties[healthCheckKey] = true;
+ addHealthCheck(builder.Services.AddHealthChecks());
+ }
+ }
+}
diff --git a/tests/Aspire.StackExchange.Redis.Tests/Aspire.StackExchange.Redis.Tests.csproj b/tests/Aspire.StackExchange.Redis.Tests/Aspire.StackExchange.Redis.Tests.csproj
index 0824e7a743..4e613f3747 100644
--- a/tests/Aspire.StackExchange.Redis.Tests/Aspire.StackExchange.Redis.Tests.csproj
+++ b/tests/Aspire.StackExchange.Redis.Tests/Aspire.StackExchange.Redis.Tests.csproj
@@ -6,6 +6,9 @@
+
+
+
diff --git a/tests/Aspire.StackExchange.Redis.Tests/AspireRedisExtensionsTests.cs b/tests/Aspire.StackExchange.Redis.Tests/AspireRedisExtensionsTests.cs
index 0191855dcc..406e583439 100644
--- a/tests/Aspire.StackExchange.Redis.Tests/AspireRedisExtensionsTests.cs
+++ b/tests/Aspire.StackExchange.Redis.Tests/AspireRedisExtensionsTests.cs
@@ -1,8 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.AspNetCore.OutputCaching;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
@@ -181,4 +185,42 @@ public void AbortOnConnectFailDefaults(bool useKeyed, IEnumerable
+ /// Verifies that both distributed and output caching components can be added to the same builder and their HealthChecks don't conflict.
+ /// See https://github.com/dotnet/aspire/issues/705
+ ///
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void MultipleRedisComponentsCanBeAdded(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedRedisDistributedCache("redis");
+ builder.AddKeyedRedisOutputCache("redis");
+ }
+ else
+ {
+ builder.AddRedisDistributedCache("redis");
+ builder.AddRedisOutputCache("redis");
+ }
+
+ var host = builder.Build();
+
+ // Note that IDistributedCache and OutputCacheStore don't support keyed services - so only the Redis ConnectionMultiplexer is keyed.
+
+ var distributedCache = host.Services.GetRequiredService();
+ Assert.IsAssignableFrom(distributedCache);
+
+ var cacheStore = host.Services.GetRequiredService();
+ Assert.StartsWith("Redis", cacheStore.GetType().Name);
+
+ // Explicitly ensure the HealthCheckService can be retrieved. It validates the registrations in its constructor.
+ // See https://github.com/dotnet/aspnetcore/blob/94ad7031db6744409de24f75777a59620cb94d9a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs#L33-L36
+ var healthCheckService = host.Services.GetRequiredService();
+ Assert.NotNull(healthCheckService);
+ }
}