AOT Publishing
WARNING
Status: in progress. Wolverine 6.0 sets up the AOT story but the full per-file annotation pass is still landing — see the AOT-pillar tracking issue. The guidance below describes the target end state plus the safe escape hatches that work today.
Wolverine 6.0 introduces first-class support for publishing applications with Native AOT (dotnet publish /p:PublishAot=true) and aggressive trimming. The story is two-part:
- At build time — pre-generate handler dispatch code via
dotnet run -- codegen write, then ship that pre-generated code as ordinary, statically-analyzable C# inside your app'sInternal/Generated/folder. - At runtime — configure Wolverine in
TypeLoadMode.Staticso the host loads the pre-generated types instead of compiling them at boot. With Static mode in effect, Wolverine's Roslyn-basedAssemblyGeneratoris never invoked, and the AOT compiler / trimmer have everything they need statically.
Why this matters
Wolverine's default TypeLoadMode.Dynamic (and Auto) configurations compile handler dispatch via JasperFx.RuntimeCompiler on host startup. That path:
- Loads
Microsoft.CodeAnalysis.CSharp(~6 MB Roslyn binaries) into your process - Emits and JIT-compiles new types at runtime — fundamentally incompatible with Native AOT
- Reflects over handler / saga / middleware types in ways the trimmer can't follow statically
TypeLoadMode.Static shifts all of that to build time. Production binaries published with Static mode + pre-generated code do not carry Roslyn at all, start faster, and pass dotnet publish /p:PublishAot=true without IL2026 / IL3050 warnings outside the explicitly-annotated runtime-codegen surface.
Walkthrough
1. Pre-generate handler code
In your project root, run:
dotnet run -- codegen writeThis invokes the JasperFx command-line surface that Wolverine extends. The command discovers handlers / sagas / middleware in the configured assemblies, runs the same code-generation passes the host would run at startup, and writes the resulting C# into Internal/Generated/ (or wherever opts.CodeGeneration.GeneratedCodeOutputPath points). The files are normal C# — check them in, run them through the trimmer, set breakpoints in them.
TIP
codegen write is the same command that's been available in earlier Wolverine versions for dev-time inspection. The 6.0 difference is that the output is byte-stable and round-trips into TypeLoadMode.Static — i.e. you can rely on the pre-generated files being the authoritative dispatch code, not just a debugging aid.
2. Configure TypeLoadMode.Static
In your application's Program.cs:
builder.Host.UseWolverine(opts =>
{
opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static;
// … the rest of your configuration
});TypeLoadMode.Static tells Wolverine "the pre-generated handler types are in this assembly; load them by name and skip the runtime compile step entirely." If a handler is missing pre-generated code, host startup fails with a clear error pointing at the missing file — so the failure mode is "loud and at startup" rather than "silent fallback to Roslyn."
3. Publish AOT
Reference PublishAot in your csproj or pass it on the command line:
dotnet publish -c Release /p:PublishAot=trueA clean AOT-published Wolverine app in Static mode emits no IL2026 / IL3050 warnings from Wolverine itself today against the AOT-clean subset of the surface (Envelope, WolverineOptions, DeliveryOptions, the scheduling helpers — what the Wolverine.AotSmoke regression-guard project exercises).
WARNING
Until the AOT-pillar tracking issue (#2746) is fully closed out, Wolverine APIs outside the AOT-clean subset above may emit IL warnings under PublishAot=true. The warnings are informative — they tell you which Wolverine APIs require dynamic code / aren't trim-safe. The Static-mode runtime path avoids most of them; the rest get annotated as the pillar work progresses.
What Wolverine.AotSmoke already guarantees
The Wolverine.AotSmoke project in the Wolverine repo sets IsAotCompatible=true, TrimMode=full, and promotes the IL2026/IL2046/IL2055/IL2065/IL2067/IL2070/IL2072/IL2075/IL2090/IL2091/IL2111/IL3050/IL3051 warning codes to errors. Every PR runs it via the aot workflow.
Currently exercised AOT-clean surface (will fail the smoke build if any of these APIs gains a [RequiresDynamicCode] / [RequiresUnreferencedCode] annotation):
Envelopeconstruction + value-shape (Id,Headers,CorrelationId,MessageType, …)Envelope.TryGetHeaderfast pathEnvelope.SetMessageType<T>closed-genericEnvelope.SetMetricsTag- Envelope scheduling helpers (
ScheduleAt,ScheduleDelayed,IsScheduledForLater,IsExpired) DeliveryOptionsconstructionWolverineOptionsconstructionWolverineOptions.DetermineSerializer(dictionary lookup)
As the per-file annotation pass in #2746 progresses, the smoke project expands to exercise the newly-annotated entry points — tightening the regression guard incrementally.
Surfaces that intentionally aren't AOT-clean
A handful of Wolverine APIs require runtime codegen by design. They carry [RequiresDynamicCode] / [RequiresUnreferencedCode] annotations so the trimmer surfaces a clear warning if your AOT-published app reaches them:
HandlerGraph.Compileand its callees — runtime Roslyn codegen of handler dispatch. Static-mode apps don't reach this.- Reflective handler / saga / transport discovery (
HandlerDiscovery.IncludeAssemblytype-scan, the saga-type-descriptorStartingMessagesreflection, transportIMessageRoutingConventionreflection). Static-mode + a pre-generated handler manifest avoid these. MakeGenericType/Activator.CreateInstancedriven factories — the few that still exist in core.- The
WolverineFx.Newtonsoftcompanion package (Newtonsoft.Json itself isn't AOT-friendly; if you need Newtonsoft, you're not on the AOT path).
If you publish AOT in Dynamic mode (the default), expect warnings from these surfaces — they tell you what to migrate before you can ship a working AOT binary.
Cold-start side benefit
Even without publishing AOT, TypeLoadMode.Static produces meaningful cold-start improvements: the host doesn't pay the per-handler Roslyn compile cost on first message. The CritterStackScalability coldstart wolverine harness measures this against the V5.39.0 baseline.
Pre-generated static registries
codegen write also emits a GeneratedHandlerRegistry (under Internal/Generated/WolverineHandlers/) that captures the discovered handler types as a compile-time typeof(...) array. In TypeLoadMode.Static, Wolverine consumes this registry at startup and skips conventional handler discovery's assembly scan entirely — there's no Assembly.ExportedTypes walk and no convention filtering across your assemblies. Handler-method selection still runs over exactly the registry's types, so behavior is identical to a full scan.
This is on by default in TypeLoadMode.Static. Dynamic-mode apps that still want to skip the scan can opt in explicitly:
builder.Host.UseWolverine(opts =>
{
opts.UseStaticRegistries();
// … the rest of your configuration
});The fallback is safe rather than loud: if no GeneratedHandlerRegistry is present (you forgot to run codegen write, or the file wasn't committed), Wolverine logs a single warning and falls back to the runtime assembly scan instead of throwing. Re-run dotnet run -- codegen write and commit the regenerated files to eliminate the scan.
TIP
Regenerating during codegen write always performs a fresh scan, so the registry can never perpetuate a stale handler set — add the regeneration step to CI alongside the rest of your pre-generated code.
Validation and AOT
There is no pre-generated ValidatorRegistry to build for AOT — but the picture has two distinct paths, only one of which scans assemblies. See #2855 for the full investigation.
Middleware application is scan-free (AOT-clean). FluentValidationPolicy decides whether to apply validation to a handler chain by querying the IoC container per message type (container.RegistrationsFor(typeof(IValidator<>))), not by walking Assembly.ExportedTypes. The codegen-time policy and the runtime Execute* calls carry leaf trim/AOT annotations (see AOT-pillar tracking), so this path is trim-safe. Likewise DataAnnotationsValidationExecutor reflects only over the handler-rooted message type's [Validation*] attribute graph (leaf-annotated, no scan).
Validator discovery at bootstrap is the one non-AOT seam. opts.UseFluentValidation() defaults to RegistrationBehavior.DiscoverAndRegisterValidators, which runs FluentValidation's AssemblyScanner over your ApplicationAssembly to find and register IValidator<T> implementations. That scan is fundamentally trim-hostile (it walks exported types and constructs open generics reflectively). For trim/AOT publishing, opt out of the scan and register validators explicitly:
opts.UseFluentValidation(RegistrationBehavior.ExplicitRegistration);
// then register each validator yourself (trim-safe):
opts.Services.AddScoped<IValidator<CreateCustomer>, CreateCustomerValidator>();With ExplicitRegistration, Wolverine performs no assembly scan; the middleware-application path was already scan-free, so the whole FluentValidation surface is then AOT-clean. (If you instead rely on FluentValidation's own AddValidatorsFromAssembly* outside Wolverine, that scan has the same trim-hostility — register explicitly there too, and see FluentValidation's AOT guidance.)
Migration checklist
If you're moving an existing 5.x Wolverine app to 6.0 AOT:
- Upgrade to Wolverine 6.0 (see the migration guide)
- Run
dotnet run -- codegen writelocally; verify theInternal/Generated/folder gets populated and the files compile - Add
opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static;to yourUseWolverineconfiguration - Run your existing test suite — same behavior, just no runtime codegen
- Add
/p:PublishAot=trueto yourdotnet publishcommand, fix any remaining IL warnings (or accept them with<NoWarn>) and ship
If a handler type changes between releases, re-run codegen write and commit the regenerated files. Add the regeneration step to your CI pipeline (or pre-commit hook) so the pre-generated code never drifts from the source handlers.
Related
- Code Generation — full reference for
TypeLoadMode,codegen write, and the underlying JasperFx runtime-compilation surface Wolverine.AotSmoke— the regression-guard project this guide references- AOT-pillar tracking — what's annotated, what's left
- JasperFx 2.0 AOT pillar — the foundation Wolverine builds on

