New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I inject application dependencies into IHealthCheck implementations? #655

Open
rexcfnghk opened this Issue Jan 15, 2019 · 1 comment

Comments

Projects
None yet
2 participants
@rexcfnghk
Copy link

rexcfnghk commented Jan 15, 2019

I have an IHealthCheck implementation that connects to a SQL database to make sure the connection is fine. However, the connection string used is looked up through an HTTP Restful call:

public class SqlHealthCheck : IHealthCheck
{
    private readonly ISqlConnectionStringProvider _connectionStringProvider;

    public SqlHealthCheck(ISqlConnectionStringProvider connectionStringProvider)
        => _connectionStringProvider = connectionStringProvider;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken)
    {
        var connectionString =
            await _connectionStringProvider.GetConnectionStringAsync()
                                           .ConfigureAwait(false);
        using (var connection = new SqlConnection(connectionString))
        {
            // ...
        }
    }
}

I have registered a concrete implementation of ISqlConnectionStringProvider with Simple Injector but how can I tell the framework DI container to inject that implementation when activating the SqlHealthCheck?

AFAIK, the framework container (whether .NET Core/ASP.NET Core) only provides UseHealthCheck and AddHealthCheck APIs and does not provide a IHealthCheckFactory hook to let me plug a SimpleInjectorHealthCheckFactory to resolve a HealthCheck through Simple Injector.

The only solution I can think of is to implement an adapter over the IHealthCheck interface:

public sealed class SimpleInjectorActivatedHealthCheck<THealthCheck> : IHealthCheck
    where THealthCheck : class, IHealthCheck
{
    private readonly Container _container;

    public SimpleInjectorActivatedHealthCheck(Container container)
        => _container = container;

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
        => _container.GetInstance<THealthCheck>()
            .CheckHealthAsync(context, cancellationToken);
}

This allows registrations like services.AddCheck<SimpleInjectorActivatedHealthCheck<SqlHealthCheck>>("SqlServer") and dependencies of it should be resolved via Simple Injector. Is that the recommended way?

@dotnetjunkie

This comment has been minimized.

Copy link
Collaborator

dotnetjunkie commented Jan 15, 2019

I'm afraid you are right about the very limited API that the health-check extensions provide. It seems to be very tightly coupled to the built-in DI Container:

  • The DefaultHealthCheckService is internal, which makes it hard to supply scoping at that level
  • The HealthCheckRegistration does not accept a delegate that produces a Task<HealthCheckResult>. This disallows applying scoping and forwarding to Simple Injector at that point (except when using a proxy implementation)

This is a problem that keeps popping up, even though Microsoft promised to provide good extension points.

Because of its current design, I think you are on the right track with your own adapter implementation. I would suggest tweeking the implementation to the following:

public sealed class SimpleInjectorHealthCheckAdapter<THealthCheck> : IHealthCheck
    where THealthCheck : class, IHealthCheck
{
    private readonly Container _container;
    public SimpleInjectorHealthCheckAdapter(Container container)
        => _container = container;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        using (AsyncScopedLifestyle.BeginScope(_container)
        {
            var check = _container.GetInstance<THealthCheck>();
            return await check.CheckHealthAsync(context, cancellationToken);
        }
    }
}

This implementation wraps the container's GetInstance call in a Simple Injector scope. This is required, as health checks run on a background thread, triggered by a timer. At that point in time there will not be an active Simple Injector scope, which might be required when you register services using Lifestyle.Scoped.

For simplicity, you can add the following extension method:

public static IHealthChecksBuilder AddCheck<T>(
    this IHealthChecksBuilder builder, Container container, string name,
    HealthStatus? failureStatus = null, IEnumerable<string> tags = null)
    where T : class, IHealthCheck
{
    container.Register<T>(Lifestyle.Transient);
    
    builder.AddCheck(name, 
        new SimpleInjectorHealthCheckAdapter<T>(container), // this adapter is a singleton
        failureStatus, tags);
}

This allows you to apply health checks the same way as you're already used to:

services
    .AddHealthChecks()
    .AddCheck<SqlHealthCheck>(container, "sql");

Note that your adapter enables the possibility of defining your own abstraction over health checks. This allows you to place your health checks implementations in an assembly that does not reference Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment