From b7ad11b4747a7795d5c55df624692bda925fbdf4 Mon Sep 17 00:00:00 2001 From: Bernhard Windisch Date: Sun, 28 Jun 2026 10:37:48 +0200 Subject: [PATCH] fix(codegen): in-memory (Dynamic) handler codegen in Production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published container ran Wolverine codegen with TypeLoadMode.Auto. On the first hit of every handler it generated the code dynamically (Roslyn) and then tried to persist the source to /app/Internal — which the non-root `app` user cannot write — logging "Access to the path '/app/Internal' is denied". The write is non-fatal but noisy, and Auto's persisted source is never recompiled into the running image, so it bought nothing in a container. Switch Production to TypeLoadMode.Dynamic: handlers are compiled in memory and never written to disk, so the error and the failed write are gone. Dev and the Testing-env integration suite keep Auto (writable working tree), so they are unchanged. Note on the road not taken: pre-generating at build time + TypeLoadMode.Static would also remove the per-handler cold-compile, but it requires JasperFx's `RunJasperFxCommands` entry point. Any reference to that API in this assembly wires JasperFx into the host resolution that WebApplicationFactory drives, which tears the integration-test host down before it initializes ("Server hasn't been initialized yet"); the official `JasperFxEnvironment.AutoStartHost` workaround in turn starts hosted services before the imperative multi-tenant bootstrap has provisioned the `system` tenant. Static is therefore deferred until the startup bootstrap can be reworked into ordered hosted services. Verified: full backend suite green — 1254 unit + 444 integration tests pass (the integration classes that this approach's earlier Static attempt broke — FlavorRegistryTests, FirstSignalConsistencyFlowTests, AuthHardeningWave2Tests — included). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/operate/backend-architecture.md | 14 +++++++++----- src/dotnet/Modgud.Api/Program.cs | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/operate/backend-architecture.md b/docs/operate/backend-architecture.md index 43c8c727..e7390cf8 100644 --- a/docs/operate/backend-architecture.md +++ b/docs/operate/backend-architecture.md @@ -107,9 +107,12 @@ required. The Marten outbox is still active for event side-effects: SignalR notifications fire after `SaveChangesAsync` via `ProjectionSideEffects`. -Codegen runs with `TypeLoadMode.Auto` — Wolverine/Marten generated -classes are pre-generated at build time to save cold-start time and -avoid Roslyn compilation at runtime. +Codegen mode is environment-aware. Production runs `TypeLoadMode.Dynamic` +— Wolverine/Marten handler classes are generated in memory on first use +and never written to disk, so the container never tries to write into its +read-only application directory. Local dev and tests run +`TypeLoadMode.Auto`, which generates into `Internal/Generated/` on first +boot and reuses it on the next. ## Marten usage @@ -241,5 +244,6 @@ Integration tests (`Modgud.Api.Tests`) use: - **Shared PostgreSQL container** — one container instance for all test collections, parallelised - **WireMock** — fake OIDC server for external login tests -- **Pre-generated Wolverine/Marten code** (`TypeLoadMode.Auto`) — - eliminates Roslyn compilation at runtime +- **Wolverine/Marten codegen** (`TypeLoadMode.Auto`) — generated on the + first boot into the test working tree and reused, so repeat runs skip + Roslyn compilation diff --git a/src/dotnet/Modgud.Api/Program.cs b/src/dotnet/Modgud.Api/Program.cs index 5cd24c0e..614e95bd 100644 --- a/src/dotnet/Modgud.Api/Program.cs +++ b/src/dotnet/Modgud.Api/Program.cs @@ -885,7 +885,21 @@ opts.Discovery.IncludeAssembly(typeof(Modgud.Authentication.Api.Admin.RecoveryCli).Assembly); opts.Discovery.IncludeAssembly(typeof(Modgud.Authorization.Commands.CreateGroupCommand).Assembly); opts.Durability.Mode = wolverineMode; - opts.CodeGeneration.TypeLoadMode = JasperFx.CodeGeneration.TypeLoadMode.Auto; + // Production generates handler code in-memory (Dynamic): each handler is + // compiled via Roslyn on first use and never written to disk. The previous + // Auto mode tried to persist the generated source under /app/Internal — + // which the non-root container user cannot write — logging "Access to the + // path '/app/Internal' is denied" on the first hit of every handler. + // Dev/Test keep Auto, which writes into Internal/Generated/ on a writable + // working tree and reuses it across restarts. + // + // (Pre-generating at build time + TypeLoadMode.Static would also avoid the + // runtime Roslyn pass, but it requires JasperFx's `RunJasperFxCommands` + // entry point, which is incompatible with this app's WebApplicationFactory + // integration tests — see git history for fix/wolverine-codegen-static-prod.) + opts.CodeGeneration.TypeLoadMode = builder.Environment.IsProduction() + ? JasperFx.CodeGeneration.TypeLoadMode.Dynamic + : JasperFx.CodeGeneration.TypeLoadMode.Auto; // Wolverine 6 made `ServiceLocationPolicy.NotAllowed` the default. We // keep that strict default — accidental new service-location dependencies