| File: AppHost.cs | Web Access |
| Project: src\playground\HealthChecks\HealthChecksSandbox.AppHost\HealthChecksSandbox.AppHost.csproj (HealthChecksSandbox.AppHost) |
// 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.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; var builder = DistributedApplication.CreateBuilder(args); builder.Services.TryAddEventingSubscriber<TestResourceLifecycle>(); AddTestResource("healthy", HealthStatus.Healthy, "I'm fine, thanks for asking."); AddTestResource("unhealthy", HealthStatus.Unhealthy, "I can't do that, Dave.", exceptionMessage: "Feeling unhealthy."); AddTestResource("degraded", HealthStatus.Degraded, "Had better days.", exceptionMessage: "Feeling degraded."); // ----------------------------------------------------------------------- // External services with HTTP health checks to test friendly error messages // (PR #14072: Add friendly error messages to health check failures) // // Friendly message cases from HttpHealthCheckHelpers.GetFriendlyErrorMessage: // 1. Connection refused → "Failed to connect to {uri}." // 2. Timeout → "Request to {uri} timed out." // 3. Wrong status code → "Request to {uri} returned {code} {name}." // 4. Canceled → "Health check for {uri} was canceled." // 5. Invalid URL param → resource goes to FailedToStart // 6. Deferred URI (null) → "The URI for the health check is not set..." // 7. Generic failure → "Health check failed for {uri}." // ----------------------------------------------------------------------- // Case 1 (StaticUriHealthCheck): Connection refused — nothing listening // → "Failed to connect to http://localhost:19999/." builder.AddExternalService("http-connection-refused", "http://localhost:19999/") .WithHttpHealthCheck(); // Case 2 (StaticUriHealthCheck): Timeout — non-routable TEST-NET-1 address // → "Request to http://192.0.2.1/ timed out." builder.AddExternalService("http-timeout", "http://192.0.2.1/") .WithHttpHealthCheck(); // Case 3 (StaticUriHealthCheck): Wrong status code — expects 200 on a port with nothing listening // → "Failed to connect to http://localhost:19998/." (or status code message if a server runs there) // To test the actual status-code-mismatch message, start a server on 19998 that returns non-200: // python3 -m http.server 19998 // Then you'll see → "Request to http://localhost:19998/ returned 404 NotFound." builder.AddExternalService("http-wrong-status", "http://localhost:19998/") .WithHttpHealthCheck(statusCode: 200); // Case 4 (ParameterUriHealthCheck): Connection refused — parameterized URL // → "Failed to connect to http://localhost:19997/." builder.Configuration["Parameters:unhealthy-url"] = "http://localhost:19997/"; var unhealthyUrlParam = builder.AddParameter("unhealthy-url"); builder.AddExternalService("http-param-refused", unhealthyUrlParam) .WithHttpHealthCheck(); // Case 5 (ParameterUriHealthCheck): Invalid URL → FailedToStart // → resource immediately enters FailedToStart state (URL validation fails before health check) builder.Configuration["Parameters:invalid-url"] = "not-a-valid-url"; var invalidUrlParam = builder.AddParameter("invalid-url"); builder.AddExternalService("http-param-invalid-url", invalidUrlParam) .WithHttpHealthCheck(); // Case 6 (DeferredUriHealthCheck): Resource with endpoint, nothing listening // → "Failed to connect to http://localhost:19996/." once URI resolves // This exercises the generic WithHttpHealthCheck<T> for resources with endpoints. builder.AddResource(new TestResource("http-deferred")) .WithHttpEndpoint(port: 19996, name: "http") .WithHttpHealthCheck(endpointName: "http") .WithInitialState(new() { ResourceType = "Test Resource", State = "Starting", Properties = [], }) .ExcludeFromManifest(); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code // or build with `/p:SkipDashboardReference=true`, to test end developer // dashboard launch experience, Refer to Directory.Build.props for the path to // the dashboard binary (defaults to the Aspire.Dashboard bin output in the // artifacts dir). builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard); #endif builder.Build().Run(); void AddTestResource(string name, HealthStatus status, string? description = null, string? exceptionMessage = null) { var hasHealthyAfterFirstRunCheckRun = false; builder.Services.AddHealthChecks() .AddCheck( $"{name}_check", () => new HealthCheckResult(status, description, new InvalidOperationException(exceptionMessage)) ) .AddCheck($"{name}_resource_healthy_after_first_run_check", () => { if (!hasHealthyAfterFirstRunCheckRun) { hasHealthyAfterFirstRunCheckRun = true; return new HealthCheckResult(HealthStatus.Unhealthy, "Initial failure state."); } return new HealthCheckResult(HealthStatus.Healthy, "Healthy beginning second health check run."); }); builder .AddResource(new TestResource(name)) .WithHealthCheck($"{name}_check") .WithHealthCheck($"{name}_resource_healthy_after_first_run_check") .WithInitialState(new() { ResourceType = "Test Resource", State = "Starting", Properties = [], }) .ExcludeFromManifest(); return; } internal sealed class TestResource(string name) : Resource(name), IResourceWithEndpoints; internal sealed class TestResourceLifecycle(ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber { public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken) { foreach (var resource in @event.Model.Resources.OfType<TestResource>()) { Task.Run( async () => { await Task.Delay(TimeSpan.FromSeconds(10)); await notificationService.PublishUpdateAsync( resource, state => state with { State = new("Running", "success") }); }, cancellationToken); } return Task.CompletedTask; } public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) { eventing.Subscribe<BeforeStartEvent>(OnBeforeStartAsync); return Task.CompletedTask; } }