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.");
}
}