Skip to content

Commit

Permalink
Register SQL/Redis Health Checks (#1422)
Browse files Browse the repository at this point in the history
* Removed view and unused "using" statements

* Map healthchecks to /health

* Add Redis to Health Check

* Add DbContextChecks to Health Checks

This will ensure that the healthcheck endpoint correctly reports the status of the service if a database connection faults

* Moved package into Api project

* Remove unnecessary call to AddHealthChecks()

* This bit isn't necessary as we are already registering it on the Sql and Redis servers

* Register /health endpoint on App and API
  • Loading branch information
DrizzlyOwl authored Dec 9, 2024
1 parent ff2acc6 commit 1e73390
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 417 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.8" />
</ItemGroup>

<ItemGroup>
Expand Down
103 changes: 53 additions & 50 deletions ConcernsCaseWork/ConcernsCaseWork.API/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,53 @@
using ConcernsCaseWork.API.Extensions;
using ConcernsCaseWork.API.Middleware;
using ConcernsCaseWork.API.StartupConfiguration;
using ConcernsCaseWork.Middleware;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace ConcernsCaseWork.API
{
/// <summary>
/// THIS STARTUP ISN'T USED WHEN API IS HOSTED THROUGH WEBSITE. It is used when running API tests
/// </summary>
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddConcernsApiProject(Configuration);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseConcernsCaseworkSwagger(provider);

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseMiddleware<UrlDecoderMiddleware>();
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<UserContextReceiverMiddleware>();

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseConcernsCaseworkEndpoints();
}
}
}
using ConcernsCaseWork.API.Extensions;
using ConcernsCaseWork.API.Middleware;
using ConcernsCaseWork.API.StartupConfiguration;
using ConcernsCaseWork.Middleware;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace ConcernsCaseWork.API
{
/// <summary>
/// THIS STARTUP ISN'T USED WHEN API IS HOSTED THROUGH WEBSITE. It is used when running API tests
/// </summary>
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddConcernsApiProject(Configuration);
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseConcernsCaseworkSwagger(provider);

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseMiddleware<UrlDecoderMiddleware>();
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<UserContextReceiverMiddleware>();

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseConcernsCaseworkEndpoints();

// Add Health Checks
app.UseHealthChecks("/health");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using ConcernsCaseWork.Data;

namespace ConcernsCaseWork.API.StartupConfiguration;

public static class DatabaseConfigurationExtensions
{
public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<ConcernsDbContext>(options =>
options.UseConcernsSqlServer(connectionString)
);

return services;
}
}
using ConcernsCaseWork.Data;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace ConcernsCaseWork.API.StartupConfiguration;

