Add abstractions for Health Checks and a simple middleware and service to run them (#408)

This commit is contained in:
Andrew Stanton-Nurse 2017-10-18 13:52:06 -07:00 committed by GitHub
parent a89d01b75e
commit a30befae0f
30 changed files with 1691 additions and 1 deletions

3
.gitignore vendored
View File

@ -34,3 +34,6 @@ launchSettings.json
.testPublish/
global.json
korebuild-lock.txt
# Rider and friends
.idea/

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26228.9
VisualStudioVersion = 15.0.26923.0
MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{509A6F36-AD80-4A18-B5B1-717D38DFF29D}"
EndProject
@ -44,6 +44,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.EFCore.Function
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Diagnostics.FunctionalTests", "test\Diagnostics.FunctionalTests\Diagnostics.FunctionalTests.csproj", "{C142A666-D932-4E0D-8D18-5B08944C1077}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions", "src\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj", "{0D103C24-B9E8-468A-B113-509FCFEF7B45}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks", "src\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj", "{F285F000-9342-4A01-9706-BAB2B97B4F97}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks", "src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj", "{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealthChecksSample", "samples\HealthChecksSample\HealthChecksSample.csproj", "{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests", "test\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests\Microsoft.AspNetCore.Diagnostics.HealthChecks.Tests.csproj", "{E718CE19-23CC-427F-B06E-B866E9A35924}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.HealthChecks.Tests", "test\Microsoft.Extensions.Diagnostics.HealthChecks.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.Tests.csproj", "{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -260,6 +272,78 @@ Global
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|x86.ActiveCfg = Release|Any CPU
{C142A666-D932-4E0D-8D18-5B08944C1077}.Release|x86.Build.0 = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|x86.ActiveCfg = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Debug|x86.Build.0 = Debug|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Any CPU.Build.0 = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|x86.ActiveCfg = Release|Any CPU
{0D103C24-B9E8-468A-B113-509FCFEF7B45}.Release|x86.Build.0 = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|x86.ActiveCfg = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Debug|x86.Build.0 = Debug|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Any CPU.Build.0 = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|x86.ActiveCfg = Release|Any CPU
{F285F000-9342-4A01-9706-BAB2B97B4F97}.Release|x86.Build.0 = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|x86.ActiveCfg = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Debug|x86.Build.0 = Debug|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Any CPU.Build.0 = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|x86.ActiveCfg = Release|Any CPU
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C}.Release|x86.Build.0 = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|x86.ActiveCfg = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Debug|x86.Build.0 = Debug|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Any CPU.Build.0 = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|x86.ActiveCfg = Release|Any CPU
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7}.Release|x86.Build.0 = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|x86.ActiveCfg = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Debug|x86.Build.0 = Debug|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Any CPU.Build.0 = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|x86.ActiveCfg = Release|Any CPU
{E718CE19-23CC-427F-B06E-B866E9A35924}.Release|x86.Build.0 = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|x86.ActiveCfg = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Debug|x86.Build.0 = Debug|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Any CPU.Build.0 = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.ActiveCfg = Release|Any CPU
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -283,5 +367,14 @@ Global
{AA3661A1-CE8D-4597-ADFD-A5A30834E5D0} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
{043C5272-D7F7-4DB1-830F-5DC93CC0878E} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
{C142A666-D932-4E0D-8D18-5B08944C1077} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
{0D103C24-B9E8-468A-B113-509FCFEF7B45} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
{F285F000-9342-4A01-9706-BAB2B97B4F97} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
{1B2B1EF4-9066-4F38-ADCF-D05C6423E21C} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D}
{3B4E60F6-E42D-496E-B96F-71A11DABAEE7} = {ACAA0157-A8C4-4152-93DE-90CCDF304087}
{E718CE19-23CC-427F-B06E-B866E9A35924} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
{3783E8E4-2E96-4987-A83A-0CCCAF4891C1} = {2AF90579-B118-4583-AE88-672EFACB5BC4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D915AA7B-4ADE-4BAC-AF65-1E800D3F3580}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
namespace HealthChecksSample
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
new WebHostBuilder()
.ConfigureLogging(builder =>
{
builder.AddConsole();
})
.UseKestrel()
.UseStartup<Startup>()
.Build();
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace HealthChecksSample
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddHealthChecks();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHealthChecks("/health");
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
}

