diff --git a/build/dependencies.props b/build/dependencies.props
index 0ae59c4402..34ae14f945 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -31,6 +31,7 @@
3.0.0-alpha1-10352
3.0.0-alpha1-10352
3.0.0-alpha1-10352
+ 3.0.0-alpha1-10352
2.0.9
2.1.2
2.2.0-preview1-26618-02
diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs
index f82f4c54ef..58b6c8d157 100644
--- a/samples/HealthChecksSample/DBHealthStartup.cs
+++ b/samples/HealthChecksSample/DBHealthStartup.cs
@@ -21,7 +21,6 @@ namespace HealthChecksSample
{
// Registers required services for health checks
services.AddHealthChecks()
-
// Add a health check for a SQL database
.AddCheck(new SqlConnectionHealthCheck("MyDatabase", Configuration["ConnectionStrings:DefaultConnection"]));
}
diff --git a/samples/HealthChecksSample/LivenessProbeStartup.cs b/samples/HealthChecksSample/LivenessProbeStartup.cs
index f5eabc6696..aa644d231f 100644
--- a/samples/HealthChecksSample/LivenessProbeStartup.cs
+++ b/samples/HealthChecksSample/LivenessProbeStartup.cs
@@ -17,7 +17,6 @@ namespace HealthChecksSample
// Registers required services for health checks
services
.AddHealthChecks()
- .AddCheck("identity", () => Task.FromResult(HealthCheckResult.Healthy()))
.AddCheck(new SlowDependencyHealthCheck());
}
@@ -53,11 +52,8 @@ namespace HealthChecksSample
// The liveness check uses an 'identity' health check that always returns healthy
app.UseHealthChecks("/health/live", new HealthCheckOptions()
{
- // Filters the set of health checks run by this middleware
- HealthCheckNames =
- {
- "identity",
- },
+ // Exclude all checks, just return a 200.
+ Predicate = (check) => false,
});
app.Run(async (context) =>
diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs
index d24e0b38c1..425b0b3f1a 100644
--- a/samples/HealthChecksSample/Program.cs
+++ b/samples/HealthChecksSample/Program.cs
@@ -15,7 +15,7 @@ namespace HealthChecksSample
{
_scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- { "", typeof(BasicStartup) },
+ { "", typeof(DBHealthStartup) },
{ "basic", typeof(BasicStartup) },
{ "writer", typeof(CustomWriterStartup) },
{ "liveness", typeof(LivenessProbeStartup) },
@@ -48,6 +48,8 @@ namespace HealthChecksSample
.UseConfiguration(config)
.ConfigureLogging(builder =>
{
+ builder.SetMinimumLevel(LogLevel.Trace);
+ builder.AddConfiguration(config);
builder.AddConsole();
})
.UseKestrel()
diff --git a/samples/HealthChecksSample/appsettings.json b/samples/HealthChecksSample/appsettings.json
index 77b5f890a4..21b2dcdbfd 100644
--- a/samples/HealthChecksSample/appsettings.json
+++ b/samples/HealthChecksSample/appsettings.json
@@ -1,5 +1,13 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=HealthCheckSample;Trusted_Connection=True;MultipleActiveResultSets=true;ConnectRetryCount=0"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug"
+ },
+ "Console": {
+ "IncludeScopes": "true"
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs
index 3a220f2595..20ec5e356e 100644
--- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs
+++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckMiddleware.cs
@@ -16,7 +16,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
private readonly RequestDelegate _next;
private readonly HealthCheckOptions _healthCheckOptions;
private readonly IHealthCheckService _healthCheckService;
- private readonly IHealthCheck[] _checks;
public HealthCheckMiddleware(
RequestDelegate next,
@@ -41,8 +40,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
_next = next;
_healthCheckOptions = healthCheckOptions.Value;
_healthCheckService = healthCheckService;
-
- _checks = FilterHealthChecks(_healthCheckService.Checks, healthCheckOptions.Value.HealthCheckNames);
}
///
@@ -58,7 +55,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
}
// Get results
- var result = await _healthCheckService.CheckHealthAsync(_checks, httpContext.RequestAborted);
+ var result = await _healthCheckService.CheckHealthAsync(_healthCheckOptions.Predicate, httpContext.RequestAborted);
// Map status to response code - this is customizable via options.
if (!_healthCheckOptions.ResultStatusCodes.TryGetValue(result.Status, out var statusCode))
diff --git a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs
index b57374f2ae..7930d25577 100644
--- a/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs
+++ b/src/Microsoft.AspNetCore.Diagnostics.HealthChecks/HealthCheckOptions.cs
@@ -15,15 +15,19 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
public class HealthCheckOptions
{
///
- /// Gets a set of health check names used to filter the set of health checks run.
+ /// Gets or sets a predicate that is used to filter the set of health checks executed.
///
///
- /// If is empty, the will run all
+ /// If is null, the will run all
/// registered health checks - this is the default behavior. To run a subset of health checks,
- /// add the names of the desired health checks.
+ /// provide a function that filters the set of checks.
///
- public ISet HealthCheckNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
+ public Func Predicate { get; set; }
+ ///
+ /// Gets a dictionary mapping the to an HTTP status code applied to the response.
+ /// This property can be used to configure the status codes returned for each status.
+ ///
public IDictionary ResultStatusCodes { get; } = new Dictionary()
{
{ HealthCheckStatus.Healthy, StatusCodes.Status200OK },
diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs
index 35cc0a1c82..111113ea77 100644
--- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs
+++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthCheckService.cs
@@ -6,127 +6,139 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
- ///
- /// Default implementation of .
- ///
- public class HealthCheckService : IHealthCheckService
+ internal class HealthCheckService : IHealthCheckService
{
+ private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger _logger;
- ///
- /// A containing all the health checks registered in the application.
- ///
- ///
- /// The key maps to the property of the health check, and the value is the
- /// instance itself.
- ///
- public IReadOnlyDictionary Checks { get; }
-
- ///
- /// Constructs a from the provided collection of instances.
- ///
- /// The instances that have been registered in the application.
- public HealthCheckService(IEnumerable healthChecks) : this(healthChecks, NullLogger.Instance) { }
-
- ///
- /// Constructs a from the provided collection of instances, and the provided logger.
- ///
- /// The instances that have been registered in the application.
- /// A that can be used to log events that occur during health check operations.
- public HealthCheckService(IEnumerable healthChecks, ILogger logger)
+ public HealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger)
{
- healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(healthChecks));
+ _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- // Scan the list for duplicate names to provide a better error if there are duplicates.
- var names = new HashSet(StringComparer.OrdinalIgnoreCase);
- var duplicates = new List();
- foreach (var check in healthChecks)
+ // We're specifically going out of our way to do this at startup time. We want to make sure you
+ // get any kind of health-check related error as early as possible. Waiting until someone
+ // actually tries to **run** health checks would be real baaaaad.
+ using (var scope = _scopeFactory.CreateScope())
{
- if (!names.Add(check.Name))
- {
- duplicates.Add(check.Name);
- }
- }
-
- if (duplicates.Count > 0)
- {
- throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicates)}", nameof(healthChecks));
- }
-
- Checks = healthChecks.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase);
-
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- foreach (var check in Checks)
- {
- _logger.LogDebug("Health check '{healthCheckName}' has been registered", check.Key);
- }
+ var healthChecks = scope.ServiceProvider.GetRequiredService>();
+ EnsureNoDuplicates(healthChecks);
}
}
- ///
- /// Runs all the health checks in the application and returns the aggregated status.
- ///
- /// A which can be used to cancel the health checks.
- ///
- /// A which will complete when all the health checks have been run,
- /// yielding a containing the results.
- ///
public Task CheckHealthAsync(CancellationToken cancellationToken = default) =>
- CheckHealthAsync(Checks.Values, cancellationToken);
-
- ///
- /// Runs the provided health checks and returns the aggregated status
- ///
- /// The instances to be run.
- /// A which can be used to cancel the health checks.
- ///
- /// A which will complete when all the health checks have been run,
- /// yielding a containing the results.
- ///
- public async Task CheckHealthAsync(IEnumerable checks, CancellationToken cancellationToken = default)
+ CheckHealthAsync(predicate: null, cancellationToken);
+
+ public async Task CheckHealthAsync(
+ Func predicate,
+ CancellationToken cancellationToken = default)
{
- var results = new Dictionary(Checks.Count, StringComparer.OrdinalIgnoreCase);
- foreach (var check in checks)
+ using (var scope = _scopeFactory.CreateScope())
{
- cancellationToken.ThrowIfCancellationRequested();
- // If the health check does things like make Database queries using EF or backend HTTP calls,
- // it may be valuable to know that logs it generates are part of a health check. So we start a scope.
- using (_logger.BeginScope(new HealthCheckLogScope(check.Name)))
+ var healthChecks = scope.ServiceProvider.GetRequiredService>();
+
+ var results = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var healthCheck in healthChecks)
{
- HealthCheckResult result;
- try
+ if (predicate != null && !predicate(healthCheck))
{
- _logger.LogTrace("Running health check: {healthCheckName}", check.Name);
- result = await check.CheckHealthAsync(cancellationToken);
- _logger.LogTrace("Health check '{healthCheckName}' completed with status '{healthCheckStatus}'", check.Name, result.Status);
- }
- catch (Exception ex)
- {
- // We don't log this as an error because a health check failing shouldn't bring down the active task.
- _logger.LogError(ex, "Health check '{healthCheckName}' threw an unexpected exception", check.Name);
- result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null);
+ continue;
}
- // This can only happen if the result is default(HealthCheckResult)
- if (result.Status == HealthCheckStatus.Unknown)
- {
- // This is different from the case above. We throw here because a health check is doing something specifically incorrect.
- var exception = new InvalidOperationException($"Health check '{check.Name}' returned a result with a status of Unknown");
- _logger.LogError(exception, "Health check '{healthCheckName}' returned a result with a status of Unknown", check.Name);
- throw exception;
- }
+ cancellationToken.ThrowIfCancellationRequested();
- results[check.Name] = result;
+ // If the health check does things like make Database queries using EF or backend HTTP calls,
+ // it may be valuable to know that logs it generates are part of a health check. So we start a scope.
+ using (_logger.BeginScope(new HealthCheckLogScope(healthCheck.Name)))
+ {
+ HealthCheckResult result;
+ try
+ {
+ Log.HealthCheckBegin(_logger, healthCheck);
+ var stopwatch = ValueStopwatch.StartNew();
+ result = await healthCheck.CheckHealthAsync(cancellationToken);
+ Log.HealthCheckEnd(_logger, healthCheck, result, stopwatch.GetElapsedTime());
+ }
+ catch (Exception ex)
+ {
+ Log.HealthCheckError(_logger, healthCheck, ex);
+ result = new HealthCheckResult(HealthCheckStatus.Failed, ex, ex.Message, data: null);
+ }
+
+ // This can only happen if the result is default(HealthCheckResult)
+ if (result.Status == HealthCheckStatus.Unknown)
+ {
+ // This is different from the case above. We throw here because a health check is doing something specifically incorrect.
+ throw new InvalidOperationException($"Health check '{healthCheck.Name}' returned a result with a status of Unknown");
+ }
+
+ results[healthCheck.Name] = result;
+ }
}
+
+ return new CompositeHealthCheckResult(results);
+ }
+ }
+
+ private static void EnsureNoDuplicates(IEnumerable healthChecks)
+ {
+ // Scan the list for duplicate names to provide a better error if there are duplicates.
+ var duplicateNames = healthChecks
+ .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToList();
+
+ if (duplicateNames.Count > 0)
+ {
+ throw new ArgumentException($"Duplicate health checks were registered with the name(s): {string.Join(", ", duplicateNames)}", nameof(healthChecks));
+ }
+ }
+
+ private static class Log
+ {
+ public static class EventIds
+ {
+ public static readonly EventId HealthCheckBegin = new EventId(100, "HealthCheckBegin");
+ public static readonly EventId HealthCheckEnd = new EventId(101, "HealthCheckEnd");
+ public static readonly EventId HealthCheckError = new EventId(102, "HealthCheckError");
+ }
+
+ private static readonly Action _healthCheckBegin = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckBegin,
+ "Running health check {HealthCheckName}");
+
+ private static readonly Action _healthCheckEnd = LoggerMessage.Define(
+ LogLevel.Debug,
+ EventIds.HealthCheckEnd,
+ "Health check {HealthCheckName} completed after {ElapsedMilliseconds}ms with status {HealthCheckStatus}");
+
+ private static readonly Action _healthCheckError = LoggerMessage.Define(
+ LogLevel.Error,
+ EventIds.HealthCheckError,
+ "Health check {HealthCheckName} threw an unhandled exception");
+
+ public static void HealthCheckBegin(ILogger logger, IHealthCheck healthCheck)
+ {
+ _healthCheckBegin(logger, healthCheck.Name, null);
+ }
+
+ public static void HealthCheckEnd(ILogger logger, IHealthCheck healthCheck, HealthCheckResult result, TimeSpan duration)
+ {
+ _healthCheckEnd(logger, healthCheck.Name, duration.TotalMilliseconds, result.Status, null);
+ }
+
+ public static void HealthCheckError(ILogger logger, IHealthCheck healthCheck, Exception exception)
+ {
+ _healthCheckError(logger, healthCheck.Name, exception);
}
- return new CompositeHealthCheckResult(results);
}
}
}
diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs
index b2f931fcd3..d3af5b17c2 100644
--- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs
+++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/HealthChecksBuilderAddCheckExtensions.cs
@@ -68,7 +68,7 @@ namespace Microsoft.Extensions.DependencyInjection
}
///
- /// Adds a new health check with the implementation.
+ /// Adds a new health check with the provided implementation.
///
/// The to add the check to.
/// An implementation.
@@ -88,5 +88,27 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddSingleton(check);
return builder;
}
+
+ ///
+ /// Adds a new health check as a transient dependency injected service with the provided type.
+ ///
+ /// The health check implementation type.
+ /// The .
+ /// The .
+ ///
+ /// This method will register a transient service of type with the
+ /// provided implementation type . Using this method to register a health
+ /// check allows you to register a health check that depends on transient and scoped services.
+ ///
+ public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder) where T : class, IHealthCheck
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.Add(ServiceDescriptor.Transient(typeof(IHealthCheck), typeof(T)));
+ return builder;
+ }
}
}
diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs
index 9c1bfbafc4..f931475a4d 100644
--- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs
+++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/IHealthCheckService.cs
@@ -1,26 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System.Collections.Generic;
+using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
///
- /// A service which can be used to check the status of instances registered in the application.
+ /// A service which can be used to check the status of instances
+ /// registered in the application.
///
+ ///
+ ///
+ /// The default implementation of is registered in the dependency
+ /// injection container as a singleton service by calling
+ /// .
+ ///
+ ///
+ /// The returned by
+ ///
+ /// provides a convenience API for registering health checks.
+ ///
+ ///
+ /// The default implementation of will use all services
+ /// of type registered in the dependency injection container.
+ /// implementations may be registered with any service lifetime. The implementation will create a scope
+ /// for each aggregate health check operation and use the scope to resolve services. The scope
+ /// created for executing health checks is controlled by the health checks service and does not
+ /// share scoped services with any other scope in the application.
+ ///
+ ///
public interface IHealthCheckService
{
- ///
- /// A containing all the health checks registered in the application.
- ///
- ///
- /// The key maps to the property of the health check, and the value is the
- /// instance itself.
- ///
- IReadOnlyDictionary Checks { get; }
-
///
/// Runs all the health checks in the application and returns the aggregated status.
///
@@ -34,13 +46,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
///
/// Runs the provided health checks and returns the aggregated status
///
- /// The instances to be run.
+ ///
+ /// A predicate that can be used to include health checks based on user-defined criteria.
+ ///
/// A which can be used to cancel the health checks.
///
/// A which will complete when all the health checks have been run,
/// yielding a containing the results.
///
- Task CheckHealthAsync(IEnumerable checks,
+ Task CheckHealthAsync(Func predicate,
CancellationToken cancellationToken = default);
}
}
diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
index 56f70f7a10..2b3d0e6261 100644
--- a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
+++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Microsoft.Extensions.Diagnostics.HealthChecks.csproj
@@ -14,6 +14,7 @@ Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
+
diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..13e969bfad
--- /dev/null
+++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs
index 692e8b5bfd..1af49d8422 100644
--- a/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs
+++ b/test/Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests/HealthCheckMiddlewareTests.cs
@@ -302,11 +302,7 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
app.UseHealthChecks("/health", new HealthCheckOptions()
{
- HealthCheckNames =
- {
- "Baz",
- "FOO",
- },
+ Predicate = (check) => check.Name == "Foo" || check.Name == "Baz",
});
})
.ConfigureServices(services =>
@@ -327,35 +323,6 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
Assert.Equal("Healthy", await response.Content.ReadAsStringAsync());
}
- [Fact]
- public void CanFilterChecks_ThrowsForMissingCheck()
- {
- var builder = new WebHostBuilder()
- .Configure(app =>
- {
- app.UseHealthChecks("/health", new HealthCheckOptions()
- {
- HealthCheckNames =
- {
- "Bazzzzzz",
- "FOO",
- },
- });
- })
- .ConfigureServices(services =>
- {
- services.AddHealthChecks()
- .AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
- .AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("A-ok!")))
- .AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
- });
-
- var ex = Assert.Throws(() => new TestServer(builder));
- Assert.Equal(
- "The following health checks were not found: 'Bazzzzzz'. Registered health checks: 'Foo, Bar, Baz'.",
- ex.Message);
- }
-
[Fact]
public async Task CanListenOnPort_AcceptsRequest_OnSpecifiedPort()
{
diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs
index 2a7fa8395f..6e7c28e9ef 100644
--- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs
+++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthCheckServiceTests.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
@@ -12,40 +14,30 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class HealthCheckServiceTests
{
- [Fact]
- public void Constructor_BuildsDictionaryOfChecks()
- {
- // Arrange
- var fooCheck = new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy()));
- var barCheck = new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy()));
- var bazCheck = new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy()));
- var checks = new[] { fooCheck, barCheck, bazCheck };
-
- // Act
- var service = new HealthCheckService(checks);
-
- // Assert
- Assert.Same(fooCheck, service.Checks["Foo"]);
- Assert.Same(barCheck, service.Checks["Bar"]);
- Assert.Same(bazCheck, service.Checks["Baz"]);
- Assert.Equal(3, service.Checks.Count);
- }
-
[Fact]
public void Constructor_ThrowsUsefulExceptionForDuplicateNames()
{
// Arrange
- var checks = new[]
- {
- new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
- new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())),
- new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())),
- new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
- new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())),
- };
+ //
+ // Doing this the old fashioned way so we can verify that the exception comes
+ // from the constructor.
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging();
+ serviceCollection.AddOptions();
+ serviceCollection.AddHealthChecks()
+ .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck(new HealthCheck("Foo", _ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck(new HealthCheck("Bar", _ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())))
+ .AddCheck(new HealthCheck("Baz", _ => Task.FromResult(HealthCheckResult.Healthy())));
+
+ var services = serviceCollection.BuildServiceProvider();
+
+ var scopeFactory = services.GetRequiredService();
+ var logger = services.GetRequiredService>();
// Act
- var exception = Assert.Throws(() => new HealthCheckService(checks));
+ var exception = Assert.Throws(() => new HealthCheckService(scopeFactory, logger));
// Assert
Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message);
@@ -67,15 +59,11 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ DataKey, DataValue }
};
- var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
- var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
- var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
-
- var service = new HealthCheckService(new[]
+ var service = CreateHealthChecksService(b =>
{
- healthyCheck,
- degradedCheck,
- unhealthyCheck,
+ b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
+ b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
+ b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
@@ -85,7 +73,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
Assert.Collection(results.Results,
actual =>
{
- Assert.Equal(healthyCheck.Name, actual.Key);
+ Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@@ -97,7 +85,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
},
actual =>
{
- Assert.Equal(degradedCheck.Name, actual.Key);
+ Assert.Equal("DegradedCheck", actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@@ -105,7 +93,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
},
actual =>
{
- Assert.Equal(unhealthyCheck.Name, actual.Key);
+ Assert.Equal("UnhealthyCheck", actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status);
Assert.Same(exception, actual.Value.Exception);
@@ -114,7 +102,7 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
}
[Fact]
- public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync()
+ public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
{
const string DataKey = "Foo";
const string DataValue = "Bar";
@@ -129,28 +117,21 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
{ DataKey, DataValue }
};
- var healthyCheck = new HealthCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
- var degradedCheck = new HealthCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
- var unhealthyCheck = new HealthCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
-
- var service = new HealthCheckService(new[]
+ var service = CreateHealthChecksService(b =>
{
- healthyCheck,
- degradedCheck,
- unhealthyCheck,
+ b.AddCheck("HealthyCheck", _ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage, data)));
+ b.AddCheck("DegradedCheck", _ => Task.FromResult(HealthCheckResult.Degraded(DegradedMessage)));
+ b.AddCheck("UnhealthyCheck", _ => Task.FromResult(HealthCheckResult.Unhealthy(UnhealthyMessage, exception)));
});
// Act
- var results = await service.CheckHealthAsync(new[]
- {
- service.Checks["HealthyCheck"]
- });
+ var results = await service.CheckHealthAsync(c => c.Name == "HealthyCheck");
// Assert
Assert.Collection(results.Results,
actual =>
{
- Assert.Equal(healthyCheck.Name, actual.Key);
+ Assert.Equal("HealthyCheck", actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
@@ -168,11 +149,12 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
var faultedException = new InvalidOperationException("Ohnoes!");
- var service = new HealthCheckService(new[]
+
+ var service = CreateHealthChecksService(b =>
{
- new HealthCheck("Throws", ct => throw thrownException),
- new HealthCheck("Faults", ct => Task.FromException(faultedException)),
- new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())),
+ b.AddCheck("Throws", ct => throw thrownException);
+ b.AddCheck("Faults", ct => Task.FromException(faultedException));
+ b.AddCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy()));
});
// Act
@@ -223,8 +205,15 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
});
return Task.FromResult(HealthCheckResult.Healthy());
});
+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
- var service = new HealthCheckService(new[] { check }, loggerFactory.CreateLogger());
+ var service = CreateHealthChecksService(b =>
+ {
+ // Override the logger factory for testing
+ b.Services.AddSingleton(loggerFactory);
+
+ b.AddCheck(check);
+ });
// Act
var results = await service.CheckHealthAsync();
@@ -241,9 +230,9 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
{
// Arrange
- var service = new HealthCheckService(new[]
+ var service = CreateHealthChecksService(b =>
{
- new HealthCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))),
+ b.AddCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult)));
});
// Act
@@ -252,5 +241,108 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
// Assert
Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message);
}
+
+ [Fact]
+ public async Task CheckHealthAsync_CheckCanDependOnTransientService()
+ {
+ // Arrange
+ var service = CreateHealthChecksService(b =>
+ {
+ b.Services.AddTransient();
+
+ b.AddCheck();
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ Assert.Collection(
+ results.Results,
+ actual =>
+ {
+ Assert.Equal("Test", actual.Key);
+ Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
+ });
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_CheckCanDependOnScopedService()
+ {
+ // Arrange
+ var service = CreateHealthChecksService(b =>
+ {
+ b.Services.AddScoped();
+
+ b.AddCheck();
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ Assert.Collection(
+ results.Results,
+ actual =>
+ {
+ Assert.Equal("Test", actual.Key);
+ Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
+ });
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
+ {
+ // Arrange
+ var service = CreateHealthChecksService(b =>
+ {
+ b.Services.AddSingleton();
+
+ b.AddCheck();
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ Assert.Collection(
+ results.Results,
+ actual =>
+ {
+ Assert.Equal("Test", actual.Key);
+ Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
+ });
+ }
+
+ private static HealthCheckService CreateHealthChecksService(Action configure)
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddOptions();
+
+ var builder = services.AddHealthChecks();
+ if (configure != null)
+ {
+ configure(builder);
+ }
+
+ return (HealthCheckService)services.BuildServiceProvider(validateScopes: true).GetRequiredService();
+ }
+
+ private class AnotherService { }
+
+ private class CheckWithServiceDependency : IHealthCheck
+ {
+ public CheckWithServiceDependency(AnotherService _)
+ {
+ }
+
+ public string Name => "Test";
+
+ public Task CheckHealthAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(HealthCheckResult.Healthy());
+ }
+ }
}
}
diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs
index a24a4a5b4e..f1bcbbf00b 100644
--- a/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs
+++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.Tests/HealthChecksBuilderTests.cs
@@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@@ -19,12 +21,13 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act
- var healthCheckService = services.BuildServiceProvider().GetRequiredService();
+ var checks = services.BuildServiceProvider().GetRequiredService>();
// Assert
- Assert.Collection(healthCheckService.Checks,
- actual => Assert.Equal("Foo", actual.Key),
- actual => Assert.Equal("Bar", actual.Key));
+ Assert.Collection(
+ checks,
+ actual => Assert.Equal("Foo", actual.Name),
+ actual => Assert.Equal("Bar", actual.Name));
}
}
}