feat(health.ef): generic DatabaseHealthCheck<TContext>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
namespace ZB.MOM.WW.Health.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Health check that verifies database reachability through an EF Core <typeparamref name="TContext"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The default probe calls
|
||||
/// <see cref="Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.CanConnectAsync(CancellationToken)"/>
|
||||
/// (the ScadaBridge pattern): <see cref="HealthStatus.Healthy"/> when it returns <c>true</c>,
|
||||
/// <see cref="HealthStatus.Unhealthy"/> when it returns <c>false</c> or throws. Supplying
|
||||
/// <see cref="DatabaseHealthCheckOptions{TContext}.ProbeQuery"/> swaps in a stricter query-based probe
|
||||
/// (the OtOpcUa "query <c>Deployments</c>" pattern): the result is <see cref="HealthStatus.Healthy"/>
|
||||
/// unless the delegate throws, in which case it is <see cref="HealthStatus.Unhealthy"/>. No exception
|
||||
/// escapes <see cref="CheckHealthAsync"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The context is resolved from the application <see cref="IServiceProvider"/>: an
|
||||
/// <see cref="IDbContextFactory{TContext}"/> is used when one is registered (each probe gets a fresh,
|
||||
/// disposed context); otherwise a scoped <typeparamref name="TContext"/> is resolved from a new DI
|
||||
/// scope. Recommended registration tag: <c>ZbHealthTags.Ready</c> (applied by the registrant).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="TContext">The EF Core <see cref="DbContext"/> to probe.</typeparam>
|
||||
public sealed class DatabaseHealthCheck<TContext> : IHealthCheck
|
||||
where TContext : DbContext
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly DatabaseHealthCheckOptions<TContext> _options;
|
||||
|
||||
/// <summary>Initializes a new <see cref="DatabaseHealthCheck{TContext}"/>.</summary>
|
||||
/// <param name="serviceProvider">
|
||||
/// Application service provider used to resolve <typeparamref name="TContext"/> — preferring a
|
||||
/// registered <see cref="IDbContextFactory{TContext}"/>, otherwise a scoped instance.
|
||||
/// </param>
|
||||
/// <param name="options">
|
||||
/// Probe override and timeout. When <c>null</c>, the default <c>CanConnectAsync</c> probe with a
|
||||
/// 10 s timeout is used.
|
||||
/// </param>
|
||||
public DatabaseHealthCheck(
|
||||
IServiceProvider serviceProvider,
|
||||
DatabaseHealthCheckOptions<TContext>? options = null)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_options = options ?? new DatabaseHealthCheckOptions<TContext>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HealthCheckResult> 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<HealthCheckResult> ProbeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var factory = _serviceProvider.GetService<IDbContextFactory<TContext>>();
|
||||
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<TContext>();
|
||||
return await RunProbeAsync(scoped, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<HealthCheckResult> 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.");
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ZB.MOM.WW.Health.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Options for <see cref="DatabaseHealthCheck{TContext}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TContext">The EF Core <see cref="DbContext"/> the probe runs against.</typeparam>
|
||||
public sealed class DatabaseHealthCheckOptions<TContext>
|
||||
where TContext : DbContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional query-based probe that overrides the default
|
||||
/// <see cref="Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.CanConnectAsync(CancellationToken)"/>
|
||||
/// reachability check with stricter, query-level validation (the OtOpcUa "query <c>Deployments</c>"
|
||||
/// pattern). Throw to signal failure; return normally to signal success.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example: <c>(db, ct) => db.Deployments.AsNoTracking().Take(1).ToListAsync(ct)</c>.
|
||||
/// When <c>null</c>, the default <c>CanConnectAsync</c> probe is used.
|
||||
/// </remarks>
|
||||
public Func<TContext, CancellationToken, Task>? ProbeQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time the probe may run before it is treated as a failure. Defaults to 10 seconds.
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
Reference in New Issue
Block a user