View File

@ -0,0 +1,32 @@
// 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;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// <see cref="IApplicationBuilder"/> extension methods for the <see cref="HealthCheckMiddleware"/>.
/// </summary>
public static class HealthCheckAppBuilderExtensions
{
/// <summary>
/// Adds a middleware that provides a REST API for requesting health check status.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="path">The path on which to provide the API.</param>
/// <returns>A reference to the <paramref name="app"/> after the operation has completed.</returns>
public static IApplicationBuilder UseHealthChecks(this IApplicationBuilder app, PathString path)
{
app = app ?? throw new ArgumentNullException(nameof(app));
return app.UseMiddleware<HealthCheckMiddleware>(Options.Create(new HealthCheckOptions()
{
Path = path
}));
}
}
}

View File

@ -0,0 +1,79 @@
// 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;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class HealthCheckMiddleware
{
private readonly RequestDelegate _next;
private readonly HealthCheckOptions _healthCheckOptions;
private readonly IHealthCheckService _healthCheckService;
public HealthCheckMiddleware(RequestDelegate next, IOptions<HealthCheckOptions> healthCheckOptions, IHealthCheckService healthCheckService)
{
_next = next;
_healthCheckOptions = healthCheckOptions.Value;
_healthCheckService = healthCheckService;
}
/// <summary>
/// Process an individual request.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path == _healthCheckOptions.Path)
{
// Get results
var result = await _healthCheckService.CheckHealthAsync(context.RequestAborted);
// Map status to response code
switch (result.Status)
{
case HealthCheckStatus.Failed:
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
break;
case HealthCheckStatus.Unhealthy:
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
break;
case HealthCheckStatus.Degraded:
// Degraded doesn't mean unhealthy so we return 200, but the content will contain more details
context.Response.StatusCode = StatusCodes.Status200OK;
break;
case HealthCheckStatus.Healthy:
context.Response.StatusCode = StatusCodes.Status200OK;
break;
default:
// This will only happen when we change HealthCheckStatus and we don't update this.
Debug.Fail($"Unrecognized HealthCheckStatus value: {result.Status}");
throw new InvalidOperationException($"Unrecognized HealthCheckStatus value: {result.Status}");
}
// Render results to JSON
var json = new JObject(
new JProperty("status", result.Status.ToString()),
new JProperty("results", new JObject(result.Results.Select(pair =>
new JProperty(pair.Key, new JObject(
new JProperty("status", pair.Value.Status.ToString()),
new JProperty("description", pair.Value.Description),
new JProperty("data", new JObject(pair.Value.Data.Select(p => new JProperty(p.Key, p.Value))))))))));
await context.Response.WriteAsync(json.ToString(Formatting.None));
}
else
{
await _next(context);
}
}
}
}

View File

