Skip to content

CodeGeneration/RuntimeCompiler split: enable Roslyn-free production deployments#194

Merged
jeremydmiller merged 1 commit intomainfrom
feature/codegen-runtime-compiler-split
Apr 28, 2026
Merged

CodeGeneration/RuntimeCompiler split: enable Roslyn-free production deployments#194
jeremydmiller merged 1 commit intomainfrom
feature/codegen-runtime-compiler-split

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Splits the orchestration of code generation from the Roslyn implementation. Moves Initialize / InitializeSynchronously / WriteCodeFile extensions from the JasperFx.RuntimeCompiler namespace into JasperFx.CodeGeneration (i.e., into the JasperFx package, no Roslyn dependency), and removes the silent "?? new AssemblyGenerator()" fallback that previously hard-bound every consumer to Roslyn.

The legacy methods stay in place, marked [Obsolete] with a clear migration message — existing consumers (Wolverine, Marten, community projects) keep working until they migrate.

Why

Discovered while planning Wolverine cold-start optimizations (JasperFx/wolverine#1577). The ?? new AssemblyGenerator() fallback in JasperFx.RuntimeCompiler.CodeFileExtensions is what hard-binds every consumer to Roslyn. Even though IAssemblyGenerator itself lives in JasperFx.CodeGeneration (the trimming-friendly package), the extension methods that orchestrate compilation lived in JasperFx.RuntimeCompiler and silently constructed AssemblyGenerator if none was registered in DI.

After this PR, downstream packages (Wolverine, Marten) can:

  1. Update using JasperFx.RuntimeCompiler;using JasperFx.CodeGeneration; at the four (Wolverine) / three (Marten) call sites
  2. Drop their direct <PackageReference Include=\"JasperFx.RuntimeCompiler\" />
  3. Move that registration into an opt-in extension package (e.g., Wolverine.RuntimeCompilation / Marten.RuntimeCompilation)
  4. Production deployments that pre-generate code with TypeLoadMode.Static never need Roslyn — major cold-start, deployment-size, and AOT-readiness wins

Changes

File Change
src/JasperFx/CodeGeneration/CodeFileExtensions.cs (new) New extension methods. Same signatures and behavior as the originals, except IAssemblyGenerator MUST be registered in DI when codegen path is taken — otherwise throws InvalidOperationException with actionable guidance.
src/JasperFx.RuntimeCompiler/CodeFileExtensions.cs Class-level docstring documents the migration. The three methods (Initialize, InitializeSynchronously, WriteCodeFile) now carry [Obsolete] with the migration message. No behavior change — the fallback remains for backward compatibility.
src/GeneratorTarget/WriteCommand.cs Drops using JasperFx.RuntimeCompiler; to disambiguate the new (now-ambiguous) Initialize extension call. The only in-tree consumer hit by the ambiguity.
src/JasperFx/JasperFx.csproj Version: 1.27.0 → 1.28.0 (additive new API)
src/JasperFx.RuntimeCompiler/JasperFx.RuntimeCompiler.csproj Version: 4.4.0 → 4.5.0 (additive [Obsolete] markers, no behavior change)

Behavior contract

For the new JasperFx.CodeGeneration.CodeFileExtensions:

  • TypeLoadMode.Static + types pre-built into application assembly — never touches IAssemblyGenerator, no DI requirement. Identical to legacy.
  • TypeLoadMode.Static + missing pre-built types + within codegen command — falls into the runtime-compilation path; requires registered IAssemblyGenerator (it would have done this in the legacy path too, just silently).
  • TypeLoadMode.Auto + missing pre-built types — same as above.
  • TypeLoadMode.Dynamic — always requires registered IAssemblyGenerator.

The exception thrown when no IAssemblyGenerator is registered:

No IAssemblyGenerator is registered in the application's service provider, but runtime code generation was requested. Either: (a) install the JasperFx.RuntimeCompiler package and call services.AddSingleton<IAssemblyGenerator, AssemblyGenerator>() to enable runtime Roslyn compilation, or (b) pre-generate all code (typically with 'dotnet run -- codegen write') and set GenerationRules.TypeLoadMode = TypeLoadMode.Static so runtime compilation is never invoked.

Migration guidance for downstream packages

Wolverine

Files to update (using JasperFx.RuntimeCompiler;using JasperFx.CodeGeneration;):

  • src/Wolverine/Runtime/Handlers/HandlerGraph.cs:9
  • src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs:5
  • src/Wolverine.Grpc/WolverineGrpcExtensions.cs:6
  • src/Wolverine/HostBuilderExtensions.cs:20

The DI registration at HostBuilderExtensions.cs:104 (services.AddSingleton<IAssemblyGenerator, AssemblyGenerator>()) is the only thing keeping JasperFx.RuntimeCompiler in the Wolverine.dll deployment surface — moving it into a new Wolverine.RuntimeCompilation opt-in package is a follow-up PR on the Wolverine side.

Marten

Files to update (using JasperFx.RuntimeCompiler;using JasperFx.CodeGeneration;):

  • src/Marten/Internal/ProviderGraph.cs:8
  • src/Marten/Internal/SecondaryStoreConfig.cs:10
  • src/Marten/DocumentStore.CompiledQueryCollection.cs:10
  • src/Marten/MartenServiceCollectionExtensions.cs:16

Marten also has a direct new AssemblyGenerator() instantiation at src/Marten/Events/CodeGeneration/EventDocumentStorageGenerator.cs:47 — that's a separate concern and remains in JasperFx.RuntimeCompiler's scope.

Test plan

  • CodegenTests net9.0: 305/305 pass
  • CoreTests net9.0: 389/389 pass
  • Full multi-framework solution build: 0 errors (only pre-existing CS warnings)
  • No [Obsolete] warnings inside JasperFx itself (test files use JasperFx.RuntimeCompiler.Scenarios, not the obsolete extensions)
  • CI on full matrix (net8/net9/net10)

Versions to release

  • JasperFx 1.28.0
  • JasperFx.RuntimeCompiler 4.5.0

🤖 Generated with Claude Code

The Initialize/InitializeSynchronously/WriteCodeFile extension methods on
ICodeFile lived in JasperFx.RuntimeCompiler.CodeFileExtensions and used
'?? new AssemblyGenerator()' as a silent fallback when no IAssemblyGenerator
was registered in DI. That fallback hard-bound every consumer of these
methods to the JasperFx.RuntimeCompiler package — and through it, to
Roslyn — even when the consumer pre-generated all code in Static mode and
never actually needed runtime compilation.

This commit:

1. Adds new equivalents in JasperFx.CodeGeneration.CodeFileExtensions
   (lives in the JasperFx package) that REQUIRE an IAssemblyGenerator to
   be registered in DI when the codegen path is taken. If none is
   registered and runtime compilation is needed, throws a clear
   InvalidOperationException directing the caller to either install
   JasperFx.RuntimeCompiler + register AssemblyGenerator OR pre-generate
   code with TypeLoadMode.Static. No more silent magic dependency.

2. Marks the originals in JasperFx.RuntimeCompiler.CodeFileExtensions as
   [Obsolete] with a migration message pointing to the new namespace.
   The old methods retain their existing fallback behavior so existing
   consumers keep working until they migrate.

3. Updates the in-tree GeneratorTarget/WriteCommand.cs (the only consumer
   in this repo that hit the new ambiguity) to use the new namespace.

Effect for downstream consumers (Wolverine, Marten):

- Change `using JasperFx.RuntimeCompiler;` → `using JasperFx.CodeGeneration;`
  in any file that calls `.InitializeSynchronously()` / `.Initialize()` /
  `.WriteCodeFile()`.
- Continue registering IAssemblyGenerator (Wolverine + Marten already do).
- Once all such files migrate, the package reference to
  JasperFx.RuntimeCompiler can be moved to a separate opt-in package
  (e.g. Wolverine.RuntimeCompilation) — production deployments using
  Static mode never need to ship Roslyn.

Versions:
- JasperFx: 1.27.0 → 1.28.0 (additive: new extension methods)
- JasperFx.RuntimeCompiler: 4.4.0 → 4.5.0 (additive: [Obsolete] markers,
  no behavior change for existing callers)

Tests: CodegenTests (305) and CoreTests (389) all pass on net9.0.

Step toward wolverine#1577 (cold-start optimization, AOT compatibility).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit fe722d4 into main Apr 28, 2026
1 check passed
jeremydmiller added a commit to JasperFx/marten that referenced this pull request Apr 28, 2026
Picks up the latest JasperFx + JasperFx.Events. Also bumps the
JasperFx.RuntimeCompiler pin to 4.5.0 so the transitive dep set is
consistent.

JasperFx 1.28 moved the ICodeFile codegen extension methods (notably
InitializeSynchronously) into the JasperFx.CodeGeneration namespace
(JasperFx/jasperfx#194) and marked the JasperFx.RuntimeCompiler copies
[Obsolete]. The new method requires IAssemblyGenerator to be registered
in DI (raising a descriptive InvalidOperationException otherwise) -- a
deliberate AOT-prep step. Marten currently passes a null IServiceProvider
to InitializeSynchronously at four call sites in ProviderGraph,
DocumentStore.CompiledQueryCollection, and SecondaryStoreConfig, so the
new method's runtime path would throw.

Disambiguate by fully-qualifying those four calls to the
JasperFx.RuntimeCompiler overload. The Obsolete warning code (CS0618) is
already in NoWarn at the repo root, so the explicit qualification stays
clean. Proper migration to the new method (registering IAssemblyGenerator
in Marten's DI graph) is tracked under the AOT-mode work for Marten 9.0
(issue #4309).

Effectively obsoletes PR #4003: the test additions from that PR have
already been brought into master via 8ee1d23 ("Add test reproductions
from PR #4155 and #4003"), so the only thing left is this package bump
and the runtime-compiler disambiguation. The test
use_a_single_tenanted_document_in_multi_tenancy_ancillary_store passes
against the new package set.

Closes #4003.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant