2

after looking through many articles and not finding a clear answer, I would like to start one more time a topic about adding Health Checks to the swagger in ASP .Net Core.

Firstly, I would like to ask you if it is good idea to do that and how to do it in the easiest way.

Thanks in advance for all answers.

2 Answers2

2

First question, Why do we need Health Check?

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.

Here is simple demo about database Health check

First, Write a controller and Inject HealthCheckService in it.

[Route("[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class HealthController : ControllerBase
    {
        private readonly HealthCheckService healthCheckService;

        public HealthController(HealthCheckService healthCheckService)
        {
            this.healthCheckService = healthCheckService;
        }

        [HttpGet]
        public async Task<ActionResult> Get()
        {
            HealthReport report = await this.healthCheckService.CheckHealthAsync();
            var result = new
            {
                status = report.Status.ToString(),
                errors = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description.ToString() })
            };
            return report.Status == HealthStatus.Healthy ? this.Ok(result) : this.StatusCode((int)HttpStatusCode.ServiceUnavailable, result);
        }
    }

Then, In Program.cs(.Net 6), Configure the health check to test whether the query function of the database is normal

    //.....
    string connectionString = builder.Configuration.GetConnectionString("default");
    
    builder.Services.AddHealthChecks().AddCheck("sql", () =>
    {
        string sqlHealthCheckDescription = "Tests that we can connect and select from the database.";

        string sqlHealthCheckUnHealthDescription = "There is something wrong in database.";
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            try
            {
                connection.Open();
    
                //You can specify the table to test or test other function in database

                SqlCommand command = new SqlCommand("SELECT TOP(1) id from dbo.students", connection);
                command.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                //Log.Error(ex, "Exception in sql health check");
                return HealthCheckResult.Unhealthy(sqlHealthCheckUnHealthDescription );
            }
        }
    
        return HealthCheckResult.Healthy(sqlHealthCheckDescription);
    });

    //......

Result:

Swagger will expose this health check endpoint

enter image description here

When the query function works fine in database,It will return 200

enter image description here

When there is something wrong in database, It will return 503

enter image description here

Xinran Shen
  • 8,416
  • 2
  • 3
  • 12
  • Only issue I have with this is that even when healthy you've named it 'errors', it can be a bit confusing, should it not just be named 'entries'? – HenryMigo Nov 09 '22 at 10:43
1

For NSwag package:

Solution 1- Using PostProcess function

  // config it inside program.cs
  app.MapHealthChecks("/health", new() { });
  builder.Services.AddHealthChecks();
  builder.Services.AddSwaggerDocument(config =>
      {
        config.DocumentName = "Test1";
        config.Title = "App API";
        config.Description = "Rest API";
        //config.PostProcess = document => document.Info.Version = "v1";
        //config.ApiGroupNames = new[] { "v1" };
        config.PostProcess = document =>
        {
          var pathItem = new OpenApiPathItem();
          
          var param = new OpenApiParameter
          {
            Name = "key",
            IsRequired = true,
            Kind = OpenApiParameterKind.Query,
            Description = "The key to use for the health check auth",
            Schema = new NJsonSchema.JsonSchema { Type = JsonObjectType.String }
          };

          var operation = new OpenApiOperation
          {
            OperationId = "HealthCheckDetail",
            Description = "Check the health of the API",
            Tags = new List<string> { "Health" },
            Responses =
              {
                { "200", new OpenApiResponse { Description = "OK" } },
                { "401", new OpenApiResponse { Description = "Unauthorized" } },
                { "503", new OpenApiResponse { Description = "Service Unavailable" } }
              },

          };
          // if auth is required
          operation.Parameters.Add(param);
          pathItem.TryAdd(OpenApiOperationMethod.Get, operation);
          document.Paths.Add("/health", pathItem);
        };
      });

Solution 2- Using IApiDescriptionProvider

public class HealthCheckDescriptionProvider : IApiDescriptionProvider
{
  private readonly IModelMetadataProvider _modelMetadataProvider;

  public HealthCheckDescriptionProvider(IModelMetadataProvider modelMetadataProvider)
  {
    _modelMetadataProvider = modelMetadataProvider;
  }

