ASP.NET Core Health Checks Explained

This is part 7 in our series about ASP.NET Core:

ASP.NET Core 2.2 introduces a range of new features. One of the more interesting (IMO) is Health Checks. You may use tools like Pingdom or elmah.io Uptime Monitoring to ping your website in a specified interval. Pinging a single HTML page may or may not reveal if your application is healthy or not. Health Checks to the rescue! Before trying out the code yourself, make sure to install the recent version of ASP.NET Core 2.2 and Visual Studio 2017.

When developing a new service, I typically define a custom route (like /working). This endpoint is requested by Uptime Monitoring, and a notification is sent if things stop working. How /working is implemented, is up to each API. Some APIs may require a database connection, while others need something completely else. Until now, one of my custom health endpoints could have looked something like this:

[Route("working")]
public ActionResult Working()
{
    using (var connection = new SqlConnection(_connectionString))
    {
        try
        {
            connection.Open();
        }
        catch (SqlException)
        {
            return new HttpStatusCodeResult(503, "Generic error");
        }
    }

    return new EmptyResult();
}

In this example, the /working endpoint returns 503 if we couldn't open a connection to a SQL Server, but could be another database or any other resource.

With the new Health Checks feature in ASP.NET Core 2.2, health checks no longer require you to create new controllers like in the example above. Health checks are enabled and configured through Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddHealthChecks();
    ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...
    app.UseHealthChecks("/working");
    ...
}

With the sample above, a new endpoint named /working can be requested by your preferred uptime monitoring system or simply through the browser:

In this example, requesting the endpoint will return a status code 200 together with the body Healthy, indicating that the API is reachable. If we want to replicate the controller action in the first example, we have a couple of options. Health checks that you only need in a single API can be specified directly as a new check:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck("sql", () =>
        {
            using (var connection = new SqlConnection(_connectionString))
            {
                try
                {
                    connection.Open();
                }
                catch (SqlException)
                {
                    return HealthCheckResult.Unhealthy();
                }
            }

            return HealthCheckResult.Healthy();
        });
    ...
}

Requesting the /working endpoint automatically executes all lambdas provided to invocations of the AddCheck-method. In the case of a SqlException thrown inside the health check, a status code of 503 and body Unhealthy is returned:

A better approach is to write a re-usable health check, contained in its class:

public class SqlServerHealthCheck : IHealthCheck
{
    SqlConnection _connection;

    public string Name => "sql";

    public SqlServerHealthCheck(SqlConnection connection)
    {
        _connection = connection;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        try
        {
            connection.Open();
        }
        catch (SqlException)
        {
            return HealthCheckResult.Unhealthy();
        }
        
        return HealthCheckResult.Healthy();
    }
}

Configuring the new health check only requires a single line of code:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck<SqlServerHealthCheck>("sql");
    ...
}

Both lambda-based and IHealthCheck-based checks can be mixed and matched. When adding multiple checks, ASP.NET Core executes each check in the order they were added. All checks needs to return HealthCheckResult.Healthy() in order for the request to be successful. This means that a single failing health check, will cause an Unhealthy response.

Monitor your ASP.NET Core health checks

➡️ elmah.io Heartbeats works seamlessly with ASP.NET Core health checks ⬅️

Health checks are a great addition to an already excellent platform. In time, health checks will (hopefully) exist for most possible dependencies. In fact, the BeatPulse project already migrated their checks to ASP.NET Core Health Checks. Checks for popular databases like SQL Server, MySQL and Oracle already exists. I hope for .NET compatible software as well as the community to pick up the task of writing reusable health checks for all sorts of resources.

Dependency injection

As shown in the code for SqlServerHealthCheck, health checks added using the AddCheck<T>-method can use dependency injection, just as you know if from other areas of ASP.NET Core. Health checks are created as transient objects, meaning that a new instance is created every time the /working endpoint is requested. If you want another lifecycle, health checks can be added manually like this:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        .AddCheck<SqlServerHealthCheck>("sql");
    
    services.AddSingleton<SqlServerHealthCheck>();
    ...
}

Here, the SqlServerHealthCheck is added as a singleton.

Custom status codes

Health checks comes with a range of status codes out of the box. Like 503 for unhealthy responses. In some cases you may want to change the codes, in order to make the health endpoint work with a client expecting a certain status code.

To change a status code, you can provide custom options when calling the UseHealthChecks-method:

var options = new HealthCheckOptions();
options.ResultStatusCodes[HealthStatus.Unhealthy] = 418;
app.UseHealthChecks("/working", options);

In this example, I replace the status code for the Unhealthy state with 418 (I'm a teapot).

Custom response

Much like the returned status codes, the body of the response can be customized as well:

var options = new HealthCheckOptions();
options.ResponseWriter = async (c, r) =>
{
    c.Response.ContentType = "application/json";

    var result = JsonConvert.SerializeObject(new
    {
        status = r.Status.ToString(),
        errors = r.Entries.Select(e => new { key = e.Key, value = e.Value.Status.ToString() })
    });
    await c.Response.WriteAsync(result);
};
app.UseHealthChecks("/working", options);

In the example, a JSON object containing the overall status and status of each health check is returned.

I expect Microsoft and/or the community to come up with multiple health check response writers. A writer able to serialize the result to draft-inadarei-api-health-check-02 or rfc7807 would make a lot of sense.

Publishing health check results

If you are using an uptime monitoring solution like elmah.io, we automatically log the response from the /working endpoint. In some scenarios, publishing health check results directly from your application may be a better option.

ASP.NET Core Health Checks provide a concept named Publishers. By setting up one or more publishers, health check results can be stored in an internal or external storage like Application Insights or elmah.io.

To configure elmah.io as storage for health check results, install the Elmah.Io.AspNetCore.HealthChecks NuGet package:

Install-Package Elmah.Io.AspNetCore.HealthChecks -IncludePrerelease

Then add the elmah.io publisher:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        // Configure your health checks here
        .AddElmahIoPublisher("API_KEY", new Guid("LOG_ID"));
    ...
}

All degraded or unhealthy results are now persisted in elmah.io, where additional notification rules can be configured. For more information about our integration with health checks, check out Publishing ASP.NET Core 2.2 health check results to elmah.io.

More of an Application Insights kind of guy? Luckily, AI is supported as well:

Install-Package AspNetcore.HealthChecks.Publisher.ApplicationInsights

Then configure the AI publisher:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services
        .AddHealthChecks()
        // Configure your health checks here
        .AddApplicationInsightsPublisher();
    ...
}

Visualizing health checks

If you want your health check results visualized as more than a list of errors, there's a nice open source visualization too named HealthChecksUI.

To enable HealthChecksUI, install the AspNetCore.HealthChecks.UI package:

Install-Package AspNetCore.HealthChecks.UI

Enable the UI in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecksUI();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecksUI();
}

HealthChecksUI can be accessed on /healthchecks-ui:

Credit: Xabaril