Skip to content

Marten#4349: Add Marten.AotSmoke + extension package IsAotCompatible audit#4468

Merged
jeremydmiller merged 4 commits into
masterfrom
feat/4349-aot-smoke-and-extension-audit
May 18, 2026
Merged

Marten#4349: Add Marten.AotSmoke + extension package IsAotCompatible audit#4468
jeremydmiller merged 4 commits into
masterfrom
feat/4349-aot-smoke-and-extension-audit

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Ticks two checkboxes on #4349 (Marten 9.0 master plan):

  • AOT smoke test — Marten in a Static-mode app builds clean under IsAotCompatible=true + TrimMode=full with the standard IL warnings-as-errors list. Regressions in Marten's class-level annotations now fail Marten's own CI rather than waiting for downstream consumers to catch them.
  • Per-extension-package IsAotCompatible audit — same exercise Weasel ran in #283 across its provider packages.

Pattern mirrors src/JasperFx.AotSmoke/ (commit d4077d8) and src/Weasel.Core.AotSmoke/ (PR #283 / commit 5844c9f).

Per-package audit table

Package IsAotCompatible Notes
Marten true (pre-existing) Already AOT-flagged with class-level [UnconditionalSuppressMessage] suppressions. Out of scope to tighten further.
Marten.AspNetCore true (new) Clean — streaming endpoints route through Marten's already-annotated IDocumentStore / LINQ / JSON streaming. No annotations needed.
Marten.EntityFrameworkCore true (new, annotated) EF Core's reflective query path (IModel.FindEntityType, DbContext.Find<TEntity>, Activator.CreateInstance<TDbContext>) needs [DynamicallyAccessedMembers] on the wrapper's TDoc / TDbContext generic parameters. Annotations propagate the trim requirement to consumers — see the EF Core commit for the full surface.
Marten.Newtonsoft false (documented) Newtonsoft.Json's IContractResolver is fundamentally AOT-hostile — it reflects over the full member surface of every serialized type without DAM annotations anywhere. Annotating the Marten wrapper papers over a small slice of an unfixable upstream. Csproj carries a comment block explaining the decision. AOT consumers should use UseSystemTextJsonForSerialization instead.
Marten.NodaTime true (new) Clean — value-object types + Npgsql NodaTime type handlers, all AOT-friendly upstream.

Marten.AotSmoke

Build-time consumer project (not a test project — no xUnit, no dotnet test). Exits cleanly when the smoke surface analyzes clean under IsAotCompatible=true + TrimMode=full + the standard WarningsAsErrors list (IL2026;IL2046;IL2055;IL2065;IL2067;IL2070;IL2072;IL2075;IL2090;IL2091;IL2111;IL3050;IL3051).

Surface exercised (matches the chip's minimum):

  • services.AddMarten(opts => { ... }) — DI-facing entry point.
  • opts.GeneratedCodeMode = TypeLoadMode.Static — pins the AOT-publishing consumer profile.
  • opts.Schema.For<SmokeDoc>() — closed-shape IDocumentStorage registration.
  • opts.Events.AddEventType<SmokeEvent>() — explicit event-alias registration.
  • opts.Projections.Add<SmokeProjection>(Inline)SingleStreamProjection<,> registration (the JasperFx.Events.SourceGenerator [GeneratedEvolver] discovery path).
  • Host.CreateApplicationBuilder + Build + IDocumentStore DI resolve.

Deliberately NOT exercised: connecting / querying / persisting (build-time analyzer test, not runtime), compiled queries (covered by Marten.SourceGenerator.Tests), JasperFx.RuntimeCompiler / services.AddRuntimeCompilation() (retired in Marten 9.0 — #4454 / #4461).

Regression guard verification

Per chip acceptance: temporarily added object obj = "x"; obj.GetType().GetMethod("ToString"); to Program.cs — surfaced error IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' on both net9.0 and net10.0. Reverted before commit.

Known follow-up (outside this chip's scope)

dotnet publish /p:PublishAot=true against Marten.AotSmoke surfaces ~15 unannotated sites in Marten.Internal.ClosedShape.* (every Identification / Binder / BulkLoader reaches LambdaBuilder.Getter / LambdaBuilder.Setter from FEC). Marten's class-level [UnconditionalSuppressMessage] justifications hide these at build time, but the AOT publish walks the full reachable graph regardless. The chip explicitly carves these out:

Don't tighten existing class-level suppressions in Marten core — separate, larger task.

That work tracks on the larger Marten 9.0 master-plan item for closed-shape AOT cleanliness. This PR's gate is dotnet build (per the chip's explicit acceptance criterion) — which is clean.

Test plan

  • dotnet build src/Marten.slnx — 0 errors, full slnx builds clean (1126 warnings, all pre-existing in Marten core)
  • dotnet build src/Marten.AotSmoke/Marten.AotSmoke.csproj — 0 warnings, 0 errors on both net9.0 and net10.0
  • dotnet run --project src/Marten.AotSmoke — exits 0 with the expected Marten AOT smoke OK message
  • Marten.EntityFrameworkCore.Tests — 39/39 pass post-DAM-annotation pass (net10.0)
  • Marten.AspNetCore.Testing — 45/46 pass (one pre-existing flake in using_strong_typed_identifiers.stream_json_hit on master — confirmed unrelated to this PR by stashing and re-running on master HEAD; tracked as a sibling closed-shape ValueTypeIdentification issue)
  • Regression guard verified (see above)
  • CI matrix

References: #4349 (master plan — not "Closes", this just ticks two boxes).

🤖 Generated with Claude Code

jeremydmiller and others added 4 commits May 18, 2026 09:28
New build-time consumer project that mirrors src/JasperFx.AotSmoke/ and
src/Weasel.Core.AotSmoke/. Builds with IsAotCompatible=true,
TrimMode=full, and the IL2026/IL2046/IL2055/IL2065/IL2067/IL2070/IL2072/
IL2075/IL2090/IL2091/IL2111/IL3050/IL3051 warnings-as-errors set —
any change that adds [RequiresDynamicCode] / [RequiresUnreferencedCode]
to a Marten API exercised here fails CI, and any change to Program.cs
that calls into a reflective Marten surface fails too.

Surface exercised:
- services.AddMarten(opts => { ... }) — DI-facing entry point.
- StoreOptions.GeneratedCodeMode = TypeLoadMode.Static — pins the
  consumer profile AOT-publishing apps would set.
- Schema.For<SmokeDoc>() — closed-shape IDocumentStorage path.
- Events.AddEventType<SmokeEvent>() — explicit alias registration.
- Projections.Add<SmokeProjection>(Inline) — SingleStreamProjection<,>
  registration (the JasperFx.Events.SourceGenerator [GeneratedEvolver]
  discovery surface).
- Host.CreateApplicationBuilder + Build + IDocumentStore DI resolve.

Regression guard verification (chip acceptance criterion): manually
inserted `object obj = "x"; obj.GetType().GetMethod("ToString")` and
confirmed it fails with `error IL2075` on both net9.0 and net10.0, then
reverted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Time

Both packages compile clean with IsAotCompatible=true — no IL2026 /
IL2070 / IL2075 / IL3050 warnings surface. AspNetCore's streaming
endpoints route through Marten's already-annotated IDocumentStore /
LINQ / JSON streaming paths; NodaTime is value-object types + npgsql
type handlers, all AOT-friendly upstream.

Matches the chip's expected per-package state for the two extensions
with no AOT-hostile transitive surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…otated wrapper

EF Core's reflective query path (IModel.FindEntityType / DbContext.Find /
FindAsync / Activator.CreateInstance for DbContextOptions) needs
[DynamicallyAccessedMembers] on the TDoc / TDbContext generic parameters
the wrapper closes over. Without the annotation, the trim analyzer
flags every call site with IL2087 / IL2091.

Two DAM patterns applied at the wrapper:

- TDoc (the EF entity type) — full DAM flag set
  (PublicConstructors | NonPublicConstructors | PublicFields |
   NonPublicFields | PublicProperties | NonPublicProperties |
   Interfaces) matching what IModel.FindEntityType and Find<TEntity>
  / FindAsync<TEntity> require.
- TDbContext — PublicConstructors only, matching what
  Activator.CreateInstance(typeof(TDbContext), DbContextOptions) needs
  to find the (DbContextOptions) constructor.

Annotated:
- EfCoreProjectionStorage<TDoc, TId, TDbContext>
- EfCoreSingleStreamProjection<TDoc, TId, TDbContext>
- EfCoreMultiStreamProjection<TDoc, TId, TDbContext>
- EfCoreEventProjection<TDbContext>
- EfCoreDbContextFactory.Create<TDbContext>(...)
- EfCoreProjectionExtensions.AddEntityTablesFromDbContext<TDbContext>(...)
- EfCoreProjectionExtensions.Add<TDoc, TId, TDbContext>(...) (all four
  overloads)

The trim/AOT requirement propagates to consumers: callers close the
generic parameters with concrete entity types and DbContext subclasses,
satisfying the DAM constraint implicitly.

After the annotation pass: Marten.EntityFrameworkCore builds with
IsAotCompatible=true and 0 IL warnings. Marten.EntityFrameworkCore.Tests
all 39 pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Json is AOT-hostile)

Setting IsAotCompatible=true surfaces 4 IL2070 warnings on
JsonNetObjectContractProvider.GetAttributeConstructor and
GetTheMostSpecificConstructor — both methods receive their Type
argument from Newtonsoft.Json's IContractResolver callback, and that
Type comes unannotated from Newtonsoft itself. We can't satisfy the
analyzer with wrapper-side DAM annotations because Newtonsoft.Json's
IContractResolver contract doesn't carry them at the boundary.

Newtonsoft.Json as a whole has no AOT story — its serializer reflects
over the full member surface of every type it serializes without DAM
annotations anywhere. Annotating the Marten wrapper papers over a
small part of an inherently AOT-incompatible upstream.

Per the chip's "AOT-hostile through and through" outcome: flag stays
OFF on Marten.Newtonsoft. AOT consumers use
UseSystemTextJsonForSerialization (Marten core) instead.

A `<!-- AOT (marten#4349 audit) -->` comment block in the csproj
documents the decision so future maintainers don't re-flip the flag
without re-reading the analysis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 7d2a569 into master May 18, 2026
5 of 6 checks passed
@jeremydmiller jeremydmiller deleted the feat/4349-aot-smoke-and-extension-audit branch May 18, 2026 14:34
This was referenced May 18, 2026
jeremydmiller added a commit that referenced this pull request May 18, 2026
New page at docs/configuration/aot-publishing.md walks through publishing
a Marten-backed .NET app with PublishAot=true. Models after JasperFx's
docs/codegen/aot.md (219 lines), adapted to the Marten 9 surface — main
adaptation is the one-phase model (Marten 9 retired runtime code
generation in #4461, so there's no `dotnet run -- codegen write` step
the JasperFx guide builds around).

Sections:

- What AOT publishing buys you + when it's the right choice
- Prerequisites (.NET 9/10, Marten 9.0+, STJ serializer, optional
  Marten.SourceGenerator)
- How Marten 9 differs from the broader Critter Stack two-phase model
- Project setup walkthrough (csproj, AddMarten with STJ,
  [JasperFxAssembly] marker, publish command)
- What works / annotated / doesn't work — including the ~15 cascading
  IL warnings from Marten.Internal.ClosedShape.* construction sites
  that PR #4468's audit surfaced
- The Newtonsoft.Json escape hatch — concrete migration from
  UseNewtonsoftForSerialization to UseSystemTextJsonForSerialization
- Verifying your app is AOT-clean (publish output scan + reference to
  Marten.AotSmoke)
- Troubleshooting common failure modes (missing GeneratedEvolver,
  IL3050 on custom projection base, LINQ in AOT, Reflection.Emit
  secondary-store boot failure, publish performance)
- Performance — cites the Marten.SourceGenerator README's compiled-
  query benchmark, defers end-to-end AOT cold-start benchmarks to
  CritterStackScalability
- Cross-references — JasperFx guide, Marten.SourceGenerator README,
  migration guide anchor

Sidebar entry added under "Configuration" between "Optimized
Development Workflow" and "Multi-Tenancy with Database per Tenant".

markdownlint + cspell clean.

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