  public int Order => -1;

  public void OnProvidersExecuting(ApiDescriptionProviderContext context)
  { }

  public void OnProvidersExecuted(ApiDescriptionProviderContext context)
  {

    var actionDescriptor = new ControllerActionDescriptor
    {
      ControllerName = "HealthChecks",
      ActionName = "health",
      Parameters = new List<ParameterDescriptor>(),
      EndpointMetadata = new List<object>(),
      ActionConstraints = new List<IActionConstraintMetadata>(),
      DisplayName = "Health check endpoint",
      Properties = new Dictionary<object, object?>(),
      BoundProperties = new List<ParameterDescriptor>(),
      FilterDescriptors = new List<FilterDescriptor>(),
      ControllerTypeInfo = new TypeDelegator(typeof(string))
    };

    var apiDescription = new ApiDescription
    {
      ActionDescriptor = actionDescriptor,
      HttpMethod = HttpMethods.Get,
      RelativePath = "health",
      GroupName = "v1",
    };

    var apiResponseType = new ApiResponseType
    {
      ApiResponseFormats = new List<ApiResponseFormat>
            {
                new ApiResponseFormat
                {
                    MediaType = ("text/plain"),
                    Formatter = new StringOutputFormatter(),
                },
            },
      Type = typeof(string),
      StatusCode = StatusCodes.Status200OK,
    };

    apiDescription.SupportedResponseTypes.Add(apiResponseType);
    context.Results.Add(apiDescription);
  }
}

 // config it inside program.cs
builder.Services.AddApiVersioning(options =>
      {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ReportApiVersions = true;
      });

builder.Services.AddVersionedApiExplorer(
          options =>
          {
            options.GroupNameFormat = "'v'V";
            options.SubstituteApiVersionInUrl = true;
          });

builder.Services.AddSwaggerDocument(config =>
          {
            config.DocumentName = "A_v1";
            config.Title = "A API";
            config.Description = "Rest API ";
            config.PostProcess = document => document.Info.Version = "v1";
            config.ApiGroupNames = new[] { "v1" };
          });

builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IApiDescriptionProvider, HealthCheckDescriptionProvider>());

builder.Services
    .AddHealthChecks();

app.MapHealthChecks("/health", new() { });

For Swashbuckle package:

Solution 1- Using IDocumentFilter

// Create a health check filter 
public class HealthChecksFilter : IDocumentFilter
{

    public void Apply(OpenApiDocument openApiDocument, DocumentFilterContext context)
    {
        var schema = context.SchemaGenerator.GenerateSchema(typeof(HealthCheckResponse), context.SchemaRepository);

        var healthyResponse = new OpenApiResponse();
        healthyResponse.Content.Add("application/json", new OpenApiMediaType { Schema = schema });
        healthyResponse.Description = "API service is healthy";

        var unhealthyResponse = new OpenApiResponse();
        unhealthyResponse.Content.Add("application/json", new OpenApiMediaType { Schema = schema });
        unhealthyResponse.Description = "API service is not healthy";

        var operation = new OpenApiOperation();
        operation.Description = "Returns the health status of this service";
        operation.Tags.Add(new OpenApiTag { Name = "Health Check API" });
        operation.Responses.Add("200", healthyResponse);
        operation.Responses.Add("500", unhealthyResponse);


        operation.Parameters.Add(new()
        {
            Name = "customParam",
            In = ParameterLocation.Query,
            Required = false,
            Description = "If this parameter is true, ....",
            Schema = new()
            {
                Type = "boolean"
            }
        });

        var pathItem = new OpenApiPathItem();
        pathItem.AddOperation(OperationType.Get, operation);

        openApiDocument?.Paths.Add("/health", pathItem);
    }
    // config it inside program.cs
    builder.Services.AddHealthChecks();
    builder.Services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("1.0.0", new OpenApiInfo
            {
                Version = "1.0.0",
                Title = "Test",
                Description = "Swagger definition for ....",
            });
            // Add that filter here
            c.DocumentFilter<HealthChecksFilter>();
        });

     app.MapHealthChecks("/health", new() { });
}
Ali.Asadi
  • 643
  • 10
  • 16