@ -0,0 +1,18 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
/// <summary>
/// Contains options for the <see cref="HealthCheckMiddleware"/>.
/// </summary>
public class HealthCheckOptions
{
/// <summary>
/// Gets or sets the path at which the Health Check results will be available.
/// </summary>
public PathString Path { get; set; }
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
<Description>ASP.NET Core middleware for returning the results of Health Checks in the application
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,188 @@
// 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;
using System.Collections.Generic;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents the result of a health check.
/// </summary>
public struct HealthCheckResult
{
private static readonly IReadOnlyDictionary<string, object> _emptyReadOnlyDictionary = new Dictionary<string, object>();
private string _description;
private IReadOnlyDictionary<string, object> _data;
/// <summary>
/// Gets a <see cref="HealthCheckStatus"/> value indicating the status of the component that was checked.
/// </summary>
public HealthCheckStatus Status { get; }
/// <summary>
/// Gets an <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).
/// </summary>
/// <remarks>
/// This value is expected to be 'null' if <see cref="Status"/> is <see cref="HealthCheckStatus.Healthy"/>.
/// </remarks>
public Exception Exception { get; }
/// <summary>
/// Gets a human-readable description of the status of the component that was checked.
/// </summary>
public string Description => _description ?? string.Empty;
/// <summary>
/// Gets additional key-value pairs describing the health of the component.
/// </summary>
public IReadOnlyDictionary<string, object> Data => _data ?? _emptyReadOnlyDictionary;
/// <summary>
/// Creates a new <see cref="HealthCheckResult"/> with the specified <paramref name="status"/>, <paramref name="exception"/>,
/// <paramref name="description"/>, and <paramref name="data"/>.
/// </summary>
/// <param name="status">A <see cref="HealthCheckStatus"/> value indicating the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public HealthCheckResult(HealthCheckStatus status, Exception exception, string description, IReadOnlyDictionary<string, object> data)
{
if (status == HealthCheckStatus.Unknown)
{
throw new ArgumentException($"'{nameof(HealthCheckStatus.Unknown)}' is not a valid value for the 'status' parameter.", nameof(status));
}
Status = status;
Exception = exception;
_description = description;
_data = data;
}
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
public static HealthCheckResult Unhealthy()
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: string.Empty, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
public static HealthCheckResult Unhealthy(string description)
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary<string, object> data)
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception: null, description: description, data: data);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
public static HealthCheckResult Unhealthy(Exception exception)
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description: string.Empty, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
public static HealthCheckResult Unhealthy(string description, Exception exception)
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing an unhealthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing an unhealthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public static HealthCheckResult Unhealthy(string description, Exception exception, IReadOnlyDictionary<string, object> data)
=> new HealthCheckResult(HealthCheckStatus.Unhealthy, exception, description, data);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
public static HealthCheckResult Healthy()
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: string.Empty, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
public static HealthCheckResult Healthy(string description)
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a healthy component.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a healthy component.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public static HealthCheckResult Healthy(string description, IReadOnlyDictionary<string, object> data)
=> new HealthCheckResult(HealthCheckStatus.Healthy, exception: null, description: description, data: data);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
public static HealthCheckResult Degraded()
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
public static HealthCheckResult Degraded(string description)
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public static HealthCheckResult Degraded(string description, IReadOnlyDictionary<string, object> data)
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: description, data: data);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
public static HealthCheckResult Degraded(Exception exception)
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception: null, description: string.Empty, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
public static HealthCheckResult Degraded(string description, Exception exception)
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data: null);
/// <summary>
/// Creates a <see cref="HealthCheckResult"/> representing a component in a degraded state.
/// </summary>
/// <returns>A <see cref="HealthCheckResult"/> representing a component in a degraded state.</returns>
/// <param name="description">A human-readable description of the status of the component that was checked.</param>
/// <param name="exception">An <see cref="Exception"/> representing the exception that was thrown when checking for status (if any).</param>
/// <param name="data">Additional key-value pairs describing the health of the component.</param>
public static HealthCheckResult Degraded(string description, Exception exception, IReadOnlyDictionary<string, object> data)
=> new HealthCheckResult(HealthCheckStatus.Degraded, exception, description, data);
}
}

View File

@ -0,0 +1,41 @@
// 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.
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents the status of a health check result.
/// </summary>
/// <remarks>
/// The values of this enum or ordered from least healthy to most healthy. So <see cref="HealthCheckStatus.Degraded"/> is
/// greater than <see cref="HealthCheckStatus.Unhealthy"/> but less than <see cref="HealthCheckStatus.Healthy"/>.
/// </remarks>
public enum HealthCheckStatus
{
/// <summary>
/// This value should not be returned by a health check. It is used to represent an uninitialized value.
/// </summary>
Unknown = 0,
/// <summary>
/// This value should not be returned by a health check. It is used to indicate that an unexpected exception was
/// thrown when running the health check.
/// </summary>
Failed = 1,
/// <summary>
/// Indicates that the health check determined that the component was unhealthy.
/// </summary>
Unhealthy = 2,
/// <summary>
/// Indicates that the health check determined that the component was in a degraded state.
/// </summary>
Degraded = 3,
/// <summary>
/// Indicates that the health check determined that the component was healthy.
/// </summary>
Healthy = 4,
}
}

View File

