3
votes

I've added Microsoft.AspNetCore.Diagnostics.HealthChecks style health checks to my application as documented by Microsoft here.

I am also using Swashbuckle to generate a swagger document. I then use NSwag to generate a client API for my other applications to use.

The problem is that the healthcheck endpoint added with MapHealthChecks in Startup.cs is not being added to the ApiExplorer. This is a problem, because it is what Swashbuckle uses to generate the swagger document.

So my question what is the best way to add the healthcheck endpoint to ApiExplorer so that Swashbuckle can include it in the swagger file?

I have attempted to manually add the health check endpoint add ApiExplorer (code below). The application ran successfully, but the swagger document did not contain the endpoint.

// from Startup.cs

public virtual void ConfigureServices(IServiceCollection services)
{
    // ...

    // add swagger
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    });

    // add healthchecks
    services
        .AddHealthChecks()
        .AddDbContextCheck<DatabaseDomain.DbContext>(tags: new[] { "db" })
        ;

    // ...

}

public virtual void Configure(IApplicationBuilder app, IHostEnvironment env, IApiDescriptionGroupCollectionProvider apiExplorer)
{
    // ...

    app.UseEndpoints(endpoints =>
    {
        endpoints.
            .MapHealthChecks("/healthcheck", new HealthCheckOptions
            {
                Predicate = _ => true, // allow all healthchecks
                AllowCachingResponses = false,

                // custom writer to return health check results as JSON
                ResponseWriter = (context, result) => {
                    context.Response.ContentType = "application/json";
                    // serialize the health check results
                    var json = System.Text.Json.JsonSerializer.Serialize(new
                    {
                        // my custom response object
                    });

                    return context.Response.WriteAsync(json);
                },
            })
            .RequireAuthorization()
            ;
    });

    // attempt to get the healthcheck endpoint to ApiExplorer
    var healthcheckDescription = new ApiDescription
    {
        HttpMethod = "GET",
        RelativePath = "/healthcheck",
    };

    healthcheckDescription.SupportedRequestFormats.Add(new ApiRequestFormat
    {
        MediaType = "application/json"
    });

    healthcheckDescription.SupportedResponseTypes.Add(new ApiResponseType
    {
        IsDefaultResponse = true,
        StatusCode = (int)HttpStatusCode.OK,
        ApiResponseFormats = new List<ApiResponseFormat> {
            new ApiResponseFormat
            {
                MediaType = "application/json"
            }
        }
    });

    apiExplorer.ApiDescriptionGroups.Items.Append(new ApiDescriptionGroup("HealthCheck", new List<ApiDescription> { healthcheckDescription }));

    // configure swagger
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });

    // ...
}
1
I am battling with this right now and finally this is the right question being asked. Yes we can add a document filter (which is preferable than a controller solution). But if we can get ApiExplorer to 'see' the health endpoint the everything else just works.Jon H

1 Answers

1
votes

I ended up creating a controller specifically to return healthchecks at GET api/healthchecks.

This allows me to provide information to swagger about the data types being returned by the endpoint, and control the format of the returned data.

This is the example response given by Swagger UI:

{
  "status": "string",
  "totalDurationMs": 0,
  "apiVersion": "string",
  "apiVersionDescription": "string",
  "healthChecks": [
    {
      "name": "string",
      "status": "string",
      "description": "string",
      "durationMs": 0,
      "tags": ["string"],
      "data": [
        {
          "key": "string",
          "value": {}
        }
      ]
    }
  ]
}

And this is an actual response:

{
  "status": "Healthy",
  "totalDurationMs": 82,
  "apiVersion": "0.0.4-rc",
  "apiVersionDescription": "0.0.3 at commit 2b188d3 [25 ahead] on branch release/0.0.4 (0.0.4-rc)",
  "healthChecks": [
    {
      "name": "DbContext",
      "status": "Healthy",
      "description": null,
      "durationMs": 72,
      "tags": ["db"],
      "data": []
    }
  ]
}

