Health Checks in ASP.Net Core

Health checks are a critical part of any distributed system. Especially in the era of microservices, it is extremely important to understand if the service is running healthy or not. In this blog, I will walk through how to implement Health Checks in ASP.Net Core.

ASP.Net Core provides support for health checks out of the box through middleware and extension methods.

What are the different aspects of an application that we need through health checks?

  • Firstly, finding out dependencies such as a database or other services are alive or not
  • Secondly, the usage of physical resources on the machine

We usually use the health checks for monitoring applications and to see how the applications are behaving. The health check results are also in use for scaling out applications based on how the service is degrading in response time.

To further dig deeper into the topic let us start by creating an ASP.Net Core Web application. And let us demonstrate the feature.

Creating a new ASP.Net Core Web Application

First of all, we will create a new ASP.Net Core web application. To achieve that, I will open up Visual Studio 2019, and select the menu option File -> New -> Project.

Once the new project creation window pops up, I will select the ASP.Net Core Web Application. And then I will click on the Next button.

Secondly, on the next page of the pop-up, I will provide the name of the project in the Project Name field as HealthCheck.Demo. And then click on the Create button.

Finally, on the final page, I will select the API template option. And I will keep other values default (ASP.Net Core 3.1) and click on the Create button.

Setting up Health Checks in ASP.Net Core

Once the project is created, I will set up a basic health check. For that, I will start with the Startup class.

Change in ConfigureServices method

In the ConfigureServices of the Startup class, I will add the health check system into the dependency injections container. For that, I will call the AddHealthChecks extension method on the IServiceCollection instance. The method will return an instance of IHealthChecksBuilder.

As the name suggests the IHealthChecksBuilder provides methods to set up and chain health checks.

As a first step, I will call the AddCheck method on the IHealthChecksBuilder instance returned by the AddHealthChecks.

The AddCheck takes two parameters, the first one is a name for the health check. And the second one is a Func delegate which returns the HealthCheckResult struct.

For the time being, I will just return HealthCheckResult.Healthy as the return.

services.AddHealthChecks()
    .AddCheck("Test Health Check", () => HealthCheckResult.Healthy());

Change in Configure method

Once the dependency injection container setup is complete, it is time to set up the health check middleware. For that, I will update the implementation of the UseEndpoints extension method call to the IApplicationBuilder instance.

Inside of the delegate call for the UseEndpoints method parameter; I will call the MapHealthChecks extension method on the IEndpointRouteBuilder instance.

The MapHealthChecks method takes the URL pattern as the parameter. I will pass api/health as the parameter value. You can use what suits your application.

Microsoft uses /health as the endpoint in all the examples of their documentation here.

app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("api/health");
        endpoints.MapControllers();
    });

The complete change in code inside of the Startup class is below:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;

namespace HealthCheck.Demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddHealthChecks()
                .AddCheck("Test Health Check", () => HealthCheckResult.Healthy());
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHealthChecks("api/health");
                endpoints.MapControllers();
            });
        }
    }
}

Running the application

Once the code is complete, I will run the application to test the health check endpoint.

After running the application, if I go to the /api/health endpoint I will see the response as Healthy.

health checks in asp.net core
Healthy Response

Using Multiple Health Checks Providers

Now, in a real-life scenario, we will never return a HealthCheckResult.Healthy from the health checks return and call it a day. For health checks, we will try to validate multiple aspects of the application to see if they are working as expected.

For example, let us say we want to check the state of our database. And make sure that we are able to successfully connect to the database.

For that, we will create a new class, DbHealthCheckProvider. The only responsibility of this class is to connect to the database. And make sure that the connection was successful.

I will create this class as a static class and it will have a single static method Check. The method Check will take the connection string of the database. Now since that class will be called from the Startup class, and will not be used anywhere, it is fair to do this. Also, we will never have to test this class, for obvious reasons; it does not add any value.

For this to work, I will install the NuGet package System.Data.SqlClient.

Inside of the Check method, I will open a connection to my local SQL Server And if the connection was successful, I will return HealthCheckResult.Healthy. Otherwise, I will return HealthCheckResult.Unhealthy from the method.

