In this article, we are going to look into Health Checks in ASP.NET Core. Then we are going to look into the Health Checks Middleware provided by ASP.NET Core to create some checks and add a nice dashboard to view all our Health Checks. Finally, we will briefly look at some ways to make our Health Check endpoints more secure.

This article makes use of Docker to run SQL Server locally, but you can substitute it with your local database.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

What Is a Health Check in ASP.NET Core?

Health Checks allow us to determine the overall health and availability of our application infrastructure. They are exposed as HTTP endpoints and can be configured to provide information for various monitoring scenarios, such as the response time and memory usage of our application, or whether our application can communicate with our database provider.

Health Checks are not binary. What if our application is responding, but slower than expected? ASP.NET Core provides us with three different Health Check levels:

  • Healthy – our application is healthy and in a normal, working state.
  • Unhealthy – our application is unhealthy and is offline or an unhandled exception was thrown while executing the check.
  • Degraded – our application is still running, but not responding within an expected timeframe.

Types of Health Checks

When building applications, we can have many dependencies or services that our application requires to perform as expected. There are many different types of Health Checks we can configure:

Basic health probes are the simplest form of health checking. They are configured as a URL endpoint, which will respond with a health response. These basic checks allow us to very quickly determine if our application is healthy or not.

System health checks give us a plethora of information, such as disk storage and memory usage of the underlying host our application is running on.

Database probes can determine whether our database provider is online and whether our application can successfully talk to it. This can be anything from SQL Server, Postgres, Azure CosmosDB, and many more.

Custom health checks can range from anything such as checking a 3rd party service or API our application relies on, to checking our logging storage is not near capacity. These are more general and would require custom code to be written to execute the checks.

These are just some of the Health Checks we can utilize to determine if our application is in a healthy, functioning state.

When Should We Use Health Checks?

When we create Health Checks, we can create very granular, specific checks for certain services, which helps us greatly when diagnosing issues with our application infrastructure, as we can easily see which service/dependency is performing poorly. Our application may still be up and running, but in a degraded state that we can’t easily see by simply using the application, so having Health Checks in place give us a better understanding of what a healthy state of our application looks like.

Instead of relying on our users reporting an issue with the application, we can monitor our application health constantly and be proactive in understanding where our application isn’t functioning correctly and make adjustments as needed.

How to Implement Health Checks in ASP.NET Core Web API?

For this article, we will use an ASP.NET Core Web API to implement Health Checks.

Adding Health Checks Library

Let’s start by creating our ASP.NET Core Web API project. We will need to add a few NuGet packages to start for the Health Checks Middleware:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
  • AspNetCore.HealthChecks.UI
  • AspNetCore.HealthChecks.UI.Client

With these packages added, we will start by adding a very basic Health Check in Program class:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/health");

First, we create our WebApplicationBuilder.

Next, we add our Health Checks service to our application. We then need to call builder.Build() to get our app instance.

Finally, we map the endpoint /health to access our Health Checks.

With a basic Health Check configured, let’s run our application and navigate to /health, where we will see our application health of Healthy.

This can act as a basic health probe check, to ensure our application is up and responding. We can customize this response to include more information:

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

This time, we include a HealthCheckOptions parameter, and set the ResponseWriter to UIResponseWriter.WriteHealthCheckUIResponse. This uses the built-in response writer from the Health Checks package, but we can create our own response writer should we wish to provide further information.

Once again, we will navigate to /health where we will now see the totalDuration of the request included in the response.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Now that we have set up a basic health probe, let’s look at adding a database Health Check.

Creating Database Health Check

Before we create the Health Check, we need to add the AspNetCore.HealthChecks.SqlServer NuGet package to our project.

With this done, let’s add a check for our database to Program.cs:

builder.Services.AddHealthChecks().AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));

We will need to have a database running locally, which we can do with Docker (a local database will do as well if you’re not familiar with Docker):

docker run -e "ACCEPT_EULA=Y" -e "[email protected]" -p 1433:1433 --name sql --hostname sql -d mcr.microsoft.com/mssql/server:2019-latest

We also need to add a ConnectionStrings section to appsettings.json, using the DefaultConnection key we defined in the Health Check:

"ConnectionStrings": {
    "DefaultConnection": "Data Source=localhost,1433;Initial Catalog=master;User Id=sa;[email protected]"
}

The database check is going to execute a SELECT 1 query against the database, which in this case will be executed against the master database.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Running our application and navigating to /health, we will see our database Health Check returning a Healthy status.

We can also simulate our database being offline, or being unable to communicate with our application:

docker stop sql

By executing the docker stop command, we stop our database from running. Now when we refresh our /health page, our database Health Check will return a status of Unhealthy, stating that the timeout period expired when trying to connect.

Creating Custom Health Checks

We can create our own Health Checks, which can execute custom code to check whatever we wish. First, let’s create a class, MyHealthCheck, and implement the IHealthCheck interface:

public class MyHealthCheck: IHealthCheck
{
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
            
    }
}

Now, we’re going to add some logic that will simulate this Health Check providing different response times, to demonstrate the different Health Check results we can expect:

