Description
Since starting the discussion below, I have confirmed my findings.
Today Health Checks don't fully support DI, even though they are advertised as supporting DI.
The reason they don't fully support DI is simple:
By default, due to this line of ASP.NET code, every health check is invoked in a new scope. That means that state information inside of any scoped services will never be available to a health check that consumes that service via DI. This becomes an issue if your authorization layer updates scoped service state with the expectancy that said state information will be available to other services (or health checks) that consume that scoped service. It doesn't matter if you invoke your health checks using HealthCheckService.CheckHealthAsync
nor if you define your health checks using AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization
, the result is the same -- a new scope is created, eliminating any utility offered through the use of scoped services before the health check was invoked.
There is no documentation that highlights this DI limitation with health checks.
The following workarounds may resolve this issue, but only for some scenarios, and it is dependent on the implementation:
- use some other way to pass state (
HttpContext.Items
,HttpRequest.Headers
,ClaimsPrincipal.Claims
, etc.) - create a copy of the internal
DefaultHealthCheckService
class, register your copy as a singleton, update it such that it does not create a new scope for every health check, and inject that instead of injectingHealthCheckService
when usingHealthCheckService.CheckHealthAsync
The former workaround is not always possible (highly dependent on how you are invoking health checks), and often undesirable (state information that is only required internally within a service shouldn't unnecessarily be stored in headers or claims).
The latter workaround resolves the issue for health checks invoked using HealthCheckService.CheckHealthAsync
; however, it does nothing for health checks configured with RequireAuthorization
where the authorization layer uses scoped services that are later referenced by a health check, because the default behavior for health checks remains the same.
In my testing, I've applied the latter workaround, and for my purposes it works very well. The significant downside is that I've had to copy 337 lines of ASP.NET code just to remove two of them and change one other.
I understand that this is an infrequent issue (use of DI/authorization in health checks is uncommon). I also understand that the current behavior is likely due to a desire to keep code simple (since Health Checks are invoked in parallel, using a new scope for each one eliminates the need to worry about scoped services not using constructs like AsyncLocal
or other thread safety measures where appropriate). That said, I would still like to see this issue addressed, ideally by providing options allowing devs to have their health checks run using DI scoped services just like any other service can, with state being respected.
What resolution would you be willing to consider?
I can create a PR with whatever enhancements are agreed upon to support this scenario, which ideally would allow someone like me to do what I'm doing without having to copy the bulk of the DefaultHealthCheckService
code like I'm doing now. I'm not looking to change any defaults here -- I'm simply looking for extensibility options that allow devs who are using DI and authorization in health checks to be able to depend on them working just like DI does in other services, with docs explaining that those options mean the services that are DI'ed into the health checks involved must be able to run safely when executed in parallel.
Thanks for your time.
Discussed in #52086
Originally posted by KirkMunroSagent November 15, 2023
I'm facing a challenge with Health Checks and scope and I'm not quite sure what to do about it. My challenge seems to come down to this line of ASP.NET code.
Here's the issue:
We're using DI in Health Checks. I'm invoking Health Checks programmatically by DI'ing HealthCheckService
into a controller and calling its CheckHealthAsync
method. The controller method in question uses a policy that has a requirement authorization handler which references a scoped DI service that identifies who the caller is. That scoped "caller id" service is used later on in other services to get the caller id information, and since it's scoped, the information is available; however, if I try to get the caller id information from the same service DI'ed into my Health Check, the information is not available seemingly because of the line in question above, which creates a new scope for each health check that is run. Why create a new scope for every health check that is identified to be run according to the predicate?
Is the answer that Health Checks "support" DI, meaning that DI is only properly supported when the health check in question is invoked via ASP.NET's internal uris, but if you invoke Health Checks via the public HealthCheckService.CheckHealthAsync
method, then your DI scope is reset for each health check, which may break how scoped DI works in some scenarios because services are injected in a brand new scope instead of being used in the current scope, so you'll lose any information that was being retained in a scope-injected DI service before you programmatically invoked the health check?
This seems like a bug to me.
If I invoke a health check via an endpoint that has a DI'ed HealthCheckService.CheckHealthAsync
, that should be done using my current scope from the endpoint invocation instead of creating a new scope for each health check. Even if I invoke a set of health checks using an internal uri and ASP.NET's default invocation mechanism, I would have expected that to be done using a single scope for all health checks that are run instead of creating a new scope for each health check that is to be run based on the predicate that was passed in. Anything else seems contrary to how scoping and DI is designed to work, no? Otherwise the Health Check is DI'ing its services from a different container, losing access to anything scoped that may have been updated during authentication or during the invocation of the endpoint itself if it did any work with scoped services that it expected would be available to the Health Checks that it is about to run.
All of that said, does anyone have any idea how I can preserve my current scope when running health checks programmatically? I can always create my own ScopedHealthCheckService
and DI that instead of HealthCheckService
to perform the execution within the same scope, but I'd really prefer not copying all of that code just to skip that one line.