Aspire.Npgsql.EntityFrameworkCore.PostgreSQL healthchecks ignore configureDbContextOptions
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 201
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Describe the bug
This issue is linked to #17599
Due to the fact that `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` does not allow to use environment variables out of the box, I've written my own connection string builder and gave it to `AddNpgsqlDbContext`. As expected, this registers database healthchecks using `AddDbContextCheck`. However, when executing the check, it fails because the connection string is missing from the configuration.
Could this be caused by `AddNpgsqlDbContext` registering a pooled DbContext whereas `DbContextHealthCheck` resolves a non-pooled instance ?
I'm not sure if this is a bug / missing feature of `HealthChecks.EntityFrameworkCore` or `AddNpgsqlDbContext` not registering the checks correctly.
### Expected Behavior
`DbContextHealthCheck` using a DbContext configured with my custom `ConfigureDbContextOptions` method.
### Steps To Reproduce
* `dotnet run` the following file
* `GET http://localhost:5000/health`
```cs
#:sdk Microsoft.NET.Sdk.Web
#:package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL@13.3.3
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
ConversationsDbContext.Register(builder);
var app = builder.Build();
app.MapHealthChecks("/health");
await app.RunAsync();
internal class ConversationsDbContext(DbContextOptions options) : DbContext(options)
{
private const int DefaultPort = 5432;
private static Npgsql.NpgsqlConnectionStringBuilder ConnectionStringBuilder => new()
{
Host = Environment.GetEnvironmentVariable("CONVERSATIONS_DB_HOST"),
Port = int.TryParse(Environment.GetEnvironmentVariable("CONVERSATIONS_DB_PORT"), out int result) ? result : DefaultPort,
Username = Environment.GetEnvironmentVariable("CONVERSATIONS_DB_USERNAME"),
Password = Environment.GetEnvironmentVariable("CONVERSATIONS_DB_PASSWORD"),
Database = Environment.GetEnvironmentVariable("CONVERSATIONS_DB_DATABASENAME")
};
private static void ConfigureDbContextOptions(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql(ConnectionStringBuilder.ConnectionString);
public static void Register(WebApplicationBuilder webApplicationBuilder)
=> webApplicationBuilder.AddNpgsqlDbContext(
connectionName: "conversations-db",
configureDbContextOptions: ConfigureDbContextOptions);
}
```
### Exceptions (if any)
```
System.InvalidOperationException: ConnectionString is missing. It should be provided in 'ConnectionStrings:conversations-db' or under the 'ConnectionString' key in 'Aspire:Npgsql:EntityFrameworkCore:PostgreSQL' or 'Aspire:Npgsql:EntityFrameworkCore:PostgreSQL:ConversationsDbContext' configuration section.
at Aspire.ConnectionStringValidation.ValidateConnectionString(String connectionString, String connectionName, String defaultConfigSectionName, String typeSpecificSectionName, Boolean isEfDesignTime) in /_/src/Components/Common/ConnectionStringValidation.cs:line 16
at Microsoft.Extensions.Hosting.AspireEFPostgreSqlExtensions.<>c__DisplayClass2_0`1.g__ConfigureDbContext|1(DbContextOptionsBuilder dbContextOptionsBuilder) in /_/src/Components/Aspire.Npgsql.EntityFrameworkCore.PostgreSQL/AspireEFPostgreSqlExtensions.cs:line 76
at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass3_0`2.b__0(IServiceProvider _, DbContextOptionsBuilder ob)
at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__DisplayClass6_0`1.b__0(IServiceProvider sp, DbContextOptionsBuilder ob)
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.DbContextOptionsConfiguration`1.Configure(IServiceProvider serviceProvider, DbContextOptionsBuilder optionsBuilder)
at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.CreateDbContextOptions[TContext](IServiceProvider applicationServiceProvider)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.<>c__5`2.b__5_0(IServiceProvider sp)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance[T](IServiceProvider provider)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(HealthCheckRegistration registration, CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(HealthCheckRegistration registration, CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
```
### Aspire doctor output
Vérification de l’environnement Aspire
======================================
Kit de développement logiciel (SDK) .NET
✅ .NET 10.0.300 installed (x64)
Runtime de conteneur
⚠️ Docker client version 27.5.1 is below minimum required 28.0.0 ← active
Upgrade Docker to version 28.0.0 or later from: https://www.docker.com/products/docker-desktop
See: https://aka.ms/aspire/containers
⚠️ Podman: installed but not running
Start Podman service: sudo systemctl start podman
Environnement
✅ HTTPS development certificate is trusted
⚠️ HTTPS development certificate has an older version (v5, v4)
Run 'aspire certs clean' to remove all certificates, then run 'aspire certs trust' to create and trust a new one.
See: https://aka.ms/aspire-prerequisites#dev-certs
Details:
Older certificate versions (< v4) may not support all certificate trust scenarios.
Résumé : 2 réussis, 3 avertissements, 0 échoués
For detailed prerequisites: https://aka.ms/aspire-prerequisites
### Anything else?
_No response_
Contributor guide
Research direction
Run the inline C# reproduction and inspect AspireEFPostgreSqlExtensions.cs at the AddNpgsqlDbContext configuration shown in the stack trace, along with ConnectionStringValidation.cs. Trace how the health check creates the DbContext and compare that path with configureDbContextOptions. Done means GET /health succeeds while using the custom connection configuration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, postgresql
- Domain
- backend, databases, observability
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 57/100