microsoft / microsoft/aspire

Aspire.Npgsql.EntityFrameworkCore.PostgreSQL healthchecks ignore configureDbContextOptions

Open
#17,603 2 comments 0 reactions 0 assignees View on GitHub
area-integrations triage:bot-seen
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.