using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; namespace ZB.MOM.WW.Health.EntityFrameworkCore; /// /// Health check that verifies database reachability through an EF Core . /// /// /// /// The default probe calls /// /// (the ScadaBridge pattern): when it returns true, /// when it returns false or throws. Supplying /// swaps in a stricter query-based probe /// (the OtOpcUa "query Deployments" pattern): the result is /// unless the delegate throws, in which case it is . No exception /// escapes . /// /// /// The context is resolved from the application : an /// is used when one is registered (each probe gets a fresh, /// disposed context); otherwise a scoped is resolved from a new DI /// scope. Recommended registration tag: ZbHealthTags.Ready (applied by the registrant). /// /// /// The scoped-resolution path is safe for AddDbContextPool: disposing the /// returns the pooled context to the pool rather than destroying it, /// so no pooled instance is prematurely discarded. /// /// /// The EF Core to probe. public sealed class DatabaseHealthCheck : IHealthCheck where TContext : DbContext { private readonly IServiceProvider _serviceProvider; private readonly DatabaseHealthCheckOptions _options; /// Initializes a new . /// /// Application service provider used to resolve — preferring a /// registered , otherwise a scoped instance. /// /// /// Probe override and timeout. When null, the default CanConnectAsync probe with a /// 10 s timeout is used. /// public DatabaseHealthCheck( IServiceProvider serviceProvider, DatabaseHealthCheckOptions? options = null) { _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _options = options ?? new DatabaseHealthCheckOptions(); } /// public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(_options.Timeout); try { var result = await ProbeAsync(timeoutCts.Token).ConfigureAwait(false); // Eagerly release the pending timer on the happy path so the OS timer // resource is not held for the full timeout duration. timeoutCts.CancelAfter(Timeout.InfiniteTimeSpan); return result; } catch (OperationCanceledException ex) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) { return HealthCheckResult.Unhealthy($"Database probe timed out after {_options.Timeout}.", ex); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Database connection failed.", ex); } } private async Task ProbeAsync(CancellationToken cancellationToken) { var factory = _serviceProvider.GetService>(); if (factory is not null) { await using var db = await factory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); return await RunProbeAsync(db, cancellationToken).ConfigureAwait(false); } await using var scope = _serviceProvider.CreateAsyncScope(); var scoped = scope.ServiceProvider.GetRequiredService(); return await RunProbeAsync(scoped, cancellationToken).ConfigureAwait(false); } private async Task RunProbeAsync(TContext db, CancellationToken cancellationToken) { if (_options.ProbeQuery is { } probeQuery) { await probeQuery(db, cancellationToken).ConfigureAwait(false); return HealthCheckResult.Healthy("Database query probe succeeded."); } var canConnect = await db.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false); return canConnect ? HealthCheckResult.Healthy("Database connection is available.") : HealthCheckResult.Unhealthy("Database connection failed."); } }