The following is my implementation.

Startup.cs


public virtual void ConfigureServices(IServiceCollection services)
{
    // ...

    // add swagger
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    });

    // This allows me to access the HealthCheckOptions in my controllers
    services
        .AddSingleton(services => new HealthCheckOptions
        {
            Predicate = _ => true, // allow all healthchecks
            AllowCachingResponses = false,
        })
        ;

    // add healthchecks
    services
        .AddHealthChecks()
        .AddDbContextCheck<DatabaseDomain.DbContext>(tags: new[] { "db" })
        ;

    // ...

}

HealthCheckController.cs

Our HealthCheckController contains a single Index endpoint that will respond to GET api/healthcheck.

Index returns a custom HealthCheckReport object which is a wrapper around the actual HealthReport object. This allows me to control the data that is returns as well as the structure. I do this because I want to add additional info such as app version and commit details.

If you don't care about the format of the data that is returned, you could instead return the HealthReport object stored in the report variable. You would would need to change the return type to Task<HealthReport> as well as the in the ProducesResponseType attributes.

I use dependency injection to request the HealthCheckService and HealthCheckOptions objects. HealthCheckService is used to generate the actual report. HealthCheckOptions is used so I can access the configuration we did in Setup.cs.

[Route("api/[controller]")]
[ApiController]
public class HealthCheckController : AppController
{
    [HttpGet(Name = "Healthcheck")]
    [ProducesResponseType(typeof(HealthCheckReport), (int)HttpStatusCode.OK)]
    [ProducesResponseType(typeof(HealthCheckReport), (int)HttpStatusCode.ServiceUnavailable)]
    public async Task<HealthCheckReport> Index(
        [FromServices] HealthCheckService healthCheckService,
        [FromServices] HealthCheckOptions healthCheckOptions
    )
    {
        var report = await healthCheckService.CheckHealthAsync(healthCheckOptions.Predicate, HttpContext.RequestAborted);

        Response.StatusCode = healthCheckOptions.ResultStatusCodes[report.Status];
        Response.ContentType = "application/json";

        // if you want you can instead return `report`, but you would
        // also need to change the return type to Task<HealthReport>
        // as well as the in the ProducesResponseType attributes.
        return new HealthCheckReport
        {
            Status = report.Status.ToString(),
            TotalDurationMs = report.TotalDuration.Milliseconds,
            HealthChecks = report.Entries.Select(pair =>
            {
                var entry = pair.Value;

                return new HealthCheck
                {
                    Name = pair.Key,
                    Status = entry.Status.ToString(),
                    Description = entry.Description,
                    DurationMs = entry.Duration.Milliseconds,
                    Tags = entry.Tags,
                    Data = entry.Data.Select(p => new HealthCheckData { Key = p.Key, Value = p.Value }),
                };
            }),
        };
    }
}

The remaining classes are used to convert the HealthCheck object into the data structure I want to return from the GET api/healthchecks endpoint.

I'm only using these objects because I am not satisfied with how HealthCheck serializes into JSON and because I want to provide additional data.

For example, I add additional properties such as ApiVersion, so I can tell what version of my application is deployed.

HealthCheckReport.cs

public class HealthCheckReport
{
    public string Status { get; set; }
    public int TotalDurationMs { get; set; }

    public string ApiVersion => Startup.SemanticVersion;
    public string ApiVersionDescription => Startup.InformationalVersion;

    public IEnumerable<HealthCheck> HealthChecks { get; set; } = new HealthCheck[] { };
}

HealthCheck.cs

public class HealthCheck
{
    public string Name { get; set; }
    public string Status { get; set; }
    public string Description { get; set; }
    public int DurationMs { get; set; }
    public IEnumerable<string> Tags { get; set; } = new string[] { };
    public IEnumerable<HealthCheckData> Data { get; set; } = new HealthCheckData[] { };
}

HealthCheckData.cs

public class HealthCheckData
{
    public string Key { get; set; }
    public object Value { get; set; }
}