Creating Custom Health Checks in .NET Core
Tags: dotnet, dotnetcore, aspnet, aspdotnet
Published: 2023-09-12 09:37:57

Health checks are critical in ASP.NET Core applications developed by developers to monitor the availability and status of various dependencies and infrastructure. .NET Core provides a flexible health checking system that we can leverage at ASP.NET Development Services to create customized health checks tailored to our specific needs.

In this post, we’ll learn how ASP.NET Developers can help create custom health check implementations in ASP.NET Core to check for application-specific conditions. The [.NET Development Company](https://positiwise.com/technologies/dot-net-development) has extensive experience building robust health monitoring using the inbuilt .NET Core interfaces and middleware

Health checks are used to report the health status of different parts of an application by running diagnostic tests. Some examples are:

- Checking database connectivity
- Validating external service reachability
- Verifying available disk space
- Testing a circuit breaker’s state

In ASP.NET Core, we can register health check implementations in the dependency injection (DI) container. The health check middleware provided by ASP.NET Core will then execute these checks and expose endpoints to read their status.

The health check system is extensible so we can create custom checks for our own criteria. The results are exposed over HTTP at the /health endpoint which can be consumed by monitoring tools and load balancers. Unhealthy results can trigger alerts or graceedul shutdowns.

## Creating a Basic Health Check

To create a health check, we need to implement the IHealthCheck interface which contains a single CheckHealthAsync method:

```csharp
public class ExampleHealthCheck : IHealthCheck
{
  public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
  {
    // Check health here

    if(healthy)
    {
      return Task.FromResult(
        HealthCheckResult.Healthy("The check succeeded.")  
      );
    }

    return Task.FromResult(
      HealthCheckResult.Unhealthy("The check failed.")
    ); 
  }
}
```
The CheckHealthAsync method runs the health test logic and returns a HealthCheckResult indicating a healthy or unhealthy status.

We can include details in the result like an exception, custom data, message etc. The health check middleware will capture this result.

## Registering Health Checks

To enable health checks, we need to register our check classes in DI and enable the health check middleware in the pipeline.

Here is an example Startup configuration:

```csharp
public void ConfigureServices(IServiceCollection services)
{
  services.AddHealthChecks()
    .AddCheck<ExampleHealthCheck>("example_check"); 
}

public void Configure(IApplicationBuilder app) 
{
  app.UseHealthChecks("/health");
}
```
We register the check with a tag that identifies it. The health check middleware is added to the pipeline.

This exposes our health check at the /health/example_check endpoint. The overallhealth status is available at /health.

## Checking Database Connectivity

A common health check scenario is to validate that the application can connect to external databases and services it depends on.

Here is an example database health check:

```csharp
public class DatabaseHealthCheck : IHealthCheck
{
  private readonly IConfiguration _config;

  public DatabaseHealthCheck(IConfiguration config)
  {    
    _config = config;
  }

  public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
  {
    var connectionString = _config["Data:DefaultConnection"];
    
    using var connection = new SqlConnection(connectionString);

    try
    {
      await connection.OpenAsync(cancellationToken);

      return HealthCheckResult.Healthy();
    }
    catch (Exception ex)
    {
      return HealthCheckResult.Unhealthy(ex); 
    }                   
  }
}
```
This code tries to establish a connection to the database with the connection string loaded from configuration. A successful connection returns a healthy result, while a failure returns an unhealthy result with the exception.

We can now check for database outages from the health endpoints.

## Creating Composite Health Checks

The health check system also supports creating composite health checks that group together multiple checks.

This can be used to aggregate related checks like all database checks into a single result.

Here is an example composite health check class:

```csharp
public class StorageHealthCheck : IHealthCheck
{
  private readonly IEnumerable<IHealthCheck> _checks;  

  public StorageHealthCheck(IEnumerable<IHealthCheck> checks) 
  {
    _checks = checks;
  }

  public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
  {
    var results = await Task.WhenAll(_checks.Select(c => 
      c.CheckHealthAsync(context, cancellationToken)));
    
    if(results.Any(r => r.Status == HealthStatus.Unhealthy))
    {
        return HealthCheckResult.Unhealthy();
    }

    return HealthCheckResult.Healthy();
  }
}
```
Composite checks execute their child checks and aggregate the results. We can use this to have overall health results like “StorageHealth” composed from individual database checks.

## Response Caching

Since health checks are invoked frequently, we should enable caching of the results to optimize performance.

We can specify a cache duration when registering checks in Startup:

```csharp
services.AddHealthChecks()
  .AddCheck<ExampleCheck>()
  .AddCheck<DatabaseCheck>(tags: new[] { "db" })                
  .AddCheck<RedisCheck>(tags: new[] { "cache" })
  .SetExecutionTimeout(TimeSpan.FromSeconds(3))
  .AddDbContextCheck<AppDbContext>(tags: new []{ "db"})
  .Add Redis(redisConnectionString))
  .AddHealthChecksUi()                
  .AddCacheHealthChecks(TimeSpan.FromMinutes(5));
```

## Final Words

Health checks are an invaluable tool for monitoring critical system dependencies and infrastructure in production environments. ASP.NET Core provides an extensible health checking framework that allows us to create customized implementations to validate application-specific conditions. By leveraging the inbuilt interfaces and middleware, we can incorporate robust health monitoring in our .NET applications and gain greater visibility into their operational status. The ability to cache results and compose aggregated checks further enhances the utility of the health checking system. Overall, implementing custom health checks is a best practice for building resilient and observable ASP.NET Core services.