private Random _random = new Random();

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
    CancellationToken cancellationToken = default)
{
    var responseTime = _random.Next(1, 300);

    if (responseTime < 100)
    {
        return Task.FromResult(HealthCheckResult.Healthy("Healthy result from MyHealthCheck"));
    }
    else if (responseTime < 200)
    {
        return Task.FromResult(HealthCheckResult.Degraded("Degraded result from MyHealthCheck"));
    }
 
    return Task.FromResult(HealthCheckResult.Unhealthy("Unhealthy result from MyHealthCheck"));
}

The first thing we add is a new private Random class.

Within our CheckHealthAsync method, we will generate a random responseTime between 1 and 300. We will then check if responseTime < 100 and return a Healthy result.

If responseTime < 200 , this will return a Degraded result. Otherwise, we return an Unhealthy result.

Back in Program.cs, we need to add our new Health Check to our existing checks:

builder.Services.AddHealthChecks()
    .AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
    .AddCheck<MyHealthCheck>("MyHealthCheck");

Now when we navigate to /health we will not only see our database Health Check but also our custom one, with the status and description we provided. If we refresh the page a few times, we will see different results.

One thing to note now that we have introduced multiple Health Checks is that the overall reported health status of our application will now depend on the combined status of our individual Health Checks. For example, if our database check is Healthy, but our custom check is Unhealthy, our system will report an Unhealthy status overall.

Filtering Health Checks

Now that we have multiple Health Checks, we want an easy way to be able to filter them, possibly by the type of Health Check that is being performed. Fortunately, Health Checks provide tags, which allow us to do just that.

The first thing we want to do is add a tag to both Health Checks:

builder.Services.AddHealthChecks()
    .AddSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), tags: new[] { "database" })
    .AddCheck<MyHealthCheck>("MyHealthCheck", tags: new[] { "custom" });

Now that we have our tags defined, let’s add a new endpoint, specifically for our Health Checks tagged with custom:

app.MapHealthChecks("/health/custom", new HealthCheckOptions
{
    Predicate = reg => reg.Tags.Contains("custom"),
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

We map a new Health Check endpoint to /health/custom, and this time we add a Predicate delegate to only return Health Checks that include our custom tag.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Now if we navigate to /health/custom, we will see only MyHealthCheck, while our /health endpoint still returns both of our Health Checks.

So far we have been returning an unstyled JSON response from our endpoints. Let’s see how we can make a nice-looking dashboard.

Enhancing Health Checks UI

This new dashboard requires a persistence store, so let’s add the AspNetCore.HealthChecks.UI.InMemory.Storage NuGet package. With that added, let’s add our Health Checks UI in Program.cs:

builder.Services.AddHealthChecksUI().AddInMemoryStorage();

Below our previous AddHealthChecks() method call, we now call AddHealthChecksUI() and use in-memory storage. Next, we need to map this new UI:

app.MapHealthChecksUI();

The final thing we need to do is add some configuration in appsetttings.json, so this new UI knows what endpoints to make requests to for our previously created Health Checks:

"HealthChecksUI": {
  "HealthChecks": [
    {
      "Name": "My Health Checks",
      "Uri": "/health"
    }
  ],
  "EvaluationTimeInSeconds": 5
}

Here, we are defining our /health endpoint, and we set EvaluationTimeInSeconds to 5, which is how frequently we will poll our /health endpoint to run our Health Checks.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Now if we navigate to /healthchecks-ui, we should see a much more friendly interface:

health-checks-ui

Here, we can see MyHealthCheck and our database Health Checks that we created previously, and the polling interval of 5 seconds.

This UI will refresh every 5 seconds, and with a bit of luck, we should see the Health status of our application changing, depending on the random value generated from MyHealthCheck.

Health Checks Security

We can take advantage of the ASP.NET Core Authorization Middleware to make our Health Check endpoints secure.

Let’s create a secure endpoint for our Health Checks by using the RequireAuthorization method:

app.MapHealthChecks("/health/secure", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).RequireAuthorization();

This will ensure our /health/secure endpoint uses whatever authorization middleware we have configured for our application.

Adding CORS to Our Health Checks

Although it isn’t common practice to manually run Health Checks by navigating to the UI in the browser, we can enable CORS for our endpoints, by adding RequireCors:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
builder.Services.AddCors(opt =>
{
    opt.AddPolicy("MyCorsPolicy", builder => 
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});

// other configuration

app.MapHealthChecks("/health/cors", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}).RequireCors("MyCorsPolicy");

app.UseCors("MyCorsPolicy");

First, we add CORS to our application, creating a custom policy, MyCorsPolicy, using a very basic setup. We then need to use this same policy in our RequireCors method for our /health/cors endpoint. Finally, we call the UseCors() method, passing in our policy name to add CORS to the application’s pipeline.

Conclusion

In this article, we’ve learned what Health Checks in ASP.NET Core are and why we should use them in our applications.

After that, we’ve learned how to add a basic Health Check to an ASP.NET Core Web API, along with a database check, and finally a custom one.

Next, we created a clean, user-friendly dashboard to view all our Health Checks.

We finished up by briefly looking at the ways we can secure our health check endpoints, using the ASP.NET Core Authorization Middleware, and finally by adding CORS to make our application more secure.