How to Implement Health Check in Your ASP.NET Core Apps

Health checks are critical for tracking the status of your applications and services. They provide a quick and automated way to ensure that your application’s dependencies and components are functioning properly.

This article will look at how to implement health checks in a.NET 8 web application.

Why use Health Checks in ASP.NET Applications?

Health ch ecks enable you to proactively identify issues in your application, allowing you to address them before they affect users. Regularly verifying the health of your application’s components can help to ensure a more reliable and resilient system.

ASP.NET applications frequently rely on a variety of subsystems, including databases, file systems, APIs, and others. One of the most common scenarios is reliance on a database. Almost every application requires seamless interaction with a database, making it an essential system component. However, traditional application development has frequently overlooked this aspect, resulting in potential failures if the connection to the database is lost for any reason.

Consider a scenario in which your application relies on a database. If the connection to the database is lost for any number of reasons, the application will most likely fail. While this is a basic example of why health checks are beneficial, it does not cover the entire scope of their development. So, let’s look deeper into the database example to understand why ASP.NET health checks are important on a larger scale.

  • What if you could determine whether the database was available before making a connection?
    Imagine being able to assess the health and availability of your application’s critical subsystems before actively engaging with them. Consider a scenario in which, instead of experiencing a sudden application failure due to a lost database connection, you could proactively check its availability.
  • What if your application could gracefully handle a database failure scenario?
    Imagine allowing your application to display a user-friendly message when the database is unavailable. Instead of confusing your users with a cryptic error message, you could effectively communicate the unavailability of a critical component, resulting in a better user experience.
  • What if you could seamlessly switch to a backup database in the event of unavailability?
    Consider instructing your application to seamlessly transition to another database if the primary one becomes unavailable. This not only ensures application continuity, but also minimizes disruption for users.
  • What if you could instruct a load balancer to use a fallback environment based on health checks?
    Imagine being able to communicate your application’s health status to a load balancer. If the application is deemed unhealthy due to a missing database or another critical issue, the load balancer can intelligently redirect traffic to a fallback environment, ensuring that services remain available.

With ASP.NET Health Checks, you can:

  • Evaluate the health and availability of your subsystems.
  • Create an endpoint to notify other systems of your application’s health.
  • Consume health check endpoints from other systems.

These health checks are specifically designed for microservice environments, in which loosely coupled applications rely on knowing the status of their dependent systems. However, they are also useful in monolithic applications that rely on multiple subsystems and infrastructure.

How to implement health checks in .Net 8?

I’ll demonstrate the health check configuration in two ways in.Net 8.

1. Basic Health Checks Setup

This section aims to lay the groundwork for health checks in your application.

Required NuGet Packages
Ensure you have the following NuGet package installed:
1. Microsoft.Extensions.Diagnostics.HealthChecks

Adding Health Checks Services
In your Program.cs, add the required health check services to the Dependency Injection container:
Program.cs

builder.Services.AddHealthChecks();

//HealthCheck Middleware
app.MapHealthChecks("/api/health");

After you’ve added these to your.NET 8 web API, run the application successfully. Navigate to the following endpoint with a browser, assuming your application is running at:

https://localhost:44333/swagger/feedbackservice/index.html
To check the health of your web API, go to:
https://localhost:44333/api/health

After calling the endpoint, you will notice that your web API is “Healthy”.

When you call this endpoint, you should see that your web API is marked as “healthy.” It’s worth noting that, at this point, we’ve set up basic health checks to ensure the overall health of the application, but specific health checks for subsystems have yet to be implemented.

2. Implemented with Health Checks

Custom Health Checks for Enhanced Monitoring
In this section, we’ll look at specific examples of using custom health checks to monitor critical components of your.NET 8 web API. These checks go beyond the basics, providing a more detailed and insightful view of your application’s health.

Required NuGet Packages
Ensure you have the following NuGet package installed:
1. Microsoft.Extensions.Diagnostics.HealthChecks
2. AspNetCore.HealthChecks.SqlServer
3. AspNetCore.HealthChecks.UI
4. AspNetCore.HealthChecks.UI.Client
5. AspNetCore.HealthChecks.UI.InMemory.Storage
6. AspNetCore.HealthChecks.Uris

Note: I have separately created a file calledHealthCheck.cs , and implemented all the health check configurations.

a. Database Health Check

The database health check is an important part of monitoring the health of your application, especially if it relies on a database to store and retrieve information. This health check ensures that the database is not only accessible, but also responsive to queries.

HealthCheck.cs

public static void ConfigureHealthChecks(this IServiceCollection services,IConfiguration configuration)
{
    services.AddHealthChecks()
        .AddSqlServer(configuration["ConnectionStrings:DefaultConnection"], healthQuery: "select 1", name: "SQL Server", failureStatus: HealthStatus.Unhealthy, tags: new[] { "Feedback", "Database" });

    //services.AddHealthChecksUI();
    services.AddHealthChecksUI(opt =>
    {
        opt.SetEvaluationTimeInSeconds(10); //time in seconds between check 
        opt.MaximumHistoryEntriesPerEndpoint(60); //maximum history of checks 
        opt.SetApiMaxActiveRequests(1); //api requests concurrency 
        opt.AddHealthCheckEndpoint("feedback api", "/api/health"); //map health check api 

    })
        .AddInMemoryStorage();
}

