ServiceConnect.HealthChecks
Overview
Section titled “Overview”ServiceConnect.HealthChecks is the optional package that ships health-check classes for Microsoft.Extensions.Diagnostics.HealthChecks. It contains three sealed IHealthCheck implementations and three matching extension methods on IHealthChecksBuilder. The package is transport-agnostic — it depends only on ServiceConnect.Interfaces. A future transport that implements IBus, IConsumer, and IProducer works with these checks unchanged.
Each check is O(1), allocation-light, and side-effect-free: it inspects last-known in-process state through a public interface property (IBus.IsConsuming, IConsumer.IsConnected, IProducer.IsHealthy). No broker channels are opened per probe, no AMQP round-trips are issued. Each check honours the supplied CancellationToken — probes cancelled by the health-check framework surface as OperationCanceledException rather than returning a stale result.
See Observability — Health checks for the conceptual walk-through, K8s liveness/readiness wiring, and the producer-lazy-connect caveat.
Installation
Section titled “Installation”dotnet add package ServiceConnect.HealthChecksThen call any combination of AddServiceConnectBus, AddServiceConnectConsumer, and AddServiceConnectProducer on services.AddHealthChecks() — pick the methods that match what your host actually does.
HealthChecksBuilderExtensions
Section titled “HealthChecksBuilderExtensions”Static class containing the three registration extension methods. Each registers exactly one HealthCheckRegistration; tags, failure status, name, and timeout flow through verbatim.
AddServiceConnectBus
Section titled “AddServiceConnectBus”// Default — resolves IBus via ActivatorUtilities from DIpublic static IHealthChecksBuilder AddServiceConnectBus( this IHealthChecksBuilder builder, string name = "serviceconnect-bus", HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Factory overload — useful for multi-bus hostspublic static IHealthChecksBuilder AddServiceConnectBus( this IHealthChecksBuilder builder, string name, Func<IServiceProvider, IBus> busFactory, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Keyed-services convenience overloadpublic static IHealthChecksBuilder AddServiceConnectBus( this IHealthChecksBuilder builder, string name, object serviceKey, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Configurable recovery-grace window + optional TimeProvider and consumer factorypublic static IHealthChecksBuilder AddServiceConnectBus( this IHealthChecksBuilder builder, string name, Func<IServiceProvider, IBus> busFactory, TimeSpan recoveryGraceWindow, TimeProvider? timeProvider = null, Func<IServiceProvider, IConsumer?>? consumerFactory = null, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);Registers BusConsumingHealthCheck. The default overload resolves IBus via GetRequiredService<IBus>() inside a PerProviderCache<BusConsumingHealthCheck> factory, so the same probe instance is reused for the lifetime of the IServiceProvider. Use the factory or keyed-services overload when multiple named bus instances are registered in the same DI container. The fourth overload is the one to reach for from tests (with FakeTimeProvider) or when the default 30s recovery-grace window does not match the host’s broker-recovery characteristics; threading the optional consumerFactory through enables broker-cancelled short-circuit for hosts that observe IConsumer separately from IBus.
Parameters (default overload)
builder— theIHealthChecksBuilderreturned byservices.AddHealthChecks().name— registration name. Defaults to"serviceconnect-bus".failureStatus— status to return when the bus is not consuming. Defaults toUnhealthy.tags— tags attached to the registration (use these withMapHealthCheckspredicates to split liveness/readiness endpoints).timeout— per-check timeout. Defaults to none — the check is O(1) and does not need one.
AddServiceConnectConsumer
Section titled “AddServiceConnectConsumer”// Default — resolves IConsumer via ActivatorUtilities from DIpublic static IHealthChecksBuilder AddServiceConnectConsumer( this IHealthChecksBuilder builder, string name = "serviceconnect-consumer", HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Factory overloadpublic static IHealthChecksBuilder AddServiceConnectConsumer( this IHealthChecksBuilder builder, string name, Func<IServiceProvider, IConsumer> consumerFactory, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Keyed-services convenience overloadpublic static IHealthChecksBuilder AddServiceConnectConsumer( this IHealthChecksBuilder builder, string name, object serviceKey, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Configurable recovery-grace window + optional TimeProviderpublic static IHealthChecksBuilder AddServiceConnectConsumer( this IHealthChecksBuilder builder, string name, Func<IServiceProvider, IConsumer> consumerFactory, TimeSpan recoveryGraceWindow, TimeProvider? timeProvider = null, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);Registers ConsumerConnectionHealthCheck. Same factory pattern as AddServiceConnectBus. The default name is "serviceconnect-consumer". Skip this on publish-only hosts. The fourth overload accepts an explicit recovery-grace window and optional TimeProvider; use it from tests (FakeTimeProvider) or when the default 30s grace does not match the host’s broker-recovery characteristics.
AddServiceConnectProducer
Section titled “AddServiceConnectProducer”// Default — resolves IProducer via ActivatorUtilities from DIpublic static IHealthChecksBuilder AddServiceConnectProducer( this IHealthChecksBuilder builder, string name = "serviceconnect-producer", HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Factory overloadpublic static IHealthChecksBuilder AddServiceConnectProducer( this IHealthChecksBuilder builder, string name, Func<IServiceProvider, IProducer> producerFactory, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);
// Keyed-services convenience overloadpublic static IHealthChecksBuilder AddServiceConnectProducer( this IHealthChecksBuilder builder, string name, object serviceKey, HealthStatus failureStatus = HealthStatus.Unhealthy, IEnumerable<string>? tags = null, TimeSpan? timeout = null);Registers ProducerConnectionHealthCheck. The default name is "serviceconnect-producer". Skip this on consume-only hosts.
Health-check classes
Section titled “Health-check classes”Each class is public sealed, takes its observed interface via constructor with ArgumentNullException.ThrowIfNull, and returns Task.FromResult<HealthCheckResult> from a synchronous method body.
BusConsumingHealthCheck
Section titled “BusConsumingHealthCheck”public sealed class BusConsumingHealthCheck : IHealthCheck{ // Default 30s recovery grace; system TimeProvider; no consumer // supplied (no broker-cancelled short-circuit beyond IBus.IsCancelledByBroker). public BusConsumingHealthCheck(IBus bus);
// Configurable recovery-grace window and optional consumer for broker-cancelled // short-circuit. Pass TimeSpan.Zero to disable grace (suits liveness probes). public BusConsumingHealthCheck( IBus bus, IConsumer? consumer, TimeSpan recoveryGraceWindow, TimeProvider timeProvider);
public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);}The check evaluates four observed states in order; the first match wins:
IBus.IsConsuming == true— returnsHealthy("Bus is consuming.")and stamps the last-Healthy timestamp used by the recovery-grace path.- Broker-cancelled — when the supplied
IConsumer.IsCancelledByBrokeristrue, OR (parameterless-ctor path) whenIBus.IsCancelledByBrokeristrue. Returnsnew HealthCheckResult(failureStatus, "Bus is not consuming (broker cancelled the consumer)."). Bypasses the grace window — abasic.cancelevent (queue deleted, policy expired, mirror promoted) is terminal until the consumer restarts. - Stopped or disposed — when
IBus.IsStoppedistrue. Returnsnew HealthCheckResult(failureStatus, "Bus is not consuming (stopped or disposed)."). Also bypasses grace; there is no reconnect to wait for. - Within the recovery-grace window — when the bus has been observed Healthy at some prior point AND the time since that observation is less than
recoveryGraceWindow, returnsHealthy("Bus is not consuming, but within recovery grace (…)."). This is the readiness-friendly path: a momentary broker disconnect (auto-recovery, network blip, broker bounce) does not crash-loop pods wired on liveness probes.
If none of the above matches, the check returns new HealthCheckResult(failureStatus, "Bus is not consuming.") where failureStatus = context.Registration?.FailureStatus ?? HealthStatus.Unhealthy.
The recovery-grace window defaults to 30 seconds when the parameterless constructor is used. Pass TimeSpan.Zero to the 4-arg ctor to disable grace (suits liveness probes that must flip immediately on disconnect). The TimeProvider parameter is injectable for tests.
The grace state is keyed on the IBus instance via a ConditionalWeakTable, so per-probe re-allocations performed by HealthCheckService do not reset the last-Healthy timestamp. A never-Healthy probe (the first-probe case) always reports Unhealthy regardless of the grace window — there is no equivalent to IProducer.HasAttemptedConnection for consumers; a never-Healthy consumer is genuinely unhealthy, not lazy.
ConsumerConnectionHealthCheck
Section titled “ConsumerConnectionHealthCheck”public sealed class ConsumerConnectionHealthCheck : IHealthCheck{ // Default 30s recovery grace; system TimeProvider. public ConsumerConnectionHealthCheck(IConsumer consumer);
// Configurable recovery-grace window and injectable TimeProvider for tests. public ConsumerConnectionHealthCheck( IConsumer consumer, TimeSpan recoveryGraceWindow, TimeProvider timeProvider);
public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);}The check evaluates four observed states in order; the first match wins:
- Broker-cancelled — when
IConsumer.IsCancelledByBrokeristrue. Returnsnew HealthCheckResult(failureStatus, "Consumer connection is closed (broker cancelled the consumer)."). Bypasses the grace window —basic.cancel(queue deleted, policy expired, mirror promoted) is a permanent failure;IConsumer.IsConnectedcan remaintruewhile deliveries have stopped, so this check runs first. - Connected — when
IConsumer.IsConnectedistrue. ReturnsHealthy("Consumer connection is open.")and stamps the last-Healthy timestamp used by the recovery-grace path. - Stopped or disposed — when
IConsumer.IsStoppedistrue. Returnsnew HealthCheckResult(failureStatus, "Consumer connection is closed (stopped or disposed)."). Also bypasses grace; an intentional shutdown has no reconnect to wait for. - Within the recovery-grace window — when the consumer has been observed Healthy at some prior point AND the time since that observation is less than
recoveryGraceWindow, returnsHealthy("Consumer connection is closed, but within recovery grace (…)."). This is the readiness-friendly path: a momentary broker disconnect (auto-recovery, network blip) does not crash-loop pods wired on liveness probes.
If none of the above matches, the check returns new HealthCheckResult(failureStatus, "Consumer connection is closed.") where failureStatus = context.Registration?.FailureStatus ?? HealthStatus.Unhealthy.
The recovery-grace window defaults to 30 seconds when the single-argument constructor is used. Pass TimeSpan.Zero to the 3-arg constructor to disable grace. The TimeProvider parameter is injectable for tests. The grace state is keyed on the IConsumer instance via a ConditionalWeakTable, so per-probe re-allocations performed by HealthCheckService do not reset the last-Healthy timestamp. A never-Healthy probe always reports Unhealthy regardless of the grace window.
IConsumer.IsConnected reflects the transport client’s last-known view of the broker connection. When the broker drops, the client raises a shutdown event and the property flips to false; on reconnect, it flips back. There is a millisecond-scale gap between the underlying connection drop and the event being observed — a probe inside that window can still see Healthy. This is the same gap any in-process check has, regardless of implementation.
ProducerConnectionHealthCheck
Section titled “ProducerConnectionHealthCheck”public sealed class ProducerConnectionHealthCheck : IHealthCheck{ public ProducerConnectionHealthCheck(IProducer producer); public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default);}Calls IProducer.GetHealthSnapshot() so IsHealthy and HasAttemptedConnection are read atomically — reading the two properties separately is racy because the producer’s IsHealthy can flip false on a transient broker drop in the same instant HasAttemptedConnection is observed true, surfacing a false-negative Unhealthy. The snapshot returned by GetHealthSnapshot() captures the pair under the producer’s internal lock and is evaluated as follows:
snapshot.IsHealthy→ returnsHealthy("Producer connection is open.").!snapshot.HasAttemptedConnection→ returnsHealthy("Producer has not yet attempted connection (lazy)."). This is the state before any publish or send has been issued; treating it as unhealthy would crash-loop pods unnecessarily.- Otherwise (
HasAttemptedConnection && !IsHealthy) → returns the failure result with statusfailureStatusand"Producer connection is closed.".
The check also calls cancellationToken.ThrowIfCancellationRequested() at entry, so probes cancelled by the health-check framework surface as OperationCanceledException.
Single-bus registration
Section titled “Single-bus registration”using ServiceConnect.HealthChecks;
services.AddServiceConnect(builder =>{ builder.UseRabbitMQ(opts => opts.Host = "rabbit");});
services.AddHealthChecks() .AddServiceConnectBus(tags: ["live"]) .AddServiceConnectConsumer(tags: ["ready"]) .AddServiceConnectProducer(tags: ["ready"]);
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });Multi-bus registration
Section titled “Multi-bus registration”When multiple named bus instances are registered in the same DI container, use the factory or keyed-services overloads to bind each check to its specific instance:
// Keyed-services approach — register two named busesservices.AddKeyedSingleton<IBus>("orders", ordersFactory);services.AddKeyedSingleton<IBus>("notifications", notificationsFactory);
services.AddHealthChecks() .AddServiceConnectBus(name: "bus-orders", serviceKey: "orders", tags: ["live"]) .AddServiceConnectBus(name: "bus-notifications", serviceKey: "notifications", tags: ["live"]);
// Factory approach — resolve manually when keyed services aren't availableservices.AddHealthChecks() .AddServiceConnectProducer( name: "producer-orders", producerFactory: sp => sp.GetRequiredKeyedService<IProducer>("orders"), tags: ["ready"]);Topology choices
Section titled “Topology choices”- Consume-only — drop
AddServiceConnectProducer. - Publish-only — drop both
AddServiceConnectConsumerandAddServiceConnectBus(a host that never starts consuming would reportIsConsumingpermanentlyfalse). - Both — call all three.
Custom checks
Section titled “Custom checks”The shipped check classes are sealed. If you need a different shape — a custom predicate over multiple bus state pieces, a different failure-status mapping, integration with a non-Microsoft.Extensions.Diagnostics.HealthChecks framework — implement IHealthCheck directly against IBus, IConsumer, or IProducer. The shipped classes are short enough to copy as a starting point.
See also
Section titled “See also”- Observability — Health checks — concept and wiring walk-through
- Hosting — how the bus interacts with the .NET hosted-service lifecycle
AddServiceConnect— registration entry point