Publishing Marten with Native AOT
This guide walks through publishing a Marten-backed .NET application with Native AOT (dotnet publish /p:PublishAot=true). Native AOT ahead-of-time-compiles the app to a self-contained native binary with no JIT and aggressive trimming, which means:
- Startup is near-instant — no JIT warmup, no Roslyn loading.
- Publish size is small — typically 15-40 MB self-contained, comparable to a Go binary.
- No runtime code generation — anything that needs
Reflection.Emit,Type.MakeGenericType, orActivator.CreateInstance(Type)on a type the trimmer didn't statically see will warn at publish time and may fail at runtime.
Marten 9.0 is the first release where AOT publishing is a supported posture. The headline change: Marten 9 retired its Roslyn-driven runtime code-generation pipeline entirely (PR #4461 — "Removing All Runtime Compilation From Marten"). The document-storage, event-storage, compiled-query, and secondary-store paths now ship as hand-written closed-shape code or as compile-time source-generated output. There is no dotnet run -- codegen write step for Marten anymore.
AOT publishing is the right choice for serverless / Lambda / fast-restart workloads. It is not required for every Marten app — JIT publishes still work fine, and the dev-loop trade-offs (longer publish times, stricter analyzer policy) mean AOT only pays off when cold-start latency or binary size matters.
Prerequisites
- .NET 9 or .NET 10 SDK
- Marten 9.0.0-alpha or later
- JasperFx 2.0 / JasperFx.Events 2.0 / Weasel 9.0 alphas pulled in transitively when you bump
Marten - The System.Text.Json-based serializer (
UseSystemTextJsonForSerialization) — Newtonsoft.Json is not AOT-compatible (see The Newtonsoft.Json escape hatch) Marten.SourceGeneratorif you use compiled queries — see the Marten.SourceGenerator README
How Marten 9 differs from the broader Critter Stack AOT story
The JasperFx AOT publishing guide describes a two-phase model where dev-time dotnet run -- codegen write produces pre-built Internal/Generated/*.cs files that the publish-time AOT compile then consumes. That two-phase model does not apply to Marten 9.
Marten's storage / projection / compiled-query surfaces all build their dispatchers at compile time (via Marten.SourceGenerator and JasperFx.Events.SourceGenerator) or are hand-written closed-shape code that ships in Marten.dll directly. The runtime never invokes Roslyn. As a result:
- No
dotnet run -- codegen writestep. If you have anInternal/Generated/folder committed from a pre-9.0 Marten app, delete it and remove it from.gitignore. Nothing reads or writes those files. - No
services.AddRuntimeCompilation()call. PR #4461 ripped out the runtime-codegen seam entirely. Don't add it back. StoreOptions.GeneratedCodeMode,AllowRuntimeCodeGeneration,SourceCodeWritingEnabled,GeneratedCodeOutputPathhave been deleted entirely — references to them will fail to compile against Marten 9.0. Remove any leftover assignments from your bootstrapping.StoreOptions.ApplicationAssemblysurvives becauseAutoRegisterandTryUseSourceGeneratedDiscoverystill use it as a scan hint.
If you have a Wolverine app sitting next to your Marten app in the same composition root, the Wolverine side does still use the JasperFx two-phase model — Wolverine retains runtime codegen as an opt-in seam (per the 2026 plan). The JasperFx guide is the authoritative reference for the Wolverine half of that story.
Project setup walkthrough
1. Configure your csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsAotCompatible>true</IsAotCompatible>
<PublishAot>true</PublishAot>
<!-- Recommended: fail the publish build on any new IL warning -->
<WarningsAsErrors>IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Marten" />
<!-- Optional but recommended: compile-time source generator for ICompiledQuery<TDoc, TOut> types -->
<PackageReference Include="Marten.SourceGenerator" PrivateAssets="all" />
</ItemGroup>
</Project>Key points:
IsAotCompatible=trueturns on the IL2026 / IL2070 / IL2075 / IL3050 analyzers so any new reflective surface in your code surfaces at compile time.PublishAot=truetriggers Native AOT at publish.WarningsAsErrors=IL...is the policy enforcer — without it the analyzer warnings are easy to lose in build noise. The list above mirrors what the Marten.AotSmoke consumer in CI uses.- No
JasperFx.RuntimeCompilerreference, noMicrosoft.CodeAnalysis.*references are needed. Marten 9 doesn't pull them in.
2. Configure AddMarten for the AOT-clean serializer
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("Marten")!);
// System.Text.Json is the AOT-compatible serializer. The default options
// are fine for most apps; pass an explicit JsonSerializerContext via the
// configure callback if you need source-generated STJ as well.
opts.UseSystemTextJsonForSerialization();
// Register your document types, events, and projections as usual.
opts.Schema.For<Invoice>();
opts.Events.AddEventType<InvoiceCreated>();
opts.Projections.Add<InvoiceSummaryProjection>(ProjectionLifecycle.Inline);
});
using var host = builder.Build();
await host.RunAsync();3. Add the [JasperFxAssembly] marker (if you use compiled queries)
If your assembly declares any ICompiledQuery<TDoc, TOut> types, add the assembly-level marker that opts Marten.SourceGenerator into emitting handler code for them:
// AssemblyInfo.cs (or any file in the assembly)
[assembly: JasperFx.JasperFxAssembly]With the Marten.SourceGenerator package reference and [JasperFxAssembly] both present, every compiled query in the assembly gets a generator-emitted handler that registers itself with Marten's runtime registry via a [ModuleInitializer] at module load. No reflection, no FastExpressionCompiler, no Roslyn — just direct property/field reads compiled into your assembly.
See the Marten.SourceGenerator README for the full opt-in story and the three shapes the generator skips (where the runtime falls back to a FastExpressionCompiler-built descriptor — still no Roslyn, but the warning surface is wider).
4. Publish
dotnet publish -c Release /p:PublishAot=true -r linux-x64 -o ./publishA successful publish produces a single self-contained binary (plus a few native dependency .so / .dll files) under ./publish. The Roslyn graph is absent; the binary loads pre-generated source-generator output baked into the assembly directly.
What works, what doesn't
As of Marten 9.0.0-alpha:
Works (AOT-clean)
UseSystemTextJsonForSerializationwith the defaultJsonSerializerOptionsor a user-supplied one. For best AOT results, pass a source-generatedJsonSerializerContext.- Document storage —
Schema.For<TDoc>()and the entire CRUD / LINQ surface for closed-shape document types (Guid / string / int / long / strong-typed Id strategies). - Event storage —
StartStream,Append,FetchStream,FetchStreamStateAsync, the async daemon. - Projections —
SingleStreamProjection<TDoc, TId>,MultiStreamProjection<TDoc, TId>,EventProjection,CustomProjection, andEventApplier— the JasperFx.Events source generator emits[GeneratedEvolver]dispatchers at compile time for each registration. Marten callsOptions.Projections.DiscoverGeneratedEvolvers(...)at startup (src/Marten/DocumentStore.cs:84) to pick them up. - Compiled queries registered through
Marten.SourceGeneratorin an assembly marked[JasperFxAssembly]. - Secondary stores registered via
services.AddMartenStore<TInterface>()— Marten 9 builds the implementation type viaSystem.Reflection.Emit(PR #4459). The trimmer warning at the emit site carries a[RequiresDynamicCode]annotation; published apps that use this surface can either suppress or rely on the runtime fall-through to keep working. Marten.AspNetCorestreaming endpoints (WriteArray,StreamMany,WriteSingle, etc.) — clean.Marten.NodaTimetype handlers — clean.Marten.EntityFrameworkCoreprojection storage — clean once yourTDoc/TDbContextconsumer types satisfy the wrapper's[DynamicallyAccessedMembers]annotations (closing the generic parameters with concrete entity types and DbContext subclasses satisfies this implicitly).
Annotated (warns at AOT publish without suppression)
StoreOptions.MemberFactory— uses FastExpressionCompiler internally for property accessor delegates. Reached at registration time, not per-call. AOT consumers either accept the warning at the registration call site or wrap it with[UnconditionalSuppressMessage].FSharpTypeHelper— same pattern; FEC for F# discriminated-union member access. Only reached if you actually register F# types.- LINQ queries with complex transforms — the per-call LINQ-to-SQL translation uses runtime expression handling that the trim analyzer flags. For hot-path queries, route through
Marten.SourceGenerator-emitted compiled queries to escape the warning. - The closed-shape
IIdentification/IDocumentBinderconstruction sites — everyIdentification/Binder/BulkLoaderinMarten.Internal.ClosedShape.*reachesLambdaBuilder.Getter/Setterfrom FEC at registration time. Marten's class-level[UnconditionalSuppressMessage]justifications hide these at build time, butdotnet publish /p:PublishAot=truewalks the full reachable graph and surfaces them. Tracked on the Marten 9 master plan (#4349) — the path forward is a per-document-type source-generator extension; until that lands, AOT-publishing apps will see ~15IL2026/IL3050warnings cascading from these sites.
Doesn't work in AOT
Marten.Newtonsoft— Newtonsoft.Json is fundamentally AOT-hostile. The package's csproj deliberately leavesIsAotCompatibleoff (see PR #4468). UseUseSystemTextJsonForSerializationinstead.services.AddRuntimeCompilation()— the seam doesn't exist in Marten 9. If you have it in your composition root from a pre-9 app, delete the call.- Pre-built
Internal/Generated/— irrelevant to Marten 9. Delete the folder.
The Newtonsoft.Json escape hatch
If your existing app uses Marten.Newtonsoft, the AOT migration is:
// Before
services.AddMarten(opts =>
{
opts.Connection(connectionString);
opts.UseNewtonsoftForSerialization(); // ← not AOT-compatible
});
// After
services.AddMarten(opts =>
{
opts.Connection(connectionString);
opts.UseSystemTextJsonForSerialization(); // ← AOT-compatible
});Things to watch when switching:
- JSON property casing — STJ defaults to
camelCasefor properties; Newtonsoft typically defaults to PascalCase. Override withJsonSerializerOptions { PropertyNamingPolicy = null }if your existing on-disk JSON uses PascalCase keys. - Enum handling —
EnumStorage.AsStringworks in both; STJ uses theJsonStringEnumConverterunder the hood. - Polymorphic types — STJ requires
[JsonDerivedType]annotations or a source-genJsonSerializerContextfor type hierarchies. Newtonsoft'sTypeNameHandlingdoesn't have a direct equivalent in STJ. DateTimeformatting — STJ uses round-trip ISO-8601 by default; if your stored JSON was emitted by Newtonsoft with non-default settings, double-check the read path.
You can mix the two at the document level — register one document type with one serializer and another with a different one — but the AOT publish requires the STJ path to be the active one for the document types reached by your AOT-published code paths.
Verifying your app is AOT-clean
After a publish, scan the build output for IL2026 / IL2070 / IL2075 / IL3050:
dotnet publish -c Release /p:PublishAot=true -r linux-x64 -o ./publish 2>&1 | grep -E "warning IL|error IL"Every warning carries the source location of the offending call. The fix path depends on the source:
| Source | Action |
|---|---|
Marten core surface (e.g., Marten.Internal.ClosedShape.*) | Track in #4349; suppress at the consumer call site if you must publish today, with a justification comment pointing at the master plan. |
Your own code reaching Type.GetType(string) / MakeGenericType / Activator.CreateInstance(Type) | Refactor to source-generated code, or add [DynamicallyAccessedMembers] to the relevant Type parameter. |
| A third-party reflection-based library | Same approach as above — annotate the wrapper, or migrate to a source-generated alternative. |
For a baseline confidence check, Marten ships src/Marten.AotSmoke/ — a tiny app that consumes the AOT-clean cross-section of Marten's surface with all the IL warning codes promoted to errors. Mirror its csproj setup in your own app, scoped to the Marten surface you actually use.
Troubleshooting
"No source-generated dispatcher found for aggregate X"
JasperFx.Events.SourceGenerator only picks up projection types that the consuming assembly references at compile time and that follow the discovered method-signature conventions (Apply / Create / Evolve shapes documented on each projection base type's xmldoc). If a projection is missing from DiscoveredEvolvers:
- Confirm the assembly with
services.AddMarten(opts => opts.Projections.Add<MyProjection>(...))referencesJasperFx.Events.SourceGeneratoreither directly or transitively throughMarten. Marten core already wires it; you should not need a separate reference unless you're cross-assembly. - Confirm the projection extends
SingleStreamProjection<,>,MultiStreamProjection<,>, orEventProjection(not a custom base type the generator doesn't know about). - Re-run a clean build (
dotnet build -c Release --no-incremental); source generators run at compile time and the output is inobj/, so a stale build can mask new registrations.
IL3050 warning on a custom projection base type
The source generator skips base types it doesn't recognize. If you wrote a custom projection base, either:
- Inherit from
SingleStreamProjection<,>/MultiStreamProjection<,>/EventProjectioninstead and let the source generator emit the dispatch, or - Implement
IProjection.ApplyAsyncdirectly with no reflection (the type-switch fallback) — slower but AOT-clean by construction.
LINQ query throwing in AOT mode
Marten's runtime LINQ path uses expression-tree walking that the trim analyzer flags. The supported AOT pattern is:
- Wrap the query in an
ICompiledQuery<TDoc, TOut>. - Reference
Marten.SourceGeneratorand add[assembly: JasperFx.JasperFxAssembly]. - Call the compiled-query handler via
session.QueryAsync(compiledQueryInstance).
The compiled-query path bypasses the LINQ-to-SQL translator at runtime and dispatches directly through the source-generated handler. See Marten.SourceGenerator/README.md for the full surface.
Secondary store throws MissingMethodException at boot
AddMartenStore<TInterface> uses System.Reflection.Emit to materialize the implementation type at runtime (Marten 9 ripped out the Roslyn-emit subclass path in PR #4459). Native AOT supports Reflection.Emit only on full-AOT runtimes that include the JIT; some trimming configurations strip it. If a secondary store throws MissingMethodException at boot, check:
- Your published binary still contains
System.Reflection.Emit(verify withdotnet publishoutput — it shouldn't be trimmed away). - Your
TInterfaceis exactly: IDocumentStore(no extra abstract members).
dotnet publish is slow
Native AOT publish takes meaningfully longer than a JIT-mode publish (typically 30-90 seconds vs. a few seconds). Budget for it in your build pipeline; this is not a Marten-specific concern.
Performance
Marten 9's source-generator-driven compiled-query path is meaningfully faster than the pre-9 runtime-codegen path on cold start. Per the Marten.SourceGenerator README measurements (taken before AOT publish):
- Cold call (first invocation, includes Marten pipeline setup): codegen 75ms vs source-gen 3ms — ~25× faster.
- Steady-state per call (200 calls amortized): codegen 710μs vs source-gen 490μs — ~31% faster.
Native AOT publish layers an additional speedup on top by eliminating JIT warmup entirely. End-to-end cold-start benchmarks for Marten 9 under AOT are tracked on the Critter Stack scalability project and will be cited here once published.
References
- Master plan: Marten 9.0
- AOT compliance pillar (JasperFx)
- JasperFx AOT publishing guide — the broader Critter Stack story (Wolverine + the two-phase model)
- Marten.SourceGenerator README — the compiled-query AOT story
- Migration Guide: Runtime code generation removed — the 8.x → 9.0 migration for the codegen pipeline
- .NET trim warning reference — Microsoft's catalog of IL warning codes
- .NET Native AOT documentation — official Microsoft guide

