Implement Health Checks in C# ASP.NET Core

This guide demonstrates how to implement health checks in C# using ASP.NET Core to monitor application availability, uptime, and system dependencies. You will learn how to enable built-in diagnostic endpoints, create custom checks for external services like databases or third-party APIs, separate readiness from liveness probes, and format the output for automated monitoring systems.

1. Setting Up Basic Health Checks

ASP.NET Core includes native middleware for health monitoring. To get started, register the health check services in the dependency injection container and map the endpoint in your Program.cs file.

var builder = WebApplication.CreateBuilder(args);

// Register health check services
builder.Services.AddHealthChecks();

var app = builder.Build();

// Map the health check route
app.MapHealthChecks("/health");

app.Run();

Running the application and navigating to /health returns a plain-text response of Healthy with an HTTP status code 200 OK. If a check fails, it returns Unhealthy with an HTTP status code 503 Service Unavailable.

2. Creating Custom Health Checks

To monitor application dependencies such as databases, caches, or remote HTTP services, implement the IHealthCheck interface.

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class DatabaseHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Simulate a database ping or connection test
            bool isDatabaseConnected = await CheckDatabaseConnectionAsync(cancellationToken);

            if (isDatabaseConnected)
            {
                return HealthCheckResult.Healthy("Database connection is operational.");
            }

            return HealthCheckResult.Degraded("Database response latency is higher than expected.");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database connection failed.", ex);
        }
    }

    private static Task<bool> CheckDatabaseConnectionAsync(CancellationToken cancellationToken)
    {
        // Replace with actual database ping logic
        return Task.FromResult(true);
    }
}

Register the custom health check in Program.cs:

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database_check", tags: new[] { "ready", "db" });

3. Configuring Liveness and Readiness Probes

Container orchestrators like Kubernetes distinguish between whether an application process is alive (Liveness) and whether it is ready to accept incoming traffic (Readiness). You can implement this separation using tags.

// Register checks with specific tags
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
    .AddCheck<DatabaseHealthCheck>("database", tags: new[] { "ready" });

var app = builder.Build();

// Liveness probe: returns 200 if the process is alive
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

// Readiness probe: returns 200 only if all required dependencies are healthy
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

4. Formatting Health Check Responses as JSON

By default, ASP.NET Core returns a plain-text status string. To integrate with logging pipelines and monitoring dashboards, customize the response to output structured JSON:

using System.Text.Json;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

app.MapHealthChecks("/health/details", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";

        var response = new
        {
            Status = report.Status.ToString(),
            TotalDuration = report.TotalDuration.TotalMilliseconds,
            Checks = report.Entries.Select(entry => new
            {
                Component = entry.Key,
                Status = entry.Value.Status.ToString(),
                Description = entry.Value.Description,
                Duration = entry.Value.Duration.TotalMilliseconds,
                Error = entry.Value.Exception?.Message
            })
        };

        await context.Response.WriteAsync(JsonSerializer.Serialize(response, new JsonSerializerOptions 
        { 
            WriteIndented = true 
        }));
    }
});

This endpoint returns a comprehensive snapshot of all registered checks, enabling automated platforms to detect partial degradations or specific component failures instantly.