Rants and Raves

Thanks for visiting my blog!

Health Checks in Your OpenAPI Specs
Health Checks in Your OpenAPI Specs
June 17, 2024

I recently was working on a project where the client wanted the health checks to be part of the OpenAPI specification. Here’s how you can do it.


Annoucement: In case you’re new here, I’ve just launched my blog with a new combined website. Take a gander around and tell me what you think!


ASP.NET Core supports health checks out of the box. You can add them to your project by adding the Microsoft.AspNetCore.Diagnostics.HealthChecks NuGet package. Once you’ve added the package, you can add health checks dependencies to your project by adding them to the ServiceCollection:

builder.Services.AddHealthChecks();

Once you’ve added the health checks, you need to map the health checks to an endpoint (usually /health ). You do that by calling MapHealthChecks:

app.MapHealthChecks("/health");

This works great. If you need to use the health checks, you can just call the /health endpoint.

At the client, our APIs were were generated via some tooling by reading the OpenApPI spec. The client wanted the health checks to be part of the OpenAPI specification so that the client could call it with the same generated code. But how to get it to work?

The solution is to not use the MapHealthChecks method, but instead to build an API (in my case, Minimal APIs) use perform the health checks. Here’s how you can do it:

builder.MapGet("/health", async (HealthCheckService healthCheck, 
  IHttpContextAccessor contextAccessor) =>
{
    var report = await healthCheck.CheckHealthAsync();
    if (report.Status == HealthStatus.Healthy)
    {
        return Results.Ok(new { Success = true });
    }
    else 
    {
        return Results.Problem("Unhealthy", statusCode: 500);
    }
}); //  ...

This works great. One of the reasons I decided to do it this way, is that instead of just a string, I wanted to return some context about the health check. This way, the client can know what is wrong with the health check.

NOTE: Returning reasons for the failure can be a security risk. Be careful not to return any sensitive information.

I found the best way to do this is to create a problem report with some information:

var report = await healthCheck.CheckHealthAsync();
if (report.Status == HealthStatus.Healthy)
{
  return Results.Ok(new { Success = true });
}
else
{
  var failures = report.Entries
    .Select(e => e.Value.Description)
    .ToArray();

  var details = new ProblemDetails()
  {
      Instance = contextAccessor.HttpContext.Request.GetServerUrl(),
      Status = 503,
      Title = "Healthcheck Failed",
      Type = "healthchecks",
      Detail = string.Join(Environment.NewLine, failures)
  };
  return Results.Problem(details);
}

By creating a problem detail, I can specify what the URL was used, the status code to use (503 in this case), and a list of the failures. The report that the CheckHealthAsync method returns has a dictionary of the health checks. I’m just using the description of the health check as the failure reason. Remember when you call AddHealthChecks you can add additional checks like this one for testing the DbContext connection string:

builder.Services.AddhealthChecks()
  // From the 
  // Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore 
  //package
  .AddDbContextCheck<ShoeContext>();

Then you can add some additional information for the OpenAPI specification:

builder.MapGet("/health", async (HealthCheckService healthCheck, 
  IHttpContextAccessor contextAccessor) => { ... })
            .Produces(200)
            .ProducesProblem(503)
            .WithName("HealthCheck")
            .WithTags("HealthCheck")
            .AllowAnonymous();

Make sense? Let me know what you think!