using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Data.SqlClient;

namespace HealthCheck.Demo
{
    public static class DbHealthCheckProvider
    {
        public static HealthCheckResult Check(string connectionString)
        {
            // Code to check if DB is running
            try
            {
                using var connection = new SqlConnection(connectionString);
                connection.Open();
                return HealthCheckResult.Healthy();
            }
            catch
            {
                return HealthCheckResult.Unhealthy();
            }
        }
    }
}

Setting up Startup class

Once the DbHealthCheckProvider class is ready, I will set up the Startup to use this class.

For that, I will update the ConfigureServices method. And I will change the delegate implementation inside the AddCheck method. Instead of calling an inline delegate to return HealthCheckResult.Healthy. Now, I will call the DbHealthCheckProvider.Check.

And for the database connection string, I will pass an empty string. This should result in an Unhealthy response when we run the application.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHealthChecks()
        .AddCheck("DB Health Check", () => 
            DbHealthCheckProvider.Check(""));
}

Once the code inside of Startup is configured to use the new DbHealthCheckProvider class, I will run the application.

Unhealthy Response
Unhealthy Response

As expected, when we navigate to the /api/health endpoint, we can see an Unhealthy response back.

Health checks in asp.net core console response
Console Response

In the above console response, we can see that the Health Checks system logs a fail in the console. This log can be used to identify what exactly failed. As you can see it prints the name of the health checks “DB Health Check”.

The HealthCheckResult.Unhealthy or HealthCheckResult.Healthy method takes an optional string description parameter. This is very handy when it comes to logging.

To demonstrate that I will update the implementation of the HealthCheckResult.Unhealthy method inside of the DbHealthCheckProvider. And I will add a description to the Unhealthy method.

using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Data.SqlClient;

namespace HealthCheck.Demo
{
    public static class DbHealthCheckProvider
    {
        public static HealthCheckResult Check(string connectionString)
        {
            // Code to check if DB is running
            try
            {
                using var connection = new SqlConnection(connectionString);
                connection.Open();
                return HealthCheckResult.Healthy();
            }
            catch
            {
                return HealthCheckResult.Unhealthy("Could not connect to database!");
            }
        }
    }
}

Now if I run the application and navigate to the /api/health endpoint. I will see the description text “Could not connect to database!” will print out in the console.

unhealthy with description
Unhealthy with description

Queue Health Checks Provider

Now, let us say we our application also connects to a RabbitMQ server for processing messages. Now to check if we are able to connect to RabbitMQ server, we will need to write a health check provider.

Putting all health checks inside of a single class makes things difficult to manage. As well as harder to find out what was the cause of the issue.

Hence, I will create a new class, MqHealthCheckProvider. This will also be a static class with a single static method Check. For this class, we will just return HealthCheckResult.Healthy from the method, we will not implement any real RabbitMQ connectivity for the demo.

using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace HealthCheck.Demo
{
    public static class MqHealthCheckProvider
    {
        public static HealthCheckResult Check(string mqUri)
        {
            // Code to check if MQ is running
            return HealthCheckResult.Healthy();
        }
    }
}

Once the MqHealthCheckProvider is ready, I will update the Startup class to configure the MqHealthCheckProvider.

Since the AddCheck method also returns IHealthChecksBuilder instance, I will chain another AddCheck to configure the MqHealthCheckProvider.

And for this case as well when we call the Check method of the MqHealthCheckProvider class, we will pass the MQ Server string as an empty string.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHealthChecks()
        .AddCheck("DB Health Check", () => 
            DbHealthCheckProvider.Check(""))
        .AddCheck("MQ Health Check", () => 
            MqHealthCheckProvider.Check(""));
}

Now when I run the application, I will still see the response as Unhealthy. This is because the health check is the aggregation of all the health checkpoints configured.

Hence, I will update the DbHealthCheckProvider.Check call to pass a valid connection string to make that call as healthy as well.

I will get the connection string from the appsettings.json file.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHealthChecks()
        .AddCheck("DB Health Check", () => 
            DbHealthCheckProvider.Check(Configuration["Connection"]))
        .AddCheck("MQ Health Check", () => 
            MqHealthCheckProvider.Check(""));
}

