diff --git a/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs b/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs index 7b9fabebdd..a281884482 100644 --- a/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs +++ b/src/Middleware/HealthChecks/src/Builder/HealthCheckApplicationBuilderExtensions.cs @@ -4,6 +4,8 @@ using System; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder @@ -225,6 +227,14 @@ namespace Microsoft.AspNetCore.Builder private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args) { + if (app.ApplicationServices.GetService(typeof(HealthCheckService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(HealthCheckServiceCollectionExtensions.AddHealthChecks), + "ConfigureServices(...)")); + } + // NOTE: we explicitly don't use Map here because it's really common for multiple health // check middleware to overlap in paths. Ex: `/health`, `/health/detailed` - this is order // sensititive with Map, and it's really surprising to people. diff --git a/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs b/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs index 4493e8a41b..5f44fcaf77 100644 --- a/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs +++ b/src/Middleware/HealthChecks/src/Builder/HealthCheckEndpointRouteBuilderExtensions.cs @@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Builder { @@ -93,6 +95,14 @@ namespace Microsoft.AspNetCore.Builder private static IEndpointConventionBuilder MapHealthChecksCore(IEndpointRouteBuilder builder, string pattern, HealthCheckOptions options, string displayName) { + if (builder.ServiceProvider.GetService(typeof(HealthCheckService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(HealthCheckServiceCollectionExtensions.AddHealthChecks), + "ConfigureServices(...)")); + } + var args = options != null ? new[] { Options.Create(options) } : Array.Empty(); var pipeline = builder.CreateApplicationBuilder() diff --git a/src/Middleware/HealthChecks/src/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj b/src/Middleware/HealthChecks/src/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj index 34929e104e..a761a1a3b1 100644 --- a/src/Middleware/HealthChecks/src/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj +++ b/src/Middleware/HealthChecks/src/Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj @@ -24,4 +24,11 @@ + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/Middleware/HealthChecks/src/Properties/Resources.Designer.cs b/src/Middleware/HealthChecks/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..b5a253282e --- /dev/null +++ b/src/Middleware/HealthChecks/src/Properties/Resources.Designer.cs @@ -0,0 +1,44 @@ +// +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Diagnostics.HealthChecks.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string UnableToFindServices + { + get => GetString("UnableToFindServices"); + } + + /// + /// Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + /// + internal static string FormatUnableToFindServices(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("UnableToFindServices"), p0, p1, p2); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Middleware/HealthChecks/src/Resources.resx b/src/Middleware/HealthChecks/src/Resources.resx new file mode 100644 index 0000000000..306d2c4f3c --- /dev/null +++ b/src/Middleware/HealthChecks/src/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to find the required services. Please add all the required services by calling '{0}.{1}' inside the call to '{2}' in the application startup code. + + \ No newline at end of file diff --git a/src/Middleware/HealthChecks/test/UnitTests/HealthCheckEndpointRouteBuilderExtensionsTest.cs b/src/Middleware/HealthChecks/test/UnitTests/HealthCheckEndpointRouteBuilderExtensionsTest.cs index 07a32a574a..485276d898 100644 --- a/src/Middleware/HealthChecks/test/UnitTests/HealthCheckEndpointRouteBuilderExtensionsTest.cs +++ b/src/Middleware/HealthChecks/test/UnitTests/HealthCheckEndpointRouteBuilderExtensionsTest.cs @@ -18,6 +18,31 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { public class HealthCheckEndpointRouteBuilderExtensionsTest { + [Fact] + public void ThrowFriendlyErrorWhenServicesNotRegistered() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRouting(routes => + { + routes.MapHealthChecks("/healthz"); + }); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }); + + var ex = Assert.Throws(() => new TestServer(builder)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddHealthChecks' inside the call to 'ConfigureServices(...)' " + + "in the application startup code.", + ex.Message); + } + [Fact] public async Task MapHealthChecks_ReturnsOk() { diff --git a/src/Middleware/HealthChecks/test/UnitTests/HealthCheckMiddlewareTests.cs b/src/Middleware/HealthChecks/test/UnitTests/HealthCheckMiddlewareTests.cs index 135a4bed9c..629f46cebe 100644 --- a/src/Middleware/HealthChecks/test/UnitTests/HealthCheckMiddlewareTests.cs +++ b/src/Middleware/HealthChecks/test/UnitTests/HealthCheckMiddlewareTests.cs @@ -19,6 +19,24 @@ namespace Microsoft.AspNetCore.Diagnostics.HealthChecks { public class HealthCheckMiddlewareTests { + [Fact] + public void ThrowFriendlyErrorWhenServicesNotRegistered() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseHealthChecks("/health"); + }); + + var ex = Assert.Throws(() => new TestServer(builder)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddHealthChecks' inside the call to 'ConfigureServices(...)' " + + "in the application startup code.", + ex.Message); + } + [Fact] // Matches based on '.Map' public async Task IgnoresRequestThatDoesNotMatchPath() {