@ -0,0 +1,27 @@
// 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.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents a health check, which can be used to check the status of a component in the application, such as a backend service, database or some internal
/// state.
/// </summary>
public interface IHealthCheck
{
/// <summary>
/// Gets the name of the health check, which should indicate the component being checked.
/// </summary>
string Name { get; }
/// <summary>
/// Runs the health check, returning the status of the component being checked.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Abstractions for defining health checks in .NET applications
Commonly Used Types
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
</Description>
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,66 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Represents the results of multiple health checks.
/// </summary>
public class CompositeHealthCheckResult
{
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.
/// </summary>
/// <remarks>
/// The keys in this dictionary map to the name of the health check, the values are the <see cref="HealthCheckResult"/>
/// returned when <see cref="IHealthCheck.CheckHealthAsync(System.Threading.CancellationToken)"/> was called for that health check.
/// </remarks>
public IReadOnlyDictionary<string, HealthCheckResult> Results { get; }
/// <summary>
/// Gets a <see cref="HealthCheckStatus"/> representing the aggregate status of all the health checks.
/// </summary>
/// <remarks>
/// This value is determined by taking the "worst" result of all the results. So if any result is <see cref="HealthCheckStatus.Failed"/>,
/// this value is <see cref="HealthCheckStatus.Failed"/>. If no result is <see cref="HealthCheckStatus.Failed"/> but any result is
/// <see cref="HealthCheckStatus.Unhealthy"/>, this value is <see cref="HealthCheckStatus.Unhealthy"/>, etc.
/// </remarks>
public HealthCheckStatus Status { get; }
/// <summary>
/// Create a new <see cref="CompositeHealthCheckResult"/> from the specified results.
/// </summary>
/// <param name="results">A <see cref="IReadOnlyDictionary{TKey, T}"/> containing the results from each health check.</param>
public CompositeHealthCheckResult(IReadOnlyDictionary<string, HealthCheckResult> results)
{
Results = results;
Status = CalculateAggregateStatus(results.Values);
}
private HealthCheckStatus CalculateAggregateStatus(IEnumerable<HealthCheckResult> results)
{
// This is basically a Min() check, but we know the possible range, so we don't need to walk the whole list
var currentValue = HealthCheckStatus.Healthy;
foreach (var result in results)
{
if (currentValue > result.Status)
{
currentValue = result.Status;
}
if (currentValue == HealthCheckStatus.Failed)
{
// Game over, man! Game over!
// (We hit the worst possible status, so there's no need to keep iterating)
return currentValue;
}
}
return currentValue;
}
}
}

View File

@ -0,0 +1,41 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// A simple implementation of <see cref="IHealthCheck"/> which uses a provided delegate to
/// implement the check.
/// </summary>
public class HealthCheck : IHealthCheck
{
private readonly Func<CancellationToken, Task<HealthCheckResult>> _check;
/// <summary>
/// Gets the name of the health check, which should indicate the component being checked.
/// </summary>
public string Name { get; }
/// <summary>
/// Create an instance of <see cref="HealthCheck"/> from the specified <paramref name="name"/> and <paramref name="check"/>.
/// </summary>
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
public HealthCheck(string name, Func<CancellationToken, Task<HealthCheckResult>> check)
{
Name = name;
_check = check;
}
/// <summary>
/// Runs the health check, returning the status of the component being checked.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the health check.</param>
/// <returns>A <see cref="Task{HealthCheckResult}"/> that completes when the health check has finished, yielding the status of the component being checked.</returns>
public Task<HealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) => _check(cancellationToken);
}
}

View File

