| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Diagnostics.HealthChecks; |
| | | 3 | | using Microsoft.Extensions.Options; |
| | | 4 | | using ProjectTemplate.Infrastructure.Data; |
| | | 5 | | using ProjectTemplate.Web.Options; |
| | | 6 | | |
| | | 7 | | namespace ProjectTemplate.Web.HealthChecks; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Reports whether the application database accepts connections. |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks> |
| | | 13 | | /// Registered with the <see cref="ApplicationHealthCheckTags.Ready"/> tag so an instance that cannot reach its database |
| | | 14 | | /// is removed from load balancing. The result description never includes connection strings or provider exception |
| | | 15 | | /// text. For SQLite, a database file that has not been created yet is reported as unavailable. When |
| | | 16 | | /// <see cref="ApplicationHealthCheckOptions.DatabaseReadinessCheckEnabled"/> is <see langword="false"/>, the check |
| | | 17 | | /// reports healthy without contacting the database. |
| | | 18 | | /// </remarks> |
| | 8 | 19 | | public sealed class ApplicationDatabaseHealthCheck( |
| | 8 | 20 | | IServiceScopeFactory scopeFactory, |
| | 8 | 21 | | IOptionsMonitor<ApplicationHealthCheckOptions> options) |
| | | 22 | | : IHealthCheck |
| | | 23 | | { |
| | | 24 | | internal const string AvailableDescription = "The application database is reachable."; |
| | | 25 | | |
| | | 26 | | internal const string UnavailableDescription = "The application database is not reachable."; |
| | | 27 | | |
| | | 28 | | internal const string DisabledDescription = "The application database readiness check is disabled."; |
| | | 29 | | |
| | 10 | 30 | | private readonly IServiceScopeFactory _scopeFactory = |
| | 10 | 31 | | scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); |
| | 9 | 32 | | private readonly IOptionsMonitor<ApplicationHealthCheckOptions> _options = |
| | 9 | 33 | | options ?? throw new ArgumentNullException(nameof(options)); |
| | | 34 | | |
| | | 35 | | /// <summary> |
| | | 36 | | /// Checks whether the application database accepts connections. |
| | | 37 | | /// </summary> |
| | | 38 | | /// <param name="context">The health check context.</param> |
| | | 39 | | /// <param name="cancellationToken">A token that cancels the check.</param> |
| | | 40 | | /// <returns>The database health result.</returns> |
| | | 41 | | public async Task<HealthCheckResult> CheckHealthAsync( |
| | | 42 | | HealthCheckContext context, |
| | | 43 | | CancellationToken cancellationToken = default) |
| | | 44 | | { |
| | 8 | 45 | | ArgumentNullException.ThrowIfNull(context); |
| | 8 | 46 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 47 | | |
| | 8 | 48 | | if (!_options.CurrentValue.DatabaseReadinessCheckEnabled) |
| | | 49 | | { |
| | 6 | 50 | | return HealthCheckResult.Healthy(DisabledDescription); |
| | | 51 | | } |
| | | 52 | | |
| | 2 | 53 | | using IServiceScope scope = _scopeFactory.CreateScope(); |
| | 2 | 54 | | ApplicationDbContext dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); |
| | | 55 | | |
| | 2 | 56 | | if (IsMissingSqliteDatabase(dbContext)) |
| | | 57 | | { |
| | 1 | 58 | | return new HealthCheckResult(context.Registration.FailureStatus, UnavailableDescription); |
| | | 59 | | } |
| | | 60 | | |
| | 1 | 61 | | bool canConnect = await dbContext.Database |
| | 1 | 62 | | .CanConnectAsync(cancellationToken) |
| | 1 | 63 | | .ConfigureAwait(false); |
| | | 64 | | |
| | 1 | 65 | | return canConnect |
| | 1 | 66 | | ? HealthCheckResult.Healthy(AvailableDescription) |
| | 1 | 67 | | : new HealthCheckResult(context.Registration.FailureStatus, UnavailableDescription); |
| | 8 | 68 | | } |
| | | 69 | | |
| | | 70 | | private static bool IsMissingSqliteDatabase(ApplicationDbContext dbContext) |
| | | 71 | | { |
| | 2 | 72 | | if (!string.Equals( |
| | 2 | 73 | | dbContext.Database.ProviderName, |
| | 2 | 74 | | "Microsoft.EntityFrameworkCore.Sqlite", |
| | 2 | 75 | | StringComparison.Ordinal)) |
| | | 76 | | { |
| | 0 | 77 | | return false; |
| | | 78 | | } |
| | | 79 | | |
| | 2 | 80 | | string dataSource = dbContext.Database.GetDbConnection().DataSource; |
| | | 81 | | |
| | 2 | 82 | | return !string.IsNullOrWhiteSpace(dataSource) && |
| | 2 | 83 | | !string.Equals(dataSource, ":memory:", StringComparison.OrdinalIgnoreCase) && !File.Exists(dataSource); |
| | | 84 | | } |
| | | 85 | | } |