Now if I run the application, I will see Healthy in response.

Using Generic AddCheck method

The next thing I am going to walk through is how to use the generic AddCheck method. For that let us consider our application is interacting with third part SAAS software like Sendgrid through an HTTP connection.

Firstly, I will create a new class that is responsible for checking if an HTTP request to Sendgrid is successful or not. I will name the class as SendgridHealthCheckProvider.

This class will implement the interface IHealthCheck, which is part of the namespace Microsoft.Extensions.Diagnostics.HealthChecks. I will implement the method CheckHealthAsync from the interface.

For the time being for the demo, I will return HealthCheckResult.Healthy from the method. I will not make a real HTTP call outside.

using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Threading;
using System.Threading.Tasks;

namespace HealthCheck.Demo
{
    public class SendgridHealthCheckProvider : IHealthCheck
    {
        public Task<HealthCheckResult> CheckHealthAsync(
            HealthCheckContext context, 
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(HealthCheckResult.Healthy());
        }
    }
}

Once the SendgridHealthCheckProvider is ready, I will go and update the Startup class to configure it.

For the configuration, I will use the generic version of the AddCheck method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHealthChecks()
        .AddCheck("DB Health Check", () => DbHealthCheckProvider.Check(Configuration["Connection"]))
        .AddCheck("MQ Health Check", () => MqHealthCheckProvider.Check(""))
        .AddCheck<SendgridHealthCheckProvider>("Sendgrid Health Check");
}

Finally, I will run the application. And as expected, I will get a Healthy response from the API call.

Implementing Degraded option

One of the options of the HealthCheckResult struct that we have not tried so far is the Degraded. Now the question is when we will use this option?

Whenever a database or some external service is taking more than the SLA, we can send a Degraded response. This can be achieved by using a timer to check the time taken by the external dependency.

To demonstrate that, let us update the MqHealthCheckProvider class. Now in the Check method, it will return HealthCheckResult.Degraded.

using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace HealthCheck.Demo
{
    public static class MqHealthCheckProvider
    {
        public static HealthCheckResult Check(string mqUri)
        {
            // Code to check if MQ is running
            return HealthCheckResult.Degraded("MQ is running slow!");
        }
    }
}

Now if we run the application, we can see that the response is Degraded.

degraded response
Degraded response

And also as expected the console log will print a warning, along with the same of the health check. And the description message “MQ is running slow!”.

degraded console output
Degraded warning in console

Health Checks in ASP.Net Core through Postman

Now, the question is, how the monitoring tools will interpret this response. Well, the best way to implement that will be through checking the HTTP status code. And if needed further investigation then checking the response body.

Let us run the requests through the postman and see how it all works. So to test that, we will just run the application as it is running now. Which means it will return a Degraded response.

If we navigate to the /api/health, we will see the HTTP Status code is still 200, but the response body is Degraded.

postman health checks in asp.net core
Degraded in Postman

Now let us see how an Unhealthy response will look like in postman. Specifically what status code will it return.

For doing that, let us just update the Startup classes ConfigureServices method. And for the AddCheck of the DbHealthCheckProvider, let us pass an empty connection string. This will create an Unhealthy response.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHealthChecks()
        .AddCheck("DB Health Check", () => DbHealthCheckProvider.Check(""))
        .AddCheck("MQ Health Check", () => MqHealthCheckProvider.Check(""))
        .AddCheck<SendgridHealthCheckProvider>("Sendgrid Health Check");
}

Once I make this change, let us run the application and check the postman response.

Unhealthy response in Postman

In the response, we can see that the HTTP status code is 503 Service Unavailable. And the response string is Unhealthy. Based on the HTTP status code any motoring tool can find out if the service is up or down.

For a Healthy response, we will get an HTTP Status Code of 200 and Healthy in the response string.

Conclusion

The Health Checks in ASP.Net Core implementation is really awesome in my opinion. It is super simple to implement and integrate with individual implementation classes.

I have done a YouTube video going through the concepts here.

The source code is available in Github here.