$ // Program.cs — the version that gives you false confidence
app.MapGet("/health", () => "Healthy");
// Program.cs — the version that gives you false confidence
app.MapGet("/health", () => "Healthy");
// Program.cs — the version that gives you false confidence
app.MapGet("/health", () => "Healthy");
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");
public interface IHealthCheck
{ Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);
}
public interface IHealthCheck
{ Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);
}
public interface IHealthCheck
{ Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);
}
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.NpgSql # PostgreSQL
dotnet add package AspNetCore.HealthChecks.MySql # MySQL
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.NpgSql # PostgreSQL
dotnet add package AspNetCore.HealthChecks.MySql # MySQL
dotnet add package AspNetCore.HealthChecks.SqlServer
dotnet add package AspNetCore.HealthChecks.NpgSql # PostgreSQL
dotnet add package AspNetCore.HealthChecks.MySql # MySQL
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!, healthQuery: "SELECT 1", name: "sql-server", failureStatus: HealthStatus.Unhealthy, tags: ["database", "sql"]) .AddNpgSql( connectionString: builder.Configuration.GetConnectionString("Postgres")!, name: "postgresql", tags: ["database", "postgres"]);
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!, healthQuery: "SELECT 1", name: "sql-server", failureStatus: HealthStatus.Unhealthy, tags: ["database", "sql"]) .AddNpgSql( connectionString: builder.Configuration.GetConnectionString("Postgres")!, name: "postgresql", tags: ["database", "postgres"]);
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!, healthQuery: "SELECT 1", name: "sql-server", failureStatus: HealthStatus.Unhealthy, tags: ["database", "sql"]) .AddNpgSql( connectionString: builder.Configuration.GetConnectionString("Postgres")!, name: "postgresql", tags: ["database", "postgres"]);
dotnet add package AspNetCore.HealthChecks.Redis
dotnet add package AspNetCore.HealthChecks.Redis
dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks() .AddRedis( redisConnectionString: builder.Configuration.GetConnectionString("Redis")!, name: "redis", failureStatus: HealthStatus.Degraded, // degraded, not unhealthy — cache miss is survivable tags: ["cache"]);
builder.Services.AddHealthChecks() .AddRedis( redisConnectionString: builder.Configuration.GetConnectionString("Redis")!, name: "redis", failureStatus: HealthStatus.Degraded, // degraded, not unhealthy — cache miss is survivable tags: ["cache"]);
builder.Services.AddHealthChecks() .AddRedis( redisConnectionString: builder.Configuration.GetConnectionString("Redis")!, name: "redis", failureStatus: HealthStatus.Degraded, // degraded, not unhealthy — cache miss is survivable tags: ["cache"]);
dotnet add package AspNetCore.HealthChecks.Uris
dotnet add package AspNetCore.HealthChecks.Uris
dotnet add package AspNetCore.HealthChecks.Uris
builder.Services.AddHealthChecks() .AddUrlGroup( uri: new Uri("https://api.stripe.com/v1/"), name: "stripe-api", failureStatus: HealthStatus.Degraded, tags: ["external"]);
builder.Services.AddHealthChecks() .AddUrlGroup( uri: new Uri("https://api.stripe.com/v1/"), name: "stripe-api", failureStatus: HealthStatus.Degraded, tags: ["external"]);
builder.Services.AddHealthChecks() .AddUrlGroup( uri: new Uri("https://api.stripe.com/v1/"), name: "stripe-api", failureStatus: HealthStatus.Degraded, tags: ["external"]);
public class OrderServiceHealthCheck : IHealthCheck
{ private readonly IOrderRepository _orderRepository; public OrderServiceHealthCheck(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { var canConnect = await _orderRepository.CanConnectAsync(cancellationToken); if (!canConnect) { return HealthCheckResult.Unhealthy("Order repository is unreachable."); } var pendingOrders = await _orderRepository.GetPendingCountAsync(cancellationToken); // Degraded if queue is building up — not broken, but worth knowing if (pendingOrders > 10_000) { return HealthCheckResult.Degraded( $"Order queue is growing: {pendingOrders} pending orders.", data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } return HealthCheckResult.Healthy( data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } catch (Exception ex) { return HealthCheckResult.Unhealthy( "Order -weight: 500;">service check threw an exception.", exception: ex); } }
}
public class OrderServiceHealthCheck : IHealthCheck
{ private readonly IOrderRepository _orderRepository; public OrderServiceHealthCheck(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { var canConnect = await _orderRepository.CanConnectAsync(cancellationToken); if (!canConnect) { return HealthCheckResult.Unhealthy("Order repository is unreachable."); } var pendingOrders = await _orderRepository.GetPendingCountAsync(cancellationToken); // Degraded if queue is building up — not broken, but worth knowing if (pendingOrders > 10_000) { return HealthCheckResult.Degraded( $"Order queue is growing: {pendingOrders} pending orders.", data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } return HealthCheckResult.Healthy( data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } catch (Exception ex) { return HealthCheckResult.Unhealthy( "Order -weight: 500;">service check threw an exception.", exception: ex); } }
}
public class OrderServiceHealthCheck : IHealthCheck
{ private readonly IOrderRepository _orderRepository; public OrderServiceHealthCheck(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { try { var canConnect = await _orderRepository.CanConnectAsync(cancellationToken); if (!canConnect) { return HealthCheckResult.Unhealthy("Order repository is unreachable."); } var pendingOrders = await _orderRepository.GetPendingCountAsync(cancellationToken); // Degraded if queue is building up — not broken, but worth knowing if (pendingOrders > 10_000) { return HealthCheckResult.Degraded( $"Order queue is growing: {pendingOrders} pending orders.", data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } return HealthCheckResult.Healthy( data: new Dictionary<string, object> { ["pending_orders"] = pendingOrders }); } catch (Exception ex) { return HealthCheckResult.Unhealthy( "Order -weight: 500;">service check threw an exception.", exception: ex); } }
}
builder.Services.AddHealthChecks() .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", failureStatus: HealthStatus.Unhealthy, tags: ["orders", "database"]);
builder.Services.AddHealthChecks() .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", failureStatus: HealthStatus.Unhealthy, tags: ["orders", "database"]);
builder.Services.AddHealthChecks() .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", failureStatus: HealthStatus.Unhealthy, tags: ["orders", "database"]);
public class DiskSpaceHealthCheck : IHealthCheck
{ private readonly long _minimumFreeBytesThreshold; public DiskSpaceHealthCheck(long minimumFreeMegabytes = 500) { _minimumFreeBytesThreshold = minimumFreeMegabytes * 1024 * 1024; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var drive = DriveInfo.GetDrives() .FirstOrDefault(d => d.IsReady && d.Name == "/"); if (drive is null) { return Task.FromResult(HealthCheckResult.Unhealthy("Root drive not found.")); } var freeBytes = drive.AvailableFreeSpace; var freeMb = freeBytes / (1024 * 1024); var data = new Dictionary<string, object> { ["free_mb"] = freeMb, ["total_mb"] = drive.TotalSize / (1024 * 1024) }; if (freeBytes < _minimumFreeBytesThreshold) { return Task.FromResult( HealthCheckResult.Unhealthy($"Low disk space: {freeMb} MB free.", data: data)); } if (freeBytes < _minimumFreeBytesThreshold * 2) { return Task.FromResult( HealthCheckResult.Degraded($"Disk space getting low: {freeMb} MB free.", data: data)); } return Task.FromResult(HealthCheckResult.Healthy(data: data)); }
}
public class DiskSpaceHealthCheck : IHealthCheck
{ private readonly long _minimumFreeBytesThreshold; public DiskSpaceHealthCheck(long minimumFreeMegabytes = 500) { _minimumFreeBytesThreshold = minimumFreeMegabytes * 1024 * 1024; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var drive = DriveInfo.GetDrives() .FirstOrDefault(d => d.IsReady && d.Name == "/"); if (drive is null) { return Task.FromResult(HealthCheckResult.Unhealthy("Root drive not found.")); } var freeBytes = drive.AvailableFreeSpace; var freeMb = freeBytes / (1024 * 1024); var data = new Dictionary<string, object> { ["free_mb"] = freeMb, ["total_mb"] = drive.TotalSize / (1024 * 1024) }; if (freeBytes < _minimumFreeBytesThreshold) { return Task.FromResult( HealthCheckResult.Unhealthy($"Low disk space: {freeMb} MB free.", data: data)); } if (freeBytes < _minimumFreeBytesThreshold * 2) { return Task.FromResult( HealthCheckResult.Degraded($"Disk space getting low: {freeMb} MB free.", data: data)); } return Task.FromResult(HealthCheckResult.Healthy(data: data)); }
}
public class DiskSpaceHealthCheck : IHealthCheck
{ private readonly long _minimumFreeBytesThreshold; public DiskSpaceHealthCheck(long minimumFreeMegabytes = 500) { _minimumFreeBytesThreshold = minimumFreeMegabytes * 1024 * 1024; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var drive = DriveInfo.GetDrives() .FirstOrDefault(d => d.IsReady && d.Name == "/"); if (drive is null) { return Task.FromResult(HealthCheckResult.Unhealthy("Root drive not found.")); } var freeBytes = drive.AvailableFreeSpace; var freeMb = freeBytes / (1024 * 1024); var data = new Dictionary<string, object> { ["free_mb"] = freeMb, ["total_mb"] = drive.TotalSize / (1024 * 1024) }; if (freeBytes < _minimumFreeBytesThreshold) { return Task.FromResult( HealthCheckResult.Unhealthy($"Low disk space: {freeMb} MB free.", data: data)); } if (freeBytes < _minimumFreeBytesThreshold * 2) { return Task.FromResult( HealthCheckResult.Degraded($"Disk space getting low: {freeMb} MB free.", data: data)); } return Task.FromResult(HealthCheckResult.Healthy(data: data)); }
}
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: connectionString, name: "sql-server", tags: ["ready"]) // readiness only .AddRedis( redisConnectionString: redisConnection, name: "redis", failureStatus: HealthStatus.Degraded, tags: ["ready"]) // readiness only .AddCheck<DiskSpaceHealthCheck>( name: "disk-space", tags: ["live", "ready"]) // both .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", tags: ["ready"]); // readiness only
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: connectionString, name: "sql-server", tags: ["ready"]) // readiness only .AddRedis( redisConnectionString: redisConnection, name: "redis", failureStatus: HealthStatus.Degraded, tags: ["ready"]) // readiness only .AddCheck<DiskSpaceHealthCheck>( name: "disk-space", tags: ["live", "ready"]) // both .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", tags: ["ready"]); // readiness only
builder.Services.AddHealthChecks() .AddSqlServer( connectionString: connectionString, name: "sql-server", tags: ["ready"]) // readiness only .AddRedis( redisConnectionString: redisConnection, name: "redis", failureStatus: HealthStatus.Degraded, tags: ["ready"]) // readiness only .AddCheck<DiskSpaceHealthCheck>( name: "disk-space", tags: ["live", "ready"]) // both .AddCheck<OrderServiceHealthCheck>( name: "order--weight: 500;">service", tags: ["ready"]); // readiness only
// Liveness — only checks the process is alive and has disk space
app.MapHealthChecks("/health/live", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("live"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded still alive [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
}); // Readiness — checks everything needed to serve requests
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("ready"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded can still serve [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
});
// Liveness — only checks the process is alive and has disk space
app.MapHealthChecks("/health/live", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("live"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded still alive [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
}); // Readiness — checks everything needed to serve requests
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("ready"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded can still serve [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
});
// Liveness — only checks the process is alive and has disk space
app.MapHealthChecks("/health/live", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("live"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded still alive [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
}); // Readiness — checks everything needed to serve requests
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{ Predicate = check => check.Tags.Contains("ready"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, // degraded can still serve [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable }
});
livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 15 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 startupProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 30 # 30 * 5s = 150 seconds to -weight: 500;">start up
livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 15 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 startupProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 30 # 30 * 5s = 150 seconds to -weight: 500;">start up
livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 10 periodSeconds: 15 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 startupProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 30 # 30 * 5s = 150 seconds to -weight: 500;">start up
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}); static Task WriteHealthCheckResponse(HttpContext context, HealthReport report)
{ context.Response.ContentType = "application/json"; var response = new { -weight: 500;">status = report.Status.ToString(), duration = report.TotalDuration.TotalMilliseconds, checks = report.Entries.Select(e => new { name = e.Key, -weight: 500;">status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds, data = e.Value.Data, exception = e.Value.Exception?.Message }) }; return context.Response.WriteAsync( JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }));
}
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}); static Task WriteHealthCheckResponse(HttpContext context, HealthReport report)
{ context.Response.ContentType = "application/json"; var response = new { -weight: 500;">status = report.Status.ToString(), duration = report.TotalDuration.TotalMilliseconds, checks = report.Entries.Select(e => new { name = e.Key, -weight: 500;">status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds, data = e.Value.Data, exception = e.Value.Exception?.Message }) }; return context.Response.WriteAsync( JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }));
}
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}); static Task WriteHealthCheckResponse(HttpContext context, HealthReport report)
{ context.Response.ContentType = "application/json"; var response = new { -weight: 500;">status = report.Status.ToString(), duration = report.TotalDuration.TotalMilliseconds, checks = report.Entries.Select(e => new { name = e.Key, -weight: 500;">status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds, data = e.Value.Data, exception = e.Value.Exception?.Message }) }; return context.Response.WriteAsync( JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }));
}
{ "-weight: 500;">status": "Degraded", "duration": 142.5, "checks": [ { "name": "sql-server", "-weight: 500;">status": "Healthy", "duration": 12.3, "data": {} }, { "name": "redis", "-weight: 500;">status": "Degraded", "description": "Redis connection timeout after 5000ms", "duration": 5001.2, "data": {} }, { "name": "disk-space", "-weight: 500;">status": "Healthy", "duration": 0.8, "data": { "free_mb": 42301, "total_mb": 102400 } } ]
}
{ "-weight: 500;">status": "Degraded", "duration": 142.5, "checks": [ { "name": "sql-server", "-weight: 500;">status": "Healthy", "duration": 12.3, "data": {} }, { "name": "redis", "-weight: 500;">status": "Degraded", "description": "Redis connection timeout after 5000ms", "duration": 5001.2, "data": {} }, { "name": "disk-space", "-weight: 500;">status": "Healthy", "duration": 0.8, "data": { "free_mb": 42301, "total_mb": 102400 } } ]
}
{ "-weight: 500;">status": "Degraded", "duration": 142.5, "checks": [ { "name": "sql-server", "-weight: 500;">status": "Healthy", "duration": 12.3, "data": {} }, { "name": "redis", "-weight: 500;">status": "Degraded", "description": "Redis connection timeout after 5000ms", "duration": 5001.2, "data": {} }, { "name": "disk-space", "-weight: 500;">status": "Healthy", "duration": 0.8, "data": { "free_mb": 42301, "total_mb": 102400 } } ]
}
public class ExternalPaymentApiHealthCheck : IHealthCheck
{ private readonly HttpClient _httpClient; public ExternalPaymentApiHealthCheck(IHttpClientFactory factory) { _httpClient = factory.CreateClient("payment-api"); } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(3)); // never wait more than 3 seconds try { var response = await _httpClient.GetAsync("/ping", cts.Token); return response.IsSuccessStatusCode ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded($"Payment API returned {(int)response.StatusCode}"); } catch (OperationCanceledException) { return HealthCheckResult.Degraded("Payment API timed out after 3 seconds."); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Payment API unreachable.", ex); } }
}
public class ExternalPaymentApiHealthCheck : IHealthCheck
{ private readonly HttpClient _httpClient; public ExternalPaymentApiHealthCheck(IHttpClientFactory factory) { _httpClient = factory.CreateClient("payment-api"); } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(3)); // never wait more than 3 seconds try { var response = await _httpClient.GetAsync("/ping", cts.Token); return response.IsSuccessStatusCode ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded($"Payment API returned {(int)response.StatusCode}"); } catch (OperationCanceledException) { return HealthCheckResult.Degraded("Payment API timed out after 3 seconds."); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Payment API unreachable.", ex); } }
}
public class ExternalPaymentApiHealthCheck : IHealthCheck
{ private readonly HttpClient _httpClient; public ExternalPaymentApiHealthCheck(IHttpClientFactory factory) { _httpClient = factory.CreateClient("payment-api"); } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(3)); // never wait more than 3 seconds try { var response = await _httpClient.GetAsync("/ping", cts.Token); return response.IsSuccessStatusCode ? HealthCheckResult.Healthy() : HealthCheckResult.Degraded($"Payment API returned {(int)response.StatusCode}"); } catch (OperationCanceledException) { return HealthCheckResult.Degraded("Payment API timed out after 3 seconds."); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Payment API unreachable.", ex); } }
}
// Public endpoint — minimal response
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = (ctx, report) => { ctx.Response.ContentType = "text/plain"; return ctx.Response.WriteAsync(report.Status.ToString()); }
}); // Internal endpoint — full details, restricted by host
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).RequireHost("*.internal.mycompany.com");
// Public endpoint — minimal response
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = (ctx, report) => { ctx.Response.ContentType = "text/plain"; return ctx.Response.WriteAsync(report.Status.ToString()); }
}); // Internal endpoint — full details, restricted by host
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).RequireHost("*.internal.mycompany.com");
// Public endpoint — minimal response
app.MapHealthChecks("/health", new HealthCheckOptions
{ ResponseWriter = (ctx, report) => { ctx.Response.ContentType = "text/plain"; return ctx.Response.WriteAsync(report.Status.ToString()); }
}); // Internal endpoint — full details, restricted by host
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).RequireHost("*.internal.mycompany.com");
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).AddEndpointFilter(async (context, next) =>
{ var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); var expectedKey = config["HealthChecks:ApiKey"]; var providedKey = context.HttpContext.Request.Headers["X-Health-Key"].FirstOrDefault(); if (string.IsNullOrEmpty(expectedKey) || providedKey != expectedKey) { context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Results.Unauthorized(); } return await next(context);
});
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).AddEndpointFilter(async (context, next) =>
{ var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); var expectedKey = config["HealthChecks:ApiKey"]; var providedKey = context.HttpContext.Request.Headers["X-Health-Key"].FirstOrDefault(); if (string.IsNullOrEmpty(expectedKey) || providedKey != expectedKey) { context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Results.Unauthorized(); } return await next(context);
});
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{ ResponseWriter = WriteHealthCheckResponse
}).AddEndpointFilter(async (context, next) =>
{ var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>(); var expectedKey = config["HealthChecks:ApiKey"]; var providedKey = context.HttpContext.Request.Headers["X-Health-Key"].FirstOrDefault(); if (string.IsNullOrEmpty(expectedKey) || providedKey != expectedKey) { context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Results.Unauthorized(); } return await next(context);
});
public class CachedDatabaseHealthCheck : IHealthCheck
{ private readonly IDbConnectionFactory _factory; private HealthCheckResult _cachedResult = HealthCheckResult.Healthy(); private DateTime _lastCheck = DateTime.MinValue; private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); public CachedDatabaseHealthCheck(IDbConnectionFactory factory) { _factory = factory; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (DateTime.UtcNow - _lastCheck < CacheDuration) return _cachedResult; try { await using var conn = await _factory.CreateConnectionAsync(cancellationToken); await conn.ExecuteAsync("SELECT 1"); _cachedResult = HealthCheckResult.Healthy(); } catch (Exception ex) { _cachedResult = HealthCheckResult.Unhealthy("Database check failed.", ex); } _lastCheck = DateTime.UtcNow; return _cachedResult; }
}
public class CachedDatabaseHealthCheck : IHealthCheck
{ private readonly IDbConnectionFactory _factory; private HealthCheckResult _cachedResult = HealthCheckResult.Healthy(); private DateTime _lastCheck = DateTime.MinValue; private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); public CachedDatabaseHealthCheck(IDbConnectionFactory factory) { _factory = factory; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (DateTime.UtcNow - _lastCheck < CacheDuration) return _cachedResult; try { await using var conn = await _factory.CreateConnectionAsync(cancellationToken); await conn.ExecuteAsync("SELECT 1"); _cachedResult = HealthCheckResult.Healthy(); } catch (Exception ex) { _cachedResult = HealthCheckResult.Unhealthy("Database check failed.", ex); } _lastCheck = DateTime.UtcNow; return _cachedResult; }
}
public class CachedDatabaseHealthCheck : IHealthCheck
{ private readonly IDbConnectionFactory _factory; private HealthCheckResult _cachedResult = HealthCheckResult.Healthy(); private DateTime _lastCheck = DateTime.MinValue; private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); public CachedDatabaseHealthCheck(IDbConnectionFactory factory) { _factory = factory; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (DateTime.UtcNow - _lastCheck < CacheDuration) return _cachedResult; try { await using var conn = await _factory.CreateConnectionAsync(cancellationToken); await conn.ExecuteAsync("SELECT 1"); _cachedResult = HealthCheckResult.Healthy(); } catch (Exception ex) { _cachedResult = HealthCheckResult.Unhealthy("Database check failed.", ex); } _lastCheck = DateTime.UtcNow; return _cachedResult; }
}
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
builder.Services.AddHealthChecksUI(options =>
{ options.SetEvaluationTimeInSeconds(30); // poll every 30 seconds options.AddHealthCheckEndpoint("API", "/health/detail");
}).AddInMemoryStorage(); // After app.MapHealthChecks(...)
app.MapHealthChecksUI(options => options.UIPath = "/health-ui");
builder.Services.AddHealthChecksUI(options =>
{ options.SetEvaluationTimeInSeconds(30); // poll every 30 seconds options.AddHealthCheckEndpoint("API", "/health/detail");
}).AddInMemoryStorage(); // After app.MapHealthChecks(...)
app.MapHealthChecksUI(options => options.UIPath = "/health-ui");
builder.Services.AddHealthChecksUI(options =>
{ options.SetEvaluationTimeInSeconds(30); // poll every 30 seconds options.AddHealthCheckEndpoint("API", "/health/detail");
}).AddInMemoryStorage(); // After app.MapHealthChecks(...)
app.MapHealthChecksUI(options => options.UIPath = "/health-ui"); - HealthCheckResult.Healthy() — everything is fine
- HealthCheckResult.Degraded() — working but not at full capacity
- HealthCheckResult.Unhealthy() — something is broken - One liveness endpoint — is the process worth keeping alive?
- One readiness endpoint — is it ready to serve traffic right now?
- Custom checks for every dependency that has caused an incident before
- Rich JSON so you know why, not just that
- And if you want bonus points with your team, the UI takes five minutes