@ -0,0 +1,48 @@
// 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;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal class HealthCheckLogScope : IReadOnlyList<KeyValuePair<string, object>>
{
public string HealthCheckName { get; }
int IReadOnlyCollection<KeyValuePair<string, object>>.Count { get; } = 1;
KeyValuePair<string, object> IReadOnlyList<KeyValuePair<string, object>>.this[int index]
{
get
{
if (index == 0)
{
return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
/// <summary>
/// Creates a new instance of <see cref="HealthCheckLogScope"/> with the provided name.
/// </summary>
/// <param name="healthCheckName">The name of the health check being executed.</param>
public HealthCheckLogScope(string healthCheckName)
{
HealthCheckName = healthCheckName;
}
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{
yield return new KeyValuePair<string, object>(nameof(HealthCheckName), HealthCheckName);
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
}
}
}

View File

@ -0,0 +1,132 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// Default implementation of <see cref="IHealthCheckService"/>.
/// </summary>
public class HealthCheckService : IHealthCheckService
{
private readonly ILogger<HealthCheckService> _logger;
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
/// </summary>
/// <remarks>
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
/// instance itself.
/// </remarks>
public IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
/// <summary>
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances.
/// </summary>
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks) : this(healthChecks, NullLogger<HealthCheckService>.Instance) { }
/// <summary>
/// Constructs a <see cref="HealthCheckService"/> from the provided collection of <see cref="IHealthCheck"/> instances, and the provided logger.
/// </summary>
/// <param name="healthChecks">The <see cref="IHealthCheck"/> instances that have been registered in the application.</param>
/// <param name="logger">A <see cref="ILogger{T}"/> that can be used to log events that occur during health check operations.</param>
public HealthCheckService(IEnumerable<IHealthCheck> healthChecks, ILogger<HealthCheckService> logger)
{
healthChecks = healthChecks ?? throw new ArgumentNullException(nameof(logger));
_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<string>(StringComparer.OrdinalIgnoreCase);
var duplicates = new List<string>();
foreach (var check in healthChecks)
{
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);
}
}
}
/// <summary>
/// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
public Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default) =>
CheckHealthAsync(Checks.Values, cancellationToken);
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </summary>
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
public async Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks, CancellationToken cancellationToken = default)
{
var results = new Dictionary<string, HealthCheckResult>(Checks.Count, StringComparer.OrdinalIgnoreCase);
foreach (var check in checks)
{
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)))
{
HealthCheckResult result;
try
{
_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);
}
// 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;
}
results[check.Name] = result;
}
}
return new CompositeHealthCheckResult(results);
}
}
}

View File

@ -0,0 +1,31 @@
// 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 Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Provides extension methods for registering <see cref="IHealthCheckService"/> in an <see cref="IServiceCollection"/>.
/// </summary>
public static class HealthCheckServiceCollectionExtensions
{
/// <summary>
/// Adds the <see cref="IHealthCheckService"/> to the container, using the provided delegate to register
/// health checks.
/// </summary>
/// <remarks>
/// This operation is idempotent - multiple invocations will still only result in a single
/// <see cref="IHealthCheckService"/> instance in the <see cref="IServiceCollection"/>. It can be invoked
/// multiple times in order to get access to the <see cref="IHealthChecksBuilder"/> in multiple places.
/// </remarks>
/// <param name="services">The <see cref="IServiceCollection"/> to add the <see cref="IHealthCheckService"/> to.</param>
/// <returns>An instance of <see cref="IHealthChecksBuilder"/> from which health checks can be registered.</returns>
public static IHealthChecksBuilder AddHealthChecks(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Singleton<IHealthCheckService, HealthCheckService>());
return new HealthChecksBuilder(services);
}
}
}

View File

@ -0,0 +1,17 @@
// 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 Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
internal class HealthChecksBuilder : IHealthChecksBuilder
{
public IServiceCollection Services { get; }
public HealthChecksBuilder(IServiceCollection services)
{
Services = services;
}
}
}

View File

@ -0,0 +1,39 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Provides extension methods for registering <see cref="IHealthCheck"/> instances in an <see cref="IHealthChecksBuilder"/>.
/// </summary>
public static class HealthChecksBuilderAddCheckExtensions
{
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func<CancellationToken, Task<HealthCheckResult>> check)
{
builder.Services.AddSingleton<IHealthCheck>(services => new HealthCheck(name, check));
return builder;
}
/// <summary>
/// Adds a new health check with the specified name and implementation.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/> to add the check to.</param>
/// <param name="name">The name of the health check, which should indicate the component being checked.</param>
/// <param name="check">A delegate which provides the code to execute when the health check is run.</param>
/// <returns>The <see cref="IHealthChecksBuilder"/>.</returns>
public static IHealthChecksBuilder AddCheck(this IHealthChecksBuilder builder, string name, Func<Task<HealthCheckResult>> check) =>
builder.AddCheck(name, _ => check());
}
}

View File

