Skip to content

[Pillar] AOT compliance #213

Description

@jeremydmiller

Status: in-flight (JasperFx side complete; cross-product audits in progress)
Part of [Master] Critter Stack 2026 (#217).
Sister pillars: cold-start (#212), dedupe Marten ↔ Polecat (#214).

Every shipped library publishes AOT-clean (or trim-clean with explicit, accurate annotations). Apps that don't need runtime codegen should be able to publish with PublishAot=true / IsTrimmable=true and have the trimmer drop the entire Roslyn graph.

Definition of done

  • Every shipping library either:
    1. Compiles AOT-clean with no IL2026 / IL3050 warnings, or
    2. Has every dynamic / unreferenced surface annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode] so a downstream IsAotCompatible=true consumer sees a precise punch-list rather than a wall of analyzer noise.
  • A user app in Static TypeLoadMode with pre-generated code can dotnet publish /p:PublishAot=true against Marten 9 + Wolverine 6 + JasperFx 2.0 and produce a working binary.
  • Wolverine's runtime codegen is annotated, not removed (per Critter Stack 2026 post — source-gen is architecturally rejected). AOT users either pre-generate Wolverine code or accept the dynamic-code warning.

Codegen story for AOT users

Marten 9 and Wolverine 6 both lean on JasperFx's existing dotnet run -- codegen write for their pre-generated code. There is no Marten-specific or Wolverine-specific equivalent — JasperFx provides the canonical pre-gen pipeline via dependency. AOT-publishing apps:

  1. Run dotnet run -- codegen write at dev time. This pulls JasperFx.RuntimeCompiler (and Roslyn) in transitively only at codegen time, producing the source files on disk.
  2. Publish with TypeLoadMode = Static and without calling services.AddRuntimeCompilation(). The trimmer drops the entire Roslyn graph; the published binary contains only the pre-generated sources baked in.

This is the "conditional runtime-compiler dependency" model — present at dev time, absent in production. Per-product master plans for Marten 9 and Wolverine 6 just need to verify their pre-generated output is byte-stable and round-trips correctly under Static mode.

Cross-product workstreams

Workstream Drives Status
RuntimeCompiler / Roslyn becomes opt-in via DI All ✅ landed in JasperFx 2.0 (PR #190)
AOT attributes on every reflective public API All ✅ JasperFx + JasperFx.Events complete; per-product audits in flight
Source-generator adoption Marten, Polecat, JasperFx Per-product
AOT-clean smoke-test app per stack Cross-stack ✅ JasperFx side shipped (JasperFx.AotSmoke, PR d4077d8); per-stack consumer apps still pending
Documented "publish AOT with the Critter Stack" guide Cross-stack ✅ JasperFx side shipped (docs/codegen/aot.md); cross-stack guide pending

JasperFx workstreams

✅ Already shipped (2.0 branch)

  • PR AOT prep: introduce ITypeLoader abstraction so the Roslyn pipeline is excludable under PublishAot #190ITypeLoader interface; DynamicTypeLoader carries [RequiresDynamicCode]/[RequiresUnreferencedCode]. StaticTypeLoader is AOT-safe by construction.
  • PR AOT prep: introduce ITypeLoader abstraction so the Roslyn pipeline is excludable under PublishAot #190JasperFx.RuntimeCompiler.AssemblyGenerator annotated. services.AddRuntimeCompilation() is the single recognizable seam; AOT consumers omit it and the trimmer drops Roslyn.
  • PR AOT prep: annotate CloseAndBuildAs<T> + add a delegate-factory escape hatch for hot-path callers #191 — four CloseAndBuildAs<T> overloads carry both AOT attributes; GenericFactoryCache is the AOT-friendly replacement for hot paths.
  • <IsAotCompatible>true</IsAotCompatible> set on src/JasperFx/JasperFx.csproj and src/JasperFx.Events/JasperFx.Events.csproj (both carry inline xmldoc pointing back to this issue).
  • Audited remaining MakeGenericType / Activator.CreateInstance / Type.GetMethod(...).Invoke call sites in JasperFx + JasperFx.Events — annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode] / [UnconditionalSuppressMessage] + justification, or migrated to GenericFactoryCache. Live surfaces: ServiceContainer.cs:217/303/350, JasperFx.Events.Aggregation/* (FEC-driven; class-level suppressions pointing at the SourceGenerator escape hatch per the AOT publishing guide).
  • Audited reflective discovery in JasperFx.CommandLineCommandFactory.cs lines 68/76/124/142/148/162/208 carry [RequiresUnreferencedCode] / [RequiresDynamicCode]; line 304 carries [DynamicallyAccessedMembers(Interfaces)]. AOT-publishing apps should consume commands through the source-generated manifest emitted by JasperFx.SourceGenerator.
  • Added an AOT-clean smoke-test consumer project — src/JasperFx.AotSmoke/ (commit d4077d8). References JasperFx + JasperFx.Events, sets <IsAotCompatible>true</IsAotCompatible> + <TrimMode>full</TrimMode>, escalates IL2026/IL2046/IL2055/IL2065/IL2067/IL2070/IL2072/IL2075/IL2090/IL2091/IL2111/IL3050/IL3051 to errors. Wired into the standard build via jasperfx.sln; a regression in JasperFx core's AOT annotations fails the build in our CI.
  • Documented "publish AOT with JasperFx" story at docs/codegen/aot.md (Static mode, no AddRuntimeCompilation, pre-gen via codegen write).

What's left

JasperFx-side work is complete. Remaining items are cross-product and tracked under each product's master plan:

Cross-product dependencies

  • Marten 9 / Polecat 4 / Wolverine 6 AOT readiness depends on JasperFx 2.0 + JasperFx.Events 2.0 being annotation-complete.
  • The cross-stack AOT smoke-test app depends on at least one consumer (Marten or Polecat) reaching annotation-complete state.

Open design questions

None at filing time. Per-product audits may surface new questions as they go.

Explicitly out of scope for this pillar

  • Wolverine source-generator conversion.
  • Native-AOT Roslyn (the AssemblyGenerator + Microsoft.CodeAnalysis pipeline doesn't AOT-publish; that's by design — it's the dev-time path).

Linked implementation work

Filled in as per-product master plans land.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions