diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index 655c4fcb5a..28e6a87e8a 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26923.0 MinimumVisualStudioVersion = 15.0.26730.03 @@ -56,6 +56,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Diagno 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore", "src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj", "{61F8B71C-4BDA-431C-9485-743D619ACF9A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests", "test\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj", "{4416D921-0E43-496D-9156-D7EAA9CB8DD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -344,6 +348,30 @@ Global {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 + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Debug|x86.Build.0 = Debug|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Any CPU.Build.0 = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|x86.ActiveCfg = Release|Any CPU + {61F8B71C-4BDA-431C-9485-743D619ACF9A}.Release|x86.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|x86.ActiveCfg = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Debug|x86.Build.0 = Debug|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Any CPU.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|x86.ActiveCfg = Release|Any CPU + {4416D921-0E43-496D-9156-D7EAA9CB8DD3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -373,6 +401,8 @@ Global {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} + {61F8B71C-4BDA-431C-9485-743D619ACF9A} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} + {4416D921-0E43-496D-9156-D7EAA9CB8DD3} = {2AF90579-B118-4583-AE88-672EFACB5BC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D915AA7B-4ADE-4BAC-AF65-1E800D3F3580} diff --git a/samples/HealthChecksSample/DBHealthStartup.cs b/samples/HealthChecksSample/DBHealthStartup.cs index 43dd6abda6..46639d26ea 100644 --- a/samples/HealthChecksSample/DBHealthStartup.cs +++ b/samples/HealthChecksSample/DBHealthStartup.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.DependencyInjection; namespace HealthChecksSample { // Pass in `--scenario db` at the command line to run this sample. - public class DBHealthStartup + public class DbHealthStartup { - public DBHealthStartup(IConfiguration configuration) + public DbHealthStartup(IConfiguration configuration) { Configuration = configuration; } diff --git a/samples/HealthChecksSample/DbContextHealthStartup.cs b/samples/HealthChecksSample/DbContextHealthStartup.cs new file mode 100644 index 0000000000..62167d2ed5 --- /dev/null +++ b/samples/HealthChecksSample/DbContextHealthStartup.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace HealthChecksSample +{ + // Pass in `--scenario dbcontext` at the command line to run this sample. + public class DbContextHealthStartup + { + public DbContextHealthStartup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Registers required services for health checks + services.AddHealthChecks() + + // Registers a health check for the MyContext type. By default the name of the health check will be the + // name of the DbContext type. There are other options available through AddDbContextCheck to configure + // failure status, tags, and custom test query. + .AddDbContextCheck(); + + // Registers the MyContext type and configures the database provider. + // + // The health check added by AddDbContextCheck will create instances of MyContext from the service provider, + // and so will reuse the configuration provided here. + services.AddDbContext(options => + { + options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]); + }); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + // This will register the health checks middleware at the URL /health. + // + // Since this sample doesn't do anything to create the database by default, this will + // return unhealthy by default. + // + // You can to to /createdatabase and /deletedatabase to create and delete the database + // (respectively), and see how it immediately effects the health status. + // + app.UseHealthChecks("/health"); + + app.Map("/createdatabase", b => b.Run(async (context) => + { + await context.Response.WriteAsync("Creating the database...\n"); + await context.Response.Body.FlushAsync(); + + var myContext = context.RequestServices.GetRequiredService(); + await myContext.Database.EnsureCreatedAsync(); + + await context.Response.WriteAsync("Done\n"); + await context.Response.WriteAsync("Go to /health to see the health status\n"); + })); + + app.Map("/deletedatabase", b => b.Run(async (context) => + { + await context.Response.WriteAsync("Deleting the database...\n"); + await context.Response.Body.FlushAsync(); + + var myContext = context.RequestServices.GetRequiredService(); + await myContext.Database.EnsureDeletedAsync(); + + await context.Response.WriteAsync("Done\n"); + await context.Response.WriteAsync("Go to /health to see the health status\n"); + })); + + app.Run(async (context) => + { + await context.Response.WriteAsync("Go to /health to see the health status\n"); + await context.Response.WriteAsync("Go to /createdatabase to create the database\n"); + await context.Response.WriteAsync("Go to /deletedatabase to create the database\n"); + }); + } + } +} diff --git a/samples/HealthChecksSample/HealthChecksSample.csproj b/samples/HealthChecksSample/HealthChecksSample.csproj index b013e687fa..05b9c50176 100644 --- a/samples/HealthChecksSample/HealthChecksSample.csproj +++ b/samples/HealthChecksSample/HealthChecksSample.csproj @@ -2,8 +2,8 @@ - netcoreapp2.0;net461 - netcoreapp2.0 + netcoreapp2.2;net461 + netcoreapp2.2 @@ -12,12 +12,14 @@ + + diff --git a/samples/HealthChecksSample/MyContext.cs b/samples/HealthChecksSample/MyContext.cs new file mode 100644 index 0000000000..1cfbeaab25 --- /dev/null +++ b/samples/HealthChecksSample/MyContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; + +namespace HealthChecksSample +{ + public class MyContext : DbContext + { + public MyContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Blog { get; set; } + } + + public class Blog + { + public int BlogId { get; set; } + public string Url { get; set; } + } +} diff --git a/samples/HealthChecksSample/Program.cs b/samples/HealthChecksSample/Program.cs index 0ed4ff6e3a..56e6e6478f 100644 --- a/samples/HealthChecksSample/Program.cs +++ b/samples/HealthChecksSample/Program.cs @@ -15,12 +15,13 @@ namespace HealthChecksSample { _scenarios = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "", typeof(CustomWriterStartup) }, + { "", typeof(BasicStartup) }, { "basic", typeof(BasicStartup) }, { "writer", typeof(CustomWriterStartup) }, { "liveness", typeof(LivenessProbeStartup) }, { "port", typeof(ManagementPortStartup) }, - { "db", typeof(DBHealthStartup) }, + { "db", typeof(DbHealthStartup) }, + { "dbcontext", typeof(DbContextHealthStartup) }, }; } diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs new file mode 100644 index 0000000000..9d581e89d3 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheck.cs @@ -0,0 +1,56 @@ +// 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.EntityFrameworkCore; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal sealed class DbContextHealthCheck : IHealthCheck where TContext : DbContext + { + private static readonly Func> DefaultTestQuery = (dbContext, cancellationToken) => + { + return dbContext.Database.CanConnectAsync(cancellationToken); + }; + + private readonly TContext _dbContext; + private readonly IOptionsMonitor> _options; + + public DbContextHealthCheck(TContext dbContext, IOptionsMonitor> options) + { + if (dbContext == null) + { + throw new ArgumentNullException(nameof(dbContext)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _dbContext = dbContext; + _options = options; + } + + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var options = _options.Get(context.Registration.Name); + var testQuery = options.CustomTestQuery ?? DefaultTestQuery; + + if (await testQuery(_dbContext, cancellationToken)) + { + return HealthCheckResult.Passed(); + } + + return HealthCheckResult.Failed(); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs new file mode 100644 index 0000000000..7fb330c376 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DbContextHealthCheckOptions.cs @@ -0,0 +1,15 @@ +// 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.EntityFrameworkCore; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + internal sealed class DbContextHealthCheckOptions where TContext : DbContext + { + public Func> CustomTestQuery { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs new file mode 100644 index 0000000000..bf299bc6b0 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensions.cs @@ -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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class EntityFrameworkCoreHealthChecksBuilderExtensions + { + /// + /// Adds a health check for the specified type. + /// + /// The type. + /// The . + /// + /// The health check name. Optional. If null the type name of will be used for the name. + /// + /// + /// The that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// + /// A list of tags that can be used to filter sets of health checks. Optional. + /// + /// A custom test query that will be executed when the health check executes to test the health of the database + /// connection and configurations. + /// + /// The . + /// + /// + /// The health check implementation added by this method will use the dependency injection container + /// to create an instance of . + /// + /// + /// By default the health check implementation will use the method + /// to test connectivity to the database. This method requires that the database provider has correctly implemented the + /// interface. If the database provide has not implemented this interface + /// then the health check will report a failure. + /// + /// + /// Providing a will replace the use of + /// to test database connectivity. An implementation of a test query should handle exceptions that can arise due to connectivity failure, + /// and should return a pass/fail result. The test query should be be designed to complete in a short and predicatable amount of time. + /// + /// + public static IHealthChecksBuilder AddDbContextCheck( + this IHealthChecksBuilder builder, + string name = null, + HealthStatus? failureStatus = default, + IEnumerable tags = default, + Func> customTestQuery = default) + where TContext : DbContext + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (name == null) + { + name = typeof(TContext).Name; + } + + if (customTestQuery != null) + { + builder.Services.Configure>(name, options => options.CustomTestQuery = customTestQuery); + } + + return builder.AddCheck>(name, failureStatus, tags); + } + } +} diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj new file mode 100644 index 0000000000..6a9c3cf5b9 --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj @@ -0,0 +1,23 @@ + + + + + Components for performing health checks using EntityFrameworkCore. + + netstandard2.0 + $(NoWarn);CS1591 + true + diagnostics;healthchecks;entityframeworkcore + Microsoft.Extensions.Diagnostics.HealthChecks + + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1aa256d83a --- /dev/null +++ b/src/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs new file mode 100644 index 0000000000..23b17d4939 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DbContextHealthCheckTest.cs @@ -0,0 +1,96 @@ +// 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.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.HealthChecks +{ + public class DbContextHealthCheckTest + { + // Just testing pass here since it would be complicated to simulate a failure. All of that logic lives in EF anyway. + [Fact] + public async Task CheckAsync_DefaultTest_Pass() + { + // Arrange + var services = CreateServices(); + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.True(result.Result, "Health check passed"); + } + } + + [Fact] + public async Task CheckAsync_CustomTest_Pass() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Add a blog so that the custom test passes + var context = scope.ServiceProvider.GetRequiredService(); + context.Add(new Blog()); + await context.SaveChangesAsync(); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.True(result.Result, "Health check passed"); + } + } + + + [Fact] + public async Task CheckAsync_CustomTest_Fail() + { + // Arrange + var services = CreateServices(async (c, ct) => + { + return 0 < await c.Blogs.CountAsync(); + }); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var registration = Assert.Single(services.GetRequiredService>().Value.Registrations); + var check = ActivatorUtilities.CreateInstance>(scope.ServiceProvider); + + // Act + var result = await check.CheckHealthAsync(new HealthCheckContext() { Registration = registration, }); + + // Assert + Assert.False(result.Result, "Health check failed"); + } + } + + private static IServiceProvider CreateServices(Func> testQuery = null) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDbContext(o => o.UseInMemoryDatabase("Test")); + + var builder = serviceCollection.AddHealthChecks(); + builder.AddDbContextCheck("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, testQuery); + return serviceCollection.BuildServiceProvider(); + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs new file mode 100644 index 0000000000..9efcb025e0 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/DependencyInjection/EntityFrameworkCoreHealthChecksBuilderExtensionsTest.cs @@ -0,0 +1,47 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + public class EntityFrameworkCoreHealthChecksBuilderExtensionsTest + { + [Fact] + public void AddDbContextCheck_RegistersDbContextHealthCheck() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddDbContext(o => o.UseInMemoryDatabase("Test")); + + var builder = serviceCollection.AddHealthChecks(); + + // Act + builder.AddDbContextCheck("test", HealthStatus.Degraded, new[] { "tag1", "tag2", }, (c, ct) => Task.FromResult(true)); + + // Assert + var services = serviceCollection.BuildServiceProvider(); + + var registrations = services.GetRequiredService>().Value.Registrations; + + var registration = Assert.Single(registrations); + Assert.Equal("test", registration.Name); + Assert.Equal(HealthStatus.Degraded, registration.FailureStatus); + Assert.Equal(new[] { "tag1", "tag2", }, registration.Tags.ToArray()); + + var options = services.GetRequiredService>>(); + Assert.NotNull(options.Get("test").CustomTestQuery); + + using (var scope = services.GetRequiredService().CreateScope()) + { + var check = Assert.IsType>(registration.Factory(scope.ServiceProvider)); + } + } + } +} diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj new file mode 100644 index 0000000000..be5627dca4 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(StandardTestTfms) + Microsoft.AspNetCore.Diagnostics.HealthChecks + + + + + + + + + + + + + + + diff --git a/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs new file mode 100644 index 0000000000..e38ed93674 --- /dev/null +++ b/test/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.Tests/TestDbContext.cs @@ -0,0 +1,24 @@ +// 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.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.Diagnostics.HealthChecks +{ + public class TestDbContext : DbContext + { + public TestDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Blogs { get; set; } + } + + public class Blog + { + public int Id { get; set; } + + public int Name { get; set; } + } +}