@ -0,0 +1,46 @@
// 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.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// A service which can be used to check the status of <see cref="IHealthCheck"/> instances registered in the application.
/// </summary>
public interface IHealthCheckService
{
/// <summary>
/// A <see cref="IReadOnlyDictionary{TKey, T}"/> containing all the health checks registered in the application.
/// </summary>
/// <remarks>
/// The key maps to the <see cref="IHealthCheck.Name"/> property of the health check, and the value is the <see cref="IHealthCheck"/>
/// instance itself.
/// </remarks>
IReadOnlyDictionary<string, IHealthCheck> Checks { get; }
/// <summary>
/// Runs all the health checks in the application and returns the aggregated status.
/// </summary>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
Task<CompositeHealthCheckResult> CheckHealthAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Runs the provided health checks and returns the aggregated status
/// </summary>
/// <param name="checks">The <see cref="IHealthCheck"/> instances to be run.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the health checks.</param>
/// <returns>
/// A <see cref="Task{T}"/> which will complete when all the health checks have been run,
/// yielding a <see cref="CompositeHealthCheckResult"/> containing the results.
/// </returns>
Task<CompositeHealthCheckResult> CheckHealthAsync(IEnumerable<IHealthCheck> checks,
CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,22 @@
// 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 Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
/// <summary>
/// A builder used to collect instances of <see cref="IHealthCheck"/> and register them on an <see cref="IServiceCollection"/>.
/// </summary>
/// <remarks>
/// This type wraps an <see cref="IServiceCollection"/> and provides a place for health check components to attach extension
/// methods for registering themselves in the <see cref="IServiceCollection"/>.
/// </remarks>
public interface IHealthChecksBuilder
{
/// <summary>
/// Gets the <see cref="IServiceCollection"/> into which <see cref="IHealthCheck"/> instances should be registered.
/// </summary>
IServiceCollection Services { get; }
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Components for performing health checks in .NET applications
Commonly Used Types:
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheckService
Microsoft.Extensions.Diagnostics.HealthChecks.IHealthChecksBuilder
</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>diagnostics;healthchecks</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,243 @@
// 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;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Diagnostics.HealthChecks
{
public class HealthCheckMiddlewareTests
{
[Theory]
[InlineData("/frob")]
[InlineData("/health/")] // Match is exact, for now at least
public async Task IgnoresRequestThatDoesNotMatchPath(string requestPath)
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync(requestPath);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData("/health")]
[InlineData("/Health")]
[InlineData("/HEALTH")]
public async Task ReturnsEmptyHealthyRequestIfNoHealthChecksRegistered(string requestPath)
{
var expectedJson = JsonConvert.SerializeObject(new
{
status = "Healthy",
results = new { }
}, Formatting.None);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync(requestPath);
var result = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedJson, result);
}
[Fact]
public async Task ReturnsResultsFromHealthChecks()
{
var expectedJson = JsonConvert.SerializeObject(new
{
status = "Unhealthy",
results = new
{
Foo = new
{
status = "Healthy",
description = "Good to go!",
data = new { }
},
Bar = new
{
status = "Degraded",
description = "Feeling a bit off.",
data = new { someUsefulAttribute = 42 }
},
Baz = new
{
status = "Unhealthy",
description = "Not feeling good at all",
data = new { }
},
Boz = new
{
status = "Unhealthy",
description = string.Empty,
data = new { }
},
},
}, Formatting.None);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("Good to go!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Feeling a bit off.", new Dictionary<string, object>()
{
{ "someUsefulAttribute", 42 }
})))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Unhealthy("Not feeling good at all", new Exception("Bad times"))))
.AddCheck("Boz", () => Task.FromResult(HealthCheckResult.Unhealthy(new Exception("Very bad times"))));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
var result = await response.Content.ReadAsStringAsync();
Assert.Equal(expectedJson, result);
}
[Fact]
public async Task StatusCodeIs200IfNoChecks()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks();
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task StatusCodeIs200IfAllChecksHealthy()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task StatusCodeIs200IfCheckIsDegraded()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Degraded("Not so great.")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task StatusCodeIs503IfCheckIsUnhealthy()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Unhealthy("Pretty bad.")))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
}
[Fact]
public async Task StatusCodeIs500IfCheckIsFailed()
{
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseHealthChecks("/health");
})
.ConfigureServices(services =>
{
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")))
.AddCheck("Bar", () => Task.FromResult(new HealthCheckResult(HealthCheckStatus.Failed, null, null, null)))
.AddCheck("Baz", () => Task.FromResult(HealthCheckResult.Healthy("A-ok!")));
});
var server = new TestServer(builder);
var client = server.CreateClient();
var response = await client.GetAsync("/health");
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
<RootNamespace>Microsoft.AspNetCore.Diagnostics.HealthChecks</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Diagnostics.HealthChecks\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,31 @@
// 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 Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks.Tests
{
public class CompositeHealthCheckResultTests
{
[Theory]
[InlineData(HealthCheckStatus.Healthy)]
[InlineData(HealthCheckStatus.Degraded)]
[InlineData(HealthCheckStatus.Unhealthy)]
[InlineData(HealthCheckStatus.Failed)]
public void Status_MatchesWorstStatusInResults(HealthCheckStatus statusValue)
{
var result = new CompositeHealthCheckResult(new Dictionary<string, HealthCheckResult>()
{
{"Foo", HealthCheckResult.Healthy() },
{"Bar", HealthCheckResult.Healthy() },
{"Baz", new HealthCheckResult(statusValue, exception: null, description: null, data: null) },
{"Quick", HealthCheckResult.Healthy() },
{"Quack", HealthCheckResult.Healthy() },
{"Quock", HealthCheckResult.Healthy() },
});
Assert.Equal(statusValue, result.Status);
}
}
}