public static class DatabaseConfigurationExtensions
{
public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<ConcernsDbContext>(options =>
options.UseConcernsSqlServer(connectionString)
);
services.AddHealthChecks()
.AddDbContextCheck<ConcernsDbContext>("Concerns Database");

return services;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{
"Endpoints": [
{
"Route": "/Health",
"ExpectedSecurity": "AllowAnonymous"
},
{
"Route": "/AccessDenied",
"ExpectedSecurity": "AllowAnonymous"
Expand Down Expand Up @@ -33,4 +29,4 @@
"ExpectedSecurity": "AllowAnonymous"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,55 @@
using ConcernsCaseWork.Pages;
using ConcernsCaseWork.Pages.Base;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using NUnit.Framework;
using System;
using System.Linq;
using System.Reflection;

namespace ConcernsCaseWork.Tests.Security
{
public class AuthorizeAttributeTests
{
public AuthorizeAttributeTests()
{
this.UnauthorizedPages = new Type[]
{
typeof(HealthModel),
typeof(ErrorPageModel),
};
}

public Type[] UnauthorizedPages { get; }

[Test]
public void All_Pages_Include_Authorize_Attribute()
{
var pages = this.GetAllPagesExceptUnauthorizedPages();

pages.Length.Should().BeGreaterThan(0);

using (var scope = new AssertionScope())
{
foreach (Type page in pages)
{
var authAttributes = page.GetCustomAttributes<AuthorizeAttribute>();
if (authAttributes == null || !authAttributes.Any())
{
scope.AddPreFormattedFailure($"Could not find [Authorize] attribute and no exemption for the following page type: {page.Name} ({page.FullName})");
}
}
}
}

[Test]
public void Open_Pages_Do_Not_Require_Authorization()
{
Type[] UnauthorizedPages = new[]
{
typeof(HealthModel),
};

using (var scope = new AssertionScope())
{
foreach (Type page in this.UnauthorizedPages)
{
var authAttribute = page.GetCustomAttribute<AuthorizeAttribute>();
if (authAttribute != null)
{
scope.AddPreFormattedFailure($"Expected page to be open and not require authorisation. But found [Authorize] attribute on the following page type: {page.Name} ({page.FullName})");
}
}
}
}

private Type[] GetAllPagesExceptUnauthorizedPages()
{
return Assembly
.GetAssembly(typeof(AbstractPageModel))
.GetTypes()
.Where(x => x.IsAssignableTo(typeof(PageModel)) && !this.UnauthorizedPages.Contains(x))
.ToArray();
}
}
}
using ConcernsCaseWork.Pages;
using ConcernsCaseWork.Pages.Base;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using NUnit.Framework;
using System;
using System.Linq;
using System.Reflection;

namespace ConcernsCaseWork.Tests.Security
{
public class AuthorizeAttributeTests
{
public AuthorizeAttributeTests()
{
this.UnauthorizedPages = new Type[]
{
typeof(ErrorPageModel),
};
}

public Type[] UnauthorizedPages { get; }

[Test]
public void All_Pages_Include_Authorize_Attribute()
{
var pages = this.GetAllPagesExceptUnauthorizedPages();

pages.Length.Should().BeGreaterThan(0);

using (var scope = new AssertionScope())
{
foreach (Type page in pages)
{
var authAttributes = page.GetCustomAttributes<AuthorizeAttribute>();
if (authAttributes == null || !authAttributes.Any())
{
scope.AddPreFormattedFailure($"Could not find [Authorize] attribute and no exemption for the following page type: {page.Name} ({page.FullName})");
}
}
}
}

private Type[] GetAllPagesExceptUnauthorizedPages()
{
return Assembly
.GetAssembly(typeof(AbstractPageModel))
.GetTypes()
.Where(x => x.IsAssignableTo(typeof(PageModel)) && !this.UnauthorizedPages.Contains(x))
.ToArray();
}
}
}
1 change: 1 addition & 0 deletions ConcernsCaseWork/ConcernsCaseWork/ConcernsCaseWork.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="8.0.1" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public static void AddRedis(this IServiceCollection services, IConfiguration con
options.ConnectionMultiplexerFactory = () => Task.FromResult(_redisConnectionMultiplexer);
});

services.AddHealthChecks().AddRedis(_redisConnectionMultiplexer);
}
catch (Exception ex)
{
Expand Down
8 changes: 0 additions & 8 deletions ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml

This file was deleted.

20 changes: 0 additions & 20 deletions ConcernsCaseWork/ConcernsCaseWork/Pages/Health.cshtml.cs

This file was deleted.

47 changes: 23 additions & 24 deletions ConcernsCaseWork/ConcernsCaseWork/Program.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;


namespace ConcernsCaseWork
{
public class Program
{
public static void Main(string[] args)
{

CreateHostBuilder(args).Build().Run();
}

private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureKestrel(options => options.AddServerHeader = false)
.UseStartup<Startup>();
});
}
}
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;


namespace ConcernsCaseWork
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureKestrel(options => options.AddServerHeader = false)
.UseStartup<Startup>();
});
}
}
Loading

0 comments on commit 1e73390

Please sign in to comment.