fix(pgsql): opt into Npgsql dynamic JSON so ProfileData reads work again — PRODUCTION OUTAGE - #131
Conversation
Production login returns HTTP 500: InvalidCastException: Reading as 'IEnumerable<ProfileDataItem>' is not supported for fields having DataTypeName 'jsonb' ---> NotSupportedException: Type 'IEnumerable`1' required dynamic JSON serialization, which requires an explicit opt-in; call 'EnableDynamicJson' Regression from #129, which moved Npgsql 5.0.10 -> 8.0.11 to fix the EF Core TypeLoadException. Npgsql 8 removed the implicit dynamic JSON serializer: mapping an arbitrary POCO collection to a json/jsonb column now requires EnableDynamicJson() on the data source. Three properties rely on it via StoreAsJson() — Tenant.ProfileData, TenantMembership.ProfileData and Account.ProfileData — and account.profile_data is jsonb in production. The blast radius is wider than login: UseApiKeyAsRequestContext materialises Account on every apikey-authenticated request, so ANY such request 500s. Startup.cs now builds a single NpgsqlDataSource with EnableDynamicJson() and hands it to UseNpgsql. It is built once outside the AddDbContext lambda — NpgsqlDataSource owns the connection pool, so constructing one per DbContext instance would leak pools. Verified A/B against the REAL production database, same request, same data: ghcr.io/simplify9/mtm:8.0.3 GET /api/accounts -> 500 (8 exceptions) POST .../login -> 500 this build GET /api/accounts -> 200 (0 exceptions, real account data) POST .../login -> 400 (handler reached, bad password rejected) No schema changes were applied during the test — the 2021 migrations are already present, so startup migration is a no-op. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHkpqv6dB6wjALyFe9ocMU
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: simplify9/coderabbit/.coderabbit.yaml Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWhat changed
Riskrisk:low The change affects PostgreSQL startup and data access configuration. It does not change schemas, writes, or application authorization logic. Security-sensitive areasAPI-key-authenticated requests and login previously failed when reading profile data. The change restores request processing but does not alter authentication or authorization behavior. Test coverage impactProduction verification confirmed:
No automated test coverage changes were reported. Deployment and operational concerns
WalkthroughThe PostgreSQL startup path now builds one shared ChangesPostgreSQL data source
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested labels: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔴 Production outage — fixes it
This is a regression from #129, which moved Npgsql
5.0.10 → 8.0.11to fix the EF CoreTypeLoadException. That fixed startup and introduced this: Npgsql 8 removed the implicit dynamic JSON serializer. Mapping an arbitrary POCO collection to ajson/jsonbcolumn now needsEnableDynamicJson()on the data source.Three properties depend on it via
StoreAsJson()—Tenant.ProfileData,TenantMembership.ProfileData,Account.ProfileData— andmtm.account.profile_dataisjsonbin production.The blast radius is wider than login.
UseApiKeyAsRequestContextmaterialisesAccounton everyapikey-authenticated request, so any such request 500s — not just/accounts/login.Fix
Startup.csbuilds a singleNpgsqlDataSourcewithEnableDynamicJson()and passes it toUseNpgsql. Built once, outside theAddDbContextlambda:NpgsqlDataSourceowns the connection pool, so constructing one perDbContextinstance would leak pools.18 lines, one file, PgSql path only — the MySql and MsSql branches are untouched.
Verification — A/B against the real production database
Identical environment, identical request, same data:
GET /api/accountsPOST /api/accounts/loginghcr.io/simplify9/mtm:8.0.3(live now)The
400is the correct result: it proves the request got past the jsonb read and into the handler, which then rejected a deliberately wrong password.No writes to production. The 2021 migrations are already applied, so startup migration was a no-op — confirmed, no
Applying migration/ DDL in the logs.Not the connection string
Worth recording, since that was the initial hypothesis: the secret parses correctly (
Database=funride-prod, correct host), the pod isReadywith 0 restarts, and/mtm/healthreturns 200. The app connects and queries fine — only the jsonb materialisation fails.After merge
CI publishes
8.0.4. Pinfunride-mtm'scicd-gateway.ymlto it and redeploy Prod. I'll verify over the wire.🤖 Generated with Claude Code
https://claude.ai/code/session_01VHkpqv6dB6wjALyFe9ocMU