View File

@ -0,0 +1,256 @@
// 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;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
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())),
};
// Act
var exception = Assert.Throws<ArgumentException>(() => new HealthCheckService(checks));
// Assert
Assert.Equal($"Duplicate health checks were registered with the name(s): Foo, Baz{Environment.NewLine}Parameter name: healthChecks", exception.Message);
}
[Fact]
public async Task CheckAsync_RunsAllChecksAndAggregatesResultsAsync()
{
const string DataKey = "Foo";
const string DataValue = "Bar";
const string DegradedMessage = "I'm not feeling so good";
const string UnhealthyMessage = "Halp!";
const string HealthyMessage = "Everything is A-OK";
var exception = new Exception("Things are pretty bad!");
// Arrange
var data = new Dictionary<string, object>()
{
{ 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[]
{
healthyCheck,
degradedCheck,
unhealthyCheck,
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results,
actual =>
{
Assert.Equal(healthyCheck.Name, actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item =>
{
Assert.Equal(DataKey, item.Key);
Assert.Equal(DataValue, item.Value);
});
},
actual =>
{
Assert.Equal(degradedCheck.Name, actual.Key);
Assert.Equal(DegradedMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Degraded, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Empty(actual.Value.Data);
},
actual =>
{
Assert.Equal(unhealthyCheck.Name, actual.Key);
Assert.Equal(UnhealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Unhealthy, actual.Value.Status);
Assert.Same(exception, actual.Value.Exception);
Assert.Empty(actual.Value.Data);
});
}
[Fact]
public async Task CheckAsync_RunsProvidedChecksAndAggregatesResultsAsync()
{
const string DataKey = "Foo";
const string DataValue = "Bar";
const string DegradedMessage = "I'm not feeling so good";
const string UnhealthyMessage = "Halp!";
const string HealthyMessage = "Everything is A-OK";
var exception = new Exception("Things are pretty bad!");
// Arrange
var data = new Dictionary<string, object>
{
{ 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[]
{
healthyCheck,
degradedCheck,
unhealthyCheck,
});
// Act
var results = await service.CheckHealthAsync(new[]
{
service.Checks["HealthyCheck"]
});
// Assert
Assert.Collection(results.Results,
actual =>
{
Assert.Equal(healthyCheck.Name, actual.Key);
Assert.Equal(HealthyMessage, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
Assert.Collection(actual.Value.Data, item =>
{
Assert.Equal(DataKey, item.Key);
Assert.Equal(DataValue, item.Value);
});
});
}
[Fact]
public async Task CheckHealthAsync_ConvertsExceptionInHealthCheckerToFailedResultAsync()
{
// Arrange
var thrownException = new InvalidOperationException("Whoops!");
var faultedException = new InvalidOperationException("Ohnoes!");
var service = new HealthCheckService(new[]
{
new HealthCheck("Throws", ct => throw thrownException),
new HealthCheck("Faults", ct => Task.FromException<HealthCheckResult>(faultedException)),
new HealthCheck("Succeeds", ct => Task.FromResult(HealthCheckResult.Healthy())),
});
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results,
actual =>
{
Assert.Equal("Throws", actual.Key);
Assert.Equal(thrownException.Message, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status);
Assert.Same(thrownException, actual.Value.Exception);
},
actual =>
{
Assert.Equal("Faults", actual.Key);
Assert.Equal(faultedException.Message, actual.Value.Description);
Assert.Equal(HealthCheckStatus.Failed, actual.Value.Status);
Assert.Same(faultedException, actual.Value.Exception);
},
actual =>
{
Assert.Equal("Succeeds", actual.Key);
Assert.Empty(actual.Value.Description);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
Assert.Null(actual.Value.Exception);
});
}
[Fact]
public async Task CheckHealthAsync_SetsUpALoggerScopeForEachCheck()
{
// Arrange
var sink = new TestSink();
var check = new HealthCheck("TestScope", cancellationToken =>
{
Assert.Collection(sink.Scopes,
actual =>
{
Assert.Equal(actual.LoggerName, typeof(HealthCheckService).FullName);
Assert.Collection((IEnumerable<KeyValuePair<string, object>>)actual.Scope,
item =>
{
Assert.Equal("HealthCheckName", item.Key);
Assert.Equal("TestScope", item.Value);
});
});
return Task.FromResult(HealthCheckResult.Healthy());
});
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
var service = new HealthCheckService(new[] { check }, loggerFactory.CreateLogger<HealthCheckService>());
// Act
var results = await service.CheckHealthAsync();
// Assert
Assert.Collection(results.Results, actual =>
{
Assert.Equal("TestScope", actual.Key);
Assert.Equal(HealthCheckStatus.Healthy, actual.Value.Status);
});
}
[Fact]
public async Task CheckHealthAsync_ThrowsIfCheckReturnsUnknownStatusResult()
{
// Arrange
var service = new HealthCheckService(new[]
{
new HealthCheck("Kaboom", ct => Task.FromResult(default(HealthCheckResult))),
});
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.CheckHealthAsync());
// Assert
Assert.Equal("Health check 'Kaboom' returned a result with a status of Unknown", ex.Message);
}
}
}

View File

@ -0,0 +1,30 @@
// 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.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class HealthChecksBuilderTests
{
[Fact]
public void ChecksCanBeRegisteredInMultipleCallsToAddHealthChecks()
{
var services = new ServiceCollection();
services.AddHealthChecks()
.AddCheck("Foo", () => Task.FromResult(HealthCheckResult.Healthy()));
services.AddHealthChecks()
.AddCheck("Bar", () => Task.FromResult(HealthCheckResult.Healthy()));
// Act
var healthCheckService = services.BuildServiceProvider().GetRequiredService<IHealthCheckService>();
// Assert
Assert.Collection(healthCheckService.Checks,
actual => Assert.Equal("Foo", actual.Key),
actual => Assert.Equal("Bar", actual.Key));
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
<RootNamespace>Microsoft.Extensions.Diagnostics.HealthChecks</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.Diagnostics.HealthChecks\Microsoft.Extensions.Diagnostics.HealthChecks.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.Extensions.Diagnostics.HealthChecks
{
public class ServiceCollectionExtensionsTests
{
[Fact]
public void AddHealthChecks_RegistersSingletonHealthCheckServiceIdempotently()
{
// Arrange
var services = new ServiceCollection();
// Act
services.AddHealthChecks();
services.AddHealthChecks();
// Assert
Assert.Collection(services,
actual =>
{
Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime);
Assert.Equal(typeof(IHealthCheckService), actual.ServiceType);
Assert.Equal(typeof(HealthCheckService), actual.ImplementationType);
Assert.Null(actual.ImplementationInstance);
Assert.Null(actual.ImplementationFactory);
});
}
}
}