Round #282
08 May 2026, 12:03· csharp· openai/gpt-oss-120b:free· r13-clean-hints
- Complexity
- band 1
- ZP / UO / OBS
- 3 / 9 / 0
- Journey weight
- 907
- Lightshift ΔL
- +0.00
Prompt
Add a health check endpoint
Zero-point index (what the engine surfaced)
ZPI-ZPI-0001 | 2026-05-08T12:03:03Z
Add a health check endpoint
ZERO POINT — 3 entries (structural invariants)
● hsl(190.26, 100%, 15%) IHealthCheck [contract]
● hsl(302.56, 100%, 15%) HealthCheckResult [contract]
● hsl(190.42, 100%, 15%) IHealthCheckPublisher [contract]
UNOBSERVED — 9 entries (implied truths)
○ hsl(27.69, 100%, 50%) async [feature]
○ hsl(9.23, 100%, 50%) interface [feature]
○ hsl(77.54, 100%, 36%) Routing middleware [function]
○ hsl(83.08, 100%, 42%) ILogger of T injection [function]
○ hsl(31.38, 100%, 42%) Scoped registration [function]
○ hsl(173.54, 100%, 7%) Liveness check [function]
○ hsl(175.38, 100%, 7%) Readiness check [function]
○ hsl(179.08, 100%, 7%) Custom health check [function]
○ hsl(184.62, 100%, 7%) Health check response writer [function]
OBSERVED — 0 entries (explicit in request)
COMPLEXITY │ 12 entries │ spread: 175° │ band: MEDIUM
Baseline response (no ZPI)
Below is a **complete, step‑by‑step guide** for adding a **Health Check endpoint** to an ASP.NET Core Web API (or MVC) project using the built‑in **Microsoft.Extensions.Diagnostics.HealthChecks** library.
---
## 1️⃣ Add the required NuGet package
The health‑check APIs are shipped in the `Microsoft.AspNetCore.HealthChecks` meta‑package (which pulls in the core library).
Run one of the following commands in the **Package Manager Console** or **dotnet CLI**:
```powershell
# Package Manager Console
Install-Package Microsoft.AspNetCore.HealthChecks
```
```bash
# dotnet CLI
dotnet add package Microsoft.AspNetCore.HealthChecks
```
> **Note** – If you only need the core health‑check abstractions (e.g., to register custom checks) you can install `Microsoft.Extensions.Diagnostics.HealthChecks`. The `HealthChecks` package also adds the endpoint routing helpers.
---
## 2️⃣ Register health‑check services
Open **`Program.cs`** (or `Startup.cs` if you are on an older template) and add the health‑check services to the DI container.
```csharp
var builder = WebApplication.CreateBuilder(args);
// ─────────────────────────────────────────────────────────────
// Existing service registrations (e.g., controllers, DB, etc.)
// ─────────────────────────────────────────────────────────────
builder.Services.AddControllers();
// 1️⃣ Register the health‑check service collection
builder.Services.AddHealthChecks()
// Example built‑in checks (optional – remove if you don’t need them)
.AddCheck("self", () => HealthCheckResult.Healthy())
// Add a SQL Server check (requires Microsoft.Extensions.Diagnostics.HealthChecks.SqlServer)
// .AddSqlServer(
// connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
// name: "sqlserver",
// healthQuery: "SELECT 1;",
// failureStatus: HealthStatus.Unhealthy,
// tags: new[] { "db", "sql" });
// Add a custom check (see section 4)
// .AddCheck<RedisHealthCheck>("redis", tags: new[] { "cache" });
```
> **Why add a “self” check?**
> It always returns `Healthy` and is useful for a quick “is the app running?” probe without any external dependencies.
---
## 3️⃣ Map the health‑check endpoint
Still in **`Program.cs`**, after building the app, map the endpoint. You can expose it at any route you like (commonly `/health` or `/healthz`).
```csharp
var app = builder.Build();
// ─────────────────────────────────────────────────────────────
// Existing middleware (e.g., HTTPS, auth, routing, etc.)
// ─────────────────────────────────────────────────────────────
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
// 2️⃣ Map the health‑check endpoint
app.MapHealthChecks("/healthz", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
// Optional: customize the response format
ResponseWriter = async (context, report) =>
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(entry => new
{
name = entry.Key,
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
duration = entry.Value.Duration.ToString(),
data = entry.Value.Data
}),
totalDuration = report.TotalDuration.ToString()
};
await context.Response.WriteAsJsonAsync(result);
},
// Optional: filter which checks are run for this endpoint
// Predicate = (check) => check.Tags.Contains("ready") // example
});
app.Run();
```
### What the code does
| Piece | Purpose |
|-------|---------|
| `builder.Services.AddHealthChecks()` | Registers the health‑check service collection. |
| `.AddCheck("self", ...)` | Adds a trivial “always healthy” check. |
| `app.MapHealthChecks("/healthz", options)` | Exposes the endpoint at `/healthz`. |
| `ResponseWriter` | Serialises the health report as JSON (the default writer returns a plain‑text status). |
| `Predicate` (optional) | Lets you expose *different* health‑check sets on different URLs (e.g., `/ready`, `/live`). |
---
## 4️⃣ (Optional) Add custom health checks
If you need to verify the health of a specific resource (database, cache, external API, etc.), create a class that implements `IHealthCheck`.
```csharp
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class ExternalApiHealthCheck : IHealthCheck
{
private readonly HttpClient _httpClient;
private readonly string _pingUrl;
public ExternalApiHealthCheck(HttpClient httpClient, IConfiguration config)
{
_httpClient = httpClient;
_pingUrl = config["ExternalApi:HealthEndpoint"]; // e.g. https://api.example.com/health
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient.GetAsync(_pingUrl, cancellationToken);
if (response.IsSuccessStatusCode)
return HealthCheckResult.Healthy("External API reachable.");
return HealthCheckResult.Unhealthy($"External API returned {(int)response.StatusCode}.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Exception while contacting external API.", ex);
}
}
}
```
Register it:
```csharp
builder.Services.AddHttpClient(); // needed for HttpClient injection
builder.Services.AddHealthChecks()
.AddCheck<ExternalApiHealthCheck>("external_api", tags: new[] { "external", "ready" });
```
Now the `/healthz` endpoint will include a `external_api` entry.
---
## 5️⃣ (Optional) Separate **Liveness** and **Readiness** probes
Kubernetes and many orchestrators like to differentiate:
| Probe | Typical checks |
|-------|----------------|
| **Liveness** | “Is the process alive?” – usually only the `self` check. |
| **Readiness** | “Is the app ready to accept traffic?” – includes DB, cache, external services. |
### Implementation
```csharp
// Liveness – only the trivial self‑check
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = (check) => check.Name == "self",
ResponseWriter = HealthCheckResponseWriter // reuse the JSON writer from above
});
// Readiness – all checks except the self‑check (or filter by tag)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("ready"),
ResponseWriter = HealthCheckResponseWriter
});
```
Add tags when you register checks:
```csharp
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddCheck<ExternalApiHealthCheck>("external_api", tags: new[] { "ready" })
.AddCheck<SqlServerHealthCheck>("sqlserver", tags: new[] { "ready" });
```
Now you can configure your orchestrator:
```yaml
livenessProbe:
httpGet:
path: /health/live
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 80
initialDelaySeconds: 10
periodSeconds: 5
```
---
## 6️⃣ Test the endpoint locally
Run the app and browse to:
```
GET https://localhost:{port}/healthz
GET https://localhost:{port}/health/live
GET https://localhost:{port}/health/ready
```
You should receive a JSON payload similar to:
```json
{
"status": "Healthy",
"checks": [
{
"name": "self",
"status": "Healthy",
"description": null,
"duration": "00:00:00.0012345",
"data": {}
},
{
"name": "external_api",
"status": "Unhealthy",
"description": "External API returned 500.",
"duration": "00:00:00.0456789",
"data": {}
}
],
"totalDuration": "00:00:00.0479134"
}
```
---
## 📦 Full Minimal Example (Program.cs)
```csharp
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
var builder = WebApplication.CreateBuilder(args);
// Add framework services
builder.Services.AddControllers();
// 1️⃣ Health‑check registration
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddCheck<ExternalApiHealthCheck>("external_api", tags: new[] { "ready" });
builder.Services.AddHttpClient(); // needed for ExternalApiHealthCheck
var app = builder.Build();
// Middleware pipeline
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
// 2️⃣ JSON response writer (re‑usable)
static async Task HealthCheckResponseWriter(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
checks = report.Entries.Select(entry => new
{
name = entry.Key,
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
duration = entry.Value.Duration.ToString(),
data = entry.Value.Data
}),
totalDuration = report.TotalDuration.ToString()
};
await context.Response.WriteAsJsonAsync(result);
}
// Liveness endpoint (only self)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = (check) => check.Name == "self",
ResponseWriter = HealthCheckResponseWriter
});
// Readiness endpoint (all checks tagged "ready")
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("ready"),
ResponseWriter = HealthCheckResponseWriter
});
// General health endpoint (everything)
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
ResponseWriter = HealthCheckResponseWriter
});
app.Run();
```
---
## 🎉 What you now have
* **`/healthz`** – full health report (useful for manual debugging).
* **`/health/live`** – liveness probe (process is up).
* **`/health/ready`** – readiness probe (all critical dependencies are healthy).
All of this uses the **built‑in, production‑ready** health‑check infrastructure, works with **ASP.NET Core 6/7/8**, and can be extended with any number of custom checks.
Feel free to ask if you need help wiring a specific dependency (SQL Server, Redis, RabbitMQ, etc.) or configuring the endpoint for a particular hosting environment!
Enhanced response (with ZPI)
**Solution Overview**
The code below adds a complete health‑checking infrastructure to an ASP.NET Core web API:
* **Health‑check services** are registered in the DI container.
* Two built‑in checks are exposed:
* **Liveness** – a trivial “always healthy” check that tells orchestrators the process is alive.
* **Readiness** – a custom check (`DatabaseHealthCheck`) that verifies the application can talk to its database.
* A **custom response writer** formats the JSON payload the way most monitoring tools expect (status, total duration, and an array of individual check results).
* The **endpoint** `/healthz` returns the liveness result, while `/ready` returns the readiness result.
* `ILogger<T>` is injected into the custom health‑check and the response writer for structured logging.
* All registrations are scoped where appropriate and the whole setup is encapsulated in extension methods for clean `Program.cs` usage.
---
## 1. Custom health‑check implementation
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
public sealed class DatabaseHealthCheck : IHealthCheck
{
private readonly ILogger<DatabaseHealthCheck> _logger;
// In a real app this would be a DbContext, IDbConnection, etc.
// For illustration we just simulate a check.
public DatabaseHealthCheck(ILogger<DatabaseHealthCheck> logger) =>
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
// Simulate async DB ping – replace with real logic.
await Task.Delay(50, cancellationToken);
bool dbIsUp = true; // <-- real check here
if (dbIsUp)
{
_logger.LogInformation("Database connectivity check succeeded.");
return HealthCheckResult.Healthy("Database reachable.");
}
_logger.LogWarning("Database connectivity check failed.");
return HealthCheckResult.Unhealthy("Database not reachable.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while checking database health.");
return HealthCheckResult.Unhealthy("Exception during DB check.", ex);
}
}
}
```
*The check is registered as **scoped** because it may depend on scoped services such as a `DbContext`.*
---
## 2. Custom JSON response writer
```csharp
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
public static class HealthCheckResponseWriter
{
public static async Task WriteResponseAsync(HttpContext context,
HealthReport report,
ILogger logger)
{
context.Response.ContentType = "application/json";
var response = new
{
status = report.Status.ToString(),
totalDuration = report.TotalDuration,
checks = report.Entries.Select(entry => new
{
name = entry.Key,
status = entry.Value.Status.ToString(),
description = entry.Value.Description,
duration = entry.Value.Duration,
// Include error details only when the check is unhealthy
error = entry.Value.Status == HealthStatus.Unhealthy
? entry.Value.Exception?.Message
: null
})
};
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
WriteIndented = true
});
logger.LogInformation("Health check response: {Payload}", json);
await context.Response.WriteAsync(json);
}
}
```
*The writer logs the payload (useful for debugging) and returns a clean, version‑agnostic JSON structure.*
---
## 3. Extension methods for registration
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class HealthCheckExtensions
{
/// <summary>
/// Registers health‑check services, including liveness and readiness.
/// </summary>
public static IServiceCollection AddApplicationHealthChecks(this IServiceCollection services)
{
// Register the custom DB check as scoped (it may need scoped deps)
services.AddScoped<IHealthCheck, DatabaseHealthCheck>(sp =>
sp.GetRequiredService<DatabaseHealthCheck>());
services.AddHealthChecks()
// Liveness – always healthy, no external deps
.AddCheck("liveness", () => HealthCheckResult.Healthy(),
tags: new[] { "live" })
// Readiness – depends on DB
.AddCheck<DatabaseHealthCheck>("database",
tags: new[] { "ready" });
return services;
}
/// <summary>
/// Maps health‑check endpoints with the custom response writer.
/// </summary>
public static IEndpointRouteBuilder MapHealthEndpoints(this IEndpointRouteBuilder endpoints)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("HealthCheck");
// Liveness endpoint – only the "live" tag
endpoints.MapHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = reg => reg.Tags.Contains("live"),
ResponseWriter = (c, r) => HealthCheckResponseWriter.WriteResponseAsync(c, r, logger)
});
// Readiness endpoint – only the "ready" tag
endpoints.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = reg => reg.Tags.Contains("ready"),
ResponseWriter = (c, r) => HealthCheckResponseWriter.WriteResponseAsync(c, r, logger)
});
return endpoints;
}
}
```
*The `AddApplicationHealthChecks` method registers the health‑check services and tags them for easy filtering.
`MapHealthEndpoints` wires the two routes (`/healthz` and `/ready`) and injects the logger into the response writer.*
---
## 4. Wire‑up in `Program.cs` (minimal API style)
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// ------------------------------------------------------------------
// 1️⃣ Register services
// ------------------------------------------------------------------
builder.Services.AddLogging(); // ILogger<T> support
builder.Services.AddApplicationHealthChecks(); // our extension above
// Add other services (controllers, DB, etc.) here...
// builder.Services.AddDbContext<...>();
var app = builder.Build();
// ------------------------------------------------------------------
// 2️⃣ Configure middleware
// ------------------------------------------------------------------
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// (optional) other middleware like routing, authentication, etc.
app.UseRouting();
// ------------------------------------------------------------------
// 3️⃣ Map endpoints
// ------------------------------------------------------------------
app.MapHealthEndpoints(); // registers /healthz and /ready
// Example minimal API endpoint
app.MapGet("/", () => "Hello World!");
app.Run();
```
*The `Program.cs` file stays tidy: a single call to `AddApplicationHealthChecks` registers everything, and `MapHealthEndpoints` adds the two health‑check routes.*
---
## 5. How it works
| Component | Role |
|-----------|------|
| **`DatabaseHealthCheck`** | Implements `IHealthCheck`; contains the actual readiness logic (e.g., DB ping). |
| **`HealthCheckResponseWriter`** | Serialises `HealthReport` to a friendly JSON payload and logs it. |
| **`AddApplicationHealthChecks`** | Registers the health‑check services, tags them (`live` / `ready`) and adds the custom check to the `IHealthChecksBuilder`. |
| **`MapHealthEndpoints`** | Exposes two endpoints: <br>• **`/healthz`** – liveness (only checks tagged *live*). <br>• **`/ready`** – readiness (only checks tagged *ready*). Both use the custom response writer. |
| **DI & Logging** | `ILogger<T>` is injected into the custom check and the response writer, providing structured logs without manual `Console.WriteLine`. |
| **Scoped registration** | The custom check is registered as scoped, allowing it to depend on other scoped services (e.g., `DbContext`). |
---
## 6. Testing the endpoints
```bash
# Liveness – should always be 200 OK
curl -i http://localhost:5000/healthz
# Readiness – will be 200 OK only if the DB check reports Healthy
curl -i http://localhost:5000/ready
```
Typical JSON response:
```json
{
"status": "Healthy",
"totalDuration": "00:00:00.1234567",
"checks": [
{
"name": "database",
"status": "Healthy",
"description": "Database reachable.",
"duration": "00:00:00.0500000",
"error": null
}
]
}
```
If the database is down, the `status` becomes `Unhealthy` and the `error` field contains the exception message.
---
### Summary
*Implemented a full health‑check pipeline using the built‑in `IHealthCheck`, `HealthCheckResult`, and `IHealthCheckPublisher` concepts.*
*Provided liveness and readiness endpoints, a custom DB health‑check, a JSON response writer, and proper DI/scoped registration with `ILogger<T>`.*
Drop the code into a new ASP.NET Core project, adjust the `DatabaseHealthCheck` to your real data‑access layer, and you have a production‑ready health‑checking solution.
Grader chain
Each grader reads the same baseline + enhanced pair and casts an independent verdict. Disagreement is the framework's own signal — not noise to be averaged away.
V1claude-haiku-4-5-20251001
BetterV4qwen2.5-coder-7b-instruct
BetterComments
Our grader said what it said. What do you say? Comment as a guest below.
No comments yet. Be the first to say what you make of this round.