diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs new file mode 100644 index 0000000..172c041 --- /dev/null +++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs @@ -0,0 +1,102 @@ +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 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 + { + return await ProbeAsync(timeoutCts.Token).ConfigureAwait(false); + } + 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."); + } +} diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheckOptions.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheckOptions.cs new file mode 100644 index 0000000..66bb471 --- /dev/null +++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheckOptions.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; + +namespace ZB.MOM.WW.Health.EntityFrameworkCore; + +/// +/// Options for . +/// +/// The EF Core the probe runs against. +public sealed class DatabaseHealthCheckOptions + where TContext : DbContext +{ + /// + /// Optional query-based probe that overrides the default + /// + /// reachability check with stricter, query-level validation (the OtOpcUa "query Deployments" + /// pattern). Throw to signal failure; return normally to signal success. + /// + /// + /// Example: (db, ct) => db.Deployments.AsNoTracking().Take(1).ToListAsync(ct). + /// When null, the default CanConnectAsync probe is used. + /// + public Func? ProbeQuery { get; set; } + + /// + /// Maximum time the probe may run before it is treated as a failure. Defaults to 10 seconds. + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10); +}