configuration["ConnectionStrings:DefaultConnection"] This retrieves the connection string from your configuration, allowing for greater flexibility when configuring the database connection.

failureStatus: HealthStatus.Unhealthy This means that if the health check fails, the overall health status should be flagged as unhealthy.

Program.cs
Configure the ConfigureHealthChecks() inside the program.cs

//Congiguring Health Ckeck
builder.Services.ConfigureHealthChecks(builder.Configuration);

//HealthCheck Middleware
app.MapHealthChecks("/api/health", new HealthCheckOptions()
{
    Predicate = _ => true,
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.UseHealthChecksUI(delegate (Options options) 
{
    options.UIPath = "/healthcheck-ui";
    options.AddCustomStylesheet("./HealthCheck/Custom.css");

});

Output:
Endpoint: /api/health

Endpoint: /healthcheck-ui

b. Remote Endpoints Health Check

Next, we’ll implement a health check for remote endpoints and memory.
RemoteHealthCheck.cs

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

namespace FeedbackService.Api
{
    public class RemoteHealthCheck : IHealthCheck
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public RemoteHealthCheck(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
        public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken())
        {
            using (var httpClient = _httpClientFactory.CreateClient())
            {
                var response = await httpClient.GetAsync("https://api.ipify.org");
                if (response.IsSuccessStatusCode)
                {
                    return HealthCheckResult.Healthy($"Remote endpoints is healthy.");
                }

                return HealthCheckResult.Unhealthy("Remote endpoint is unhealthy");
            }
        }
    }
}

This health check uses an HTTP request to check the status of a remote endpoint (such as an API).

b. Memory Health Check

Finally, let’s implement a health check to monitor the memory status of the API service.

MemoryHealthCheck.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;

namespace FeedbackService.Api.HealthCheck
{
    public class MemoryHealthCheck : IHealthCheck
    {
        private readonly IOptionsMonitor<MemoryCheckOptions> _options;

        public MemoryHealthCheck(IOptionsMonitor<MemoryCheckOptions> options)
        {
            _options = options;
        }

        public string Name => "memory_check";

        public Task<HealthCheckResult> CheckHealthAsync(
            HealthCheckContext context,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            var options = _options.Get(context.Registration.Name);

            // Include GC information in the reported diagnostics.
            var allocated = GC.GetTotalMemory(forceFullCollection: false);
            var data = new Dictionary<string, object>()
        {
            { "AllocatedBytes", allocated },
            { "Gen0Collections", GC.CollectionCount(0) },
            { "Gen1Collections", GC.CollectionCount(1) },
            { "Gen2Collections", GC.CollectionCount(2) },
        };
            var status = (allocated < options.Threshold) ? HealthStatus.Healthy : HealthStatus.Unhealthy;

            return Task.FromResult(new HealthCheckResult(
                status,
                description: "Reports degraded status if allocated bytes " +
                    $">= {options.Threshold} bytes.",
                exception: null,
                data: data));
        }
    }
    public class MemoryCheckOptions
    {
        public string Memorystatus { get; set; }
        //public int Threshold { get; set; }
        // Failure threshold (in bytes)
        public long Threshold { get; set; } = 1024L * 1024L * 1024L;
    }
}

This health check evaluates the Feedback Service’s memory status using the allocated bytes.

Now let’s configure RemoteHealthCheck.cs and MemoryHealthCheck.cs inside the HealthCheck.cs

HealthCheck.cs

public static void ConfigureHealthChecks(this IServiceCollection services,IConfiguration configuration)
{
    services.AddHealthChecks()
        .AddSqlServer(configuration["ConnectionStrings:Feedback"], healthQuery: "select 1", name: "SQL servere", failureStatus: HealthStatus.Unhealthy, tags: new[] { "Feedback", "Database" })
        .AddCheck<RemoteHealthCheck>("Remote endpoints Health Check", failureStatus: HealthStatus.Unhealthy)
        .AddCheck<MemoryHealthCheck>($"Feedback Service Memory Check", failureStatus: HealthStatus.Unhealthy, tags: new[] { "Feedback Service" })
        .AddUrlGroup(new Uri("https://localhost:44333/api/v1/heartbeats/ping"), name: "base URL", failureStatus: HealthStatus.Unhealthy);

    //services.AddHealthChecksUI();
    services.AddHealthChecksUI(opt =>
    {
        opt.SetEvaluationTimeInSeconds(10); //time in seconds between check 
        opt.MaximumHistoryEntriesPerEndpoint(60); //maximum history of checks 
        opt.SetApiMaxActiveRequests(1); //api requests concurrency 
        opt.AddHealthCheckEndpoint("feedback api", "/api/health"); //map health check api 

    })
        .AddInMemoryStorage();
}

outputs:
Endpoint: /api/health

Endpoint: /healthcheck-ui

So there you have it. We have successfully implemented a few health checks within the web API. With these checks in place, your application is now ready to monitor and maintain its health. .NET 8’s health checks help build robust and resilient applications. 😍

Conclusion

Implementing health checks in your.NET 8 application is an important step toward creating a robust and dependable system. Built-in and custom health checks allow you to monitor the status of your application and its dependencies, resulting in a more seamless user experience.

This article covered the fundamentals of adding health checks to your application, and you can further tailor them to your specific requirements. As you continue to develop your application, consider adding more checks to cover various aspects of your system’s performance. Regularly reviewing and updating your health checks will help you keep your application robust and responsive.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *