From 9f0b1cc7b5df22a5ba30c9fa656b6efc905b283d Mon Sep 17 00:00:00 2001 From: Juan Agudelo Date: Wed, 15 Oct 2025 17:46:20 -0500 Subject: [PATCH 1/5] Enhance health check functionality with multi-tenancy support and custom response formatting --- .../Helpers/HealthCheckServiceExtensions.cs | 22 ++++++++ .../Helpers/HealthCheckServiceExtensions.cs | 50 ++++++++++++------- Application/EdFi.Ods.AdminApi/Program.cs | 29 ++++++++++- 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs index 240f15273..0ca00c138 100644 --- a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -23,4 +23,26 @@ public static IServiceCollection AddHealthCheck(this IServiceCollection services return services; } + + public static IServiceCollection AddHealthCheck( + this IServiceCollection services, + string adminConnectionString, + string securityConnectionString, + bool isSqlServer) + { + var hcBuilder = services.AddHealthChecks(); + + if (isSqlServer) + { + hcBuilder.AddSqlServer(adminConnectionString, name: "EdFi_Admin"); + hcBuilder.AddSqlServer(securityConnectionString, name: "EdFi_Security"); + } + else + { + hcBuilder.AddNpgSql(adminConnectionString, name: "EdFi_Admin"); + hcBuilder.AddNpgSql(securityConnectionString, name: "EdFi_Security"); + } + + return services; + } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs index 09a63cfec..5aaef954c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -4,7 +4,6 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; using EdFi.Ods.AdminApi.Common.Settings; @@ -18,10 +17,31 @@ public static IServiceCollection AddHealthCheck( IConfigurationRoot configuration ) { - Dictionary connectionStrings; var databaseEngine = configuration.Get("AppSettings:DatabaseEngine", "SqlServer"); var multiTenancyEnabled = configuration.Get("AppSettings:MultiTenancy", false); - var connectionStringName = "EdFi_Admin"; + + if (!string.IsNullOrEmpty(databaseEngine)) + { + var isSqlServer = DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer); + var hcBuilder = services.AddHealthChecks(); + + // Add health checks for both EdFi_Admin and EdFi_Security databases + AddDatabaseHealthChecks(hcBuilder, configuration, "EdFi_Admin", multiTenancyEnabled, isSqlServer); + AddDatabaseHealthChecks(hcBuilder, configuration, "EdFi_Security", multiTenancyEnabled, isSqlServer); + } + + return services; + } + + private static void AddDatabaseHealthChecks( + IHealthChecksBuilder hcBuilder, + IConfigurationRoot configuration, + string connectionStringName, + bool multiTenancyEnabled, + bool isSqlServer + ) + { + Dictionary connectionStrings; if (multiTenancyEnabled) { @@ -42,24 +62,20 @@ IConfigurationRoot configuration }; } - if (!string.IsNullOrEmpty(databaseEngine)) + foreach (var connectionString in connectionStrings) { - var isSqlServer = DatabaseEngineEnum.Parse(databaseEngine).Equals(DatabaseEngineEnum.SqlServer); - var hcBuilder = services.AddHealthChecks(); + var healthCheckName = multiTenancyEnabled + ? $"{connectionString.Key}_{connectionStringName}" + : connectionStringName; - foreach (var connectionString in connectionStrings) + if (isSqlServer) + { + hcBuilder.AddSqlServer(connectionString.Value, name: healthCheckName); + } + else { - if (isSqlServer) - { - hcBuilder.AddSqlServer(connectionString.Value, name: connectionString.Key); - } - else - { - hcBuilder.AddNpgSql(connectionString.Value, name: connectionString.Key); - } + hcBuilder.AddNpgSql(connectionString.Value, name: healthCheckName); } } - - return services; } } diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 365d6f56f..af4bdb7e7 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -10,6 +10,8 @@ using EdFi.Ods.AdminApi.Infrastructure; using log4net; using log4net.Config; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.HealthChecks; var builder = WebApplication.CreateBuilder(args); @@ -61,7 +63,32 @@ app.MapFeatureEndpoints(); app.MapControllers(); -app.UseHealthChecks("/health"); +app.UseHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = async (context, report) => + { + context.Response.ContentType = "application/json"; + + // Set HTTP status code based on health check results + // 200 OK if all are healthy, 503 Service Unavailable if any are unhealthy + context.Response.StatusCode = report.Status == HealthStatus.Healthy ? 200 : 503; + + var response = new + { + status = report.Status.ToString(), + checks = report.Entries.Select(x => new + { + name = x.Key, + status = x.Value.Status.ToString(), + exception = x.Value.Exception?.Message, + duration = x.Value.Duration.ToString() + }), + duration = report.TotalDuration.ToString() + }; + + await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(response)); + } +}); if (app.Configuration.GetValue("SwaggerSettings:EnableSwagger")) { From b18c47a5bf8ac4a56dbf858e44258e61afafffdd Mon Sep 17 00:00:00 2001 From: Juan Agudelo Date: Thu, 16 Oct 2025 12:46:24 -0500 Subject: [PATCH 2/5] Add unit tests for health check registration with multi-tenancy support --- .../HealthCheckServiceExtensionsTests.cs | 75 +++++++++++++++++++ Application/EdFi.Ods.AdminApi/Program.cs | 7 +- 2 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs new file mode 100644 index 000000000..3d6b56908 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/HealthCheckServiceExtensionsTests.cs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Shouldly; +using System.Collections.Generic; +using System.Linq; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure; + +[TestFixture] +public class HealthCheckServiceExtensionsTests +{ + [Test] + public void AddHealthCheck_ShouldRegisterBothAdminAndSecurityHealthChecks_WhenMultiTenancyDisabled() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); // Required for health checks + var configuration = CreateTestConfiguration(multiTenancy: false); + + // Act + services.AddHealthCheck(configuration); + + // Assert - Check that health check services are registered + var healthCheckServiceDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(HealthCheckService)); + healthCheckServiceDescriptor.ShouldNotBeNull(); + } + + [Test] + public void AddHealthCheck_ShouldRegisterMultiTenantHealthChecks_WhenMultiTenancyEnabled() + { + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); // Required for health checks + var configuration = CreateTestConfiguration(multiTenancy: true); + + // Act + services.AddHealthCheck(configuration); + + // Assert - Check that health check services are registered + var healthCheckServiceDescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(HealthCheckService)); + healthCheckServiceDescriptor.ShouldNotBeNull(); + } + + private static IConfigurationRoot CreateTestConfiguration(bool multiTenancy) + { + var configData = new Dictionary + { + ["AppSettings:DatabaseEngine"] = "SqlServer", + ["AppSettings:MultiTenancy"] = multiTenancy.ToString(), + ["ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Test;Integrated Security=True", + ["ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Test;Integrated Security=True" + }; + + if (multiTenancy) + { + configData["Tenants:tenant1:ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True"; + configData["Tenants:tenant1:ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True"; + configData["Tenants:tenant2:ConnectionStrings:EdFi_Admin"] = "Data Source=test;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True"; + configData["Tenants:tenant2:ConnectionStrings:EdFi_Security"] = "Data Source=test;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True"; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(configData) + .Build(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index af4bdb7e7..0eb6d38b0 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -69,7 +69,6 @@ { context.Response.ContentType = "application/json"; - // Set HTTP status code based on health check results // 200 OK if all are healthy, 503 Service Unavailable if any are unhealthy context.Response.StatusCode = report.Status == HealthStatus.Healthy ? 200 : 503; @@ -80,10 +79,8 @@ { name = x.Key, status = x.Value.Status.ToString(), - exception = x.Value.Exception?.Message, - duration = x.Value.Duration.ToString() - }), - duration = report.TotalDuration.ToString() + exception = x.Value.Exception?.Message + }) }; await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(response)); From eb450185e70634f082e50d3606c0a40b16e35549 Mon Sep 17 00:00:00 2001 From: Juan Agudelo Date: Thu, 16 Oct 2025 18:39:44 -0500 Subject: [PATCH 3/5] Fix the Copilot comments --- Application/EdFi.Ods.AdminApi/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 0eb6d38b0..976662bf7 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -3,6 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Net; using EdFi.Ods.AdminApi.Common.Constants; using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; @@ -70,7 +71,7 @@ context.Response.ContentType = "application/json"; // 200 OK if all are healthy, 503 Service Unavailable if any are unhealthy - context.Response.StatusCode = report.Status == HealthStatus.Healthy ? 200 : 503; + context.Response.StatusCode = report.Status == HealthStatus.Unhealthy ? (int)HttpStatusCode.ServiceUnavailable : (int)HttpStatusCode.OK; var response = new { @@ -78,12 +79,11 @@ checks = report.Entries.Select(x => new { name = x.Key, - status = x.Value.Status.ToString(), - exception = x.Value.Exception?.Message + status = x.Value.Status.ToString() }) }; - await context.Response.WriteAsync(System.Text.Json.JsonSerializer.Serialize(response)); + await context.Response.WriteAsJsonAsync(response); } }); From 758d6f6750f132b5b9f0b31d0b70efef80d01de6 Mon Sep 17 00:00:00 2001 From: Juan Agudelo Date: Mon, 20 Oct 2025 16:00:14 -0500 Subject: [PATCH 4/5] Add the tag Databases to the health checker and return a similar response as in DMS --- .../Helpers/HealthCheckServiceExtensions.cs | 8 ++++---- .../Helpers/HealthCheckServiceExtensions.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs index 0ca00c138..ba81eefff 100644 --- a/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.V1/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -34,13 +34,13 @@ public static IServiceCollection AddHealthCheck( if (isSqlServer) { - hcBuilder.AddSqlServer(adminConnectionString, name: "EdFi_Admin"); - hcBuilder.AddSqlServer(securityConnectionString, name: "EdFi_Security"); + hcBuilder.AddSqlServer(adminConnectionString, name: "EdFi_Admin", tags: ["Databases"]); + hcBuilder.AddSqlServer(securityConnectionString, name: "EdFi_Security", tags: ["Databases"]); } else { - hcBuilder.AddNpgSql(adminConnectionString, name: "EdFi_Admin"); - hcBuilder.AddNpgSql(securityConnectionString, name: "EdFi_Security"); + hcBuilder.AddNpgSql(adminConnectionString, name: "EdFi_Admin", tags: ["Databases"]); + hcBuilder.AddNpgSql(securityConnectionString, name: "EdFi_Security", tags: ["Databases"]); } return services; diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs index 5aaef954c..40d433983 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/HealthCheckServiceExtensions.cs @@ -70,11 +70,11 @@ bool isSqlServer if (isSqlServer) { - hcBuilder.AddSqlServer(connectionString.Value, name: healthCheckName); + hcBuilder.AddSqlServer(connectionString.Value, name: healthCheckName, tags: ["Databases"]); } else { - hcBuilder.AddNpgSql(connectionString.Value, name: healthCheckName); + hcBuilder.AddNpgSql(connectionString.Value, name: healthCheckName, tags: ["Databases"]); } } } From f8efb9de3d40f6d0de91fe57778f45ecbc95b5ee Mon Sep 17 00:00:00 2001 From: Juan Agudelo Date: Mon, 20 Oct 2025 16:13:48 -0500 Subject: [PATCH 5/5] Add the response to the health service --- Application/EdFi.Ods.AdminApi/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 976662bf7..5d544d3b8 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -75,11 +75,11 @@ var response = new { - status = report.Status.ToString(), - checks = report.Entries.Select(x => new + Status = report.Status.ToString(), + Results = report.Entries.GroupBy(x => x.Value.Tags.FirstOrDefault()).Select(x => new { - name = x.Key, - status = x.Value.Status.ToString() + Name = x.Key, + Status = x.Min(y => y.Value.Status).ToString() }) };