From 6038063400cf45134ef0e6bc972884fe93d43423 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:10:05 +0100 Subject: [PATCH 1/2] Create a service scope for each ASP.NET health check, correct AddHealthContributor documentation --- .../Common/HealthChecks/HealthAggregator.cs | 6 ++- .../TestResources/TestHostBuilderFactory.cs | 1 + .../TestWebApplicationBuilderFactory.cs | 2 + .../EndpointServiceCollectionExtensions.cs | 4 +- .../Actuators/Health/HealthAggregationTest.cs | 48 +++++++++++++++++++ .../Steeltoe.Management.Endpoint.Test.csproj | 1 + 6 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/Common/src/Common/HealthChecks/HealthAggregator.cs b/src/Common/src/Common/HealthChecks/HealthAggregator.cs index 92c1e7d88a..972837e11d 100644 --- a/src/Common/src/Common/HealthChecks/HealthAggregator.cs +++ b/src/Common/src/Common/HealthChecks/HealthAggregator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Steeltoe.Common.Extensions; using MicrosoftHealthCheckResult = Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult; @@ -108,7 +109,10 @@ private static async Task RunMicrosoftHealthCheckAsyn try { - IHealthCheck check = registration.Factory(serviceProvider); + // Match the behavior of ASP.NET's HealthCheckService, which creates a scope for each check. + await using AsyncServiceScope serviceScope = serviceProvider.CreateAsyncScope(); + + IHealthCheck check = registration.Factory(serviceScope.ServiceProvider); MicrosoftHealthCheckResult result = await check.CheckHealthAsync(context, cancellationToken); healthCheckResult.Status = ToHealthStatus(result.Status); diff --git a/src/Common/test/TestResources/TestHostBuilderFactory.cs b/src/Common/test/TestResources/TestHostBuilderFactory.cs index 5821d0c7ae..a4ddb1c0aa 100644 --- a/src/Common/test/TestResources/TestHostBuilderFactory.cs +++ b/src/Common/test/TestResources/TestHostBuilderFactory.cs @@ -48,6 +48,7 @@ private static void ConfigureBuilder(HostBuilder builder, bool configureWebHost, { builder.ConfigureWebHostDefaults(webHostBuilder => { + webHostBuilder.UseDefaultServiceProvider(ConfigureServiceProvider); webHostBuilder.Configure(EmptyAction); if (useTestServer) diff --git a/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs b/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs index 5583766952..f86e176836 100644 --- a/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs +++ b/src/Common/test/TestResources/TestWebApplicationBuilderFactory.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; namespace Steeltoe.Common.TestResources; @@ -89,6 +90,7 @@ public static WebApplicationBuilder CreateDefault(bool useTestServer) private static void ConfigureBuilder(WebApplicationBuilder builder, bool useTestServer, bool deactivateDiagnostics) { + builder.Host.UseDefaultServiceProvider(ConfigureServiceProvider); builder.WebHost.UseDefaultServiceProvider(ConfigureServiceProvider); if (useTestServer) diff --git a/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs b/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs index 1226c49cc4..02019af59d 100644 --- a/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs +++ b/src/Management/src/Endpoint/Actuators/Health/EndpointServiceCollectionExtensions.cs @@ -76,7 +76,7 @@ private static void RegisterDefaultHealthContributors(IServiceCollection service } /// - /// Adds the specified to the D/I container as a scoped service. + /// Adds the specified to the D/I container as a singleton service. /// /// /// The type of health contributor to add. @@ -98,7 +98,7 @@ public static IServiceCollection AddHealthContributor(this IServiceCollection } /// - /// Adds the specified to the D/I container as a scoped service. + /// Adds the specified to the D/I container as a singleton service. /// /// /// The to add services to. diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs index 5717db1942..0a05ce7130 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs @@ -7,6 +7,7 @@ using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -516,6 +517,50 @@ public async Task Converts_AspNet_health_check_results() """); } + [Fact] + public async Task Can_use_scoped_AspNet_health_check() + { + WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); + builder.Configuration.AddInMemoryCollection(AppSettings); + builder.Services.AddDbContext(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString())); + builder.Services.AddHealthChecks().AddDbContextCheck(); + builder.Services.AddHealthActuator(); + await using WebApplication host = builder.Build(); + + // ReSharper disable once AccessToDisposedClosure + Action action = () => host.Services.GetRequiredService(); + action.Should().ThrowExactly(); + + host.MapHealthChecks("/health"); + await host.StartAsync(TestContext.Current.CancellationToken); + using HttpClient httpClient = host.GetTestClient(); + + HttpResponseMessage actuatorResponse = await httpClient.GetAsync(new Uri("http://localhost/actuator/health"), TestContext.Current.CancellationToken); + + actuatorResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + string actuatorResponseBody = await actuatorResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + actuatorResponseBody.Should().BeJson(""" + { + "status": "UP", + "components": { + "TestDbContext": { + "status": "UP" + } + } + } + """); + + HttpResponseMessage aspNetResponse = await httpClient.GetAsync(new Uri("http://localhost/health"), TestContext.Current.CancellationToken); + + aspNetResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + string aspnetResponseBody = await aspNetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + + aspnetResponseBody.Should().Be("Healthy"); + } + private sealed class AspNetHealthyCheck : IHealthCheck { public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) @@ -562,4 +607,7 @@ public Task CheckHealthAsync(HealthCheckContext cont throw new InvalidOperationException("test-exception"); } } + + private sealed class TestDbContext(DbContextOptions options) + : DbContext(options); } diff --git a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj index 99a87cce8c..97607ba184 100644 --- a/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj +++ b/src/Management/test/Endpoint.Test/Steeltoe.Management.Endpoint.Test.csproj @@ -16,6 +16,7 @@ + From e1bb3bc5d3687780b8545dadfb00e59eb3d3b9c3 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:11:31 +0100 Subject: [PATCH 2/2] Update src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Endpoint.Test/Actuators/Health/HealthAggregationTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs index 0a05ce7130..216764db61 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthAggregationTest.cs @@ -556,9 +556,9 @@ public async Task Can_use_scoped_AspNet_health_check() aspNetResponse.StatusCode.Should().Be(HttpStatusCode.OK); - string aspnetResponseBody = await aspNetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + string aspNetResponseBody = await aspNetResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); - aspnetResponseBody.Should().Be("Healthy"); + aspNetResponseBody.Should().Be("Healthy"); } private sealed class AspNetHealthyCheck : IHealthCheck