Marten#4349: Add Marten.AotSmoke + extension package IsAotCompatible audit#4468
Merged
Merged
Conversation
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>
This was referenced May 18, 2026
Closed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ticks two checkboxes on #4349 (Marten 9.0 master plan):
IsAotCompatible=true+TrimMode=fullwith 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.Pattern mirrors
src/JasperFx.AotSmoke/(commitd4077d8) andsrc/Weasel.Core.AotSmoke/(PR #283 / commit5844c9f).Per-package audit table
true(pre-existing)[UnconditionalSuppressMessage]suppressions. Out of scope to tighten further.true(new)true(new, annotated)IModel.FindEntityType,DbContext.Find<TEntity>,Activator.CreateInstance<TDbContext>) needs[DynamicallyAccessedMembers]on the wrapper'sTDoc/TDbContextgeneric parameters. Annotations propagate the trim requirement to consumers — see the EF Core commit for the full surface.false(documented)IContractResolveris 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 useUseSystemTextJsonForSerializationinstead.true(new)Marten.AotSmoke
Build-time consumer project (not a test project — no xUnit, no
dotnet test). Exits cleanly when the smoke surface analyzes clean underIsAotCompatible=true+TrimMode=full+ the standardWarningsAsErrorslist (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-shapeIDocumentStorageregistration.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+IDocumentStoreDI 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 — surfacederror IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'on bothnet9.0andnet10.0. Reverted before commit.Known follow-up (outside this chip's scope)
dotnet publish /p:PublishAot=trueagainst Marten.AotSmoke surfaces ~15 unannotated sites inMarten.Internal.ClosedShape.*(everyIdentification/Binder/BulkLoaderreachesLambdaBuilder.Getter/LambdaBuilder.Setterfrom 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: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.0dotnet run --project src/Marten.AotSmoke— exits 0 with the expectedMarten AOT smoke OKmessageMarten.EntityFrameworkCore.Tests— 39/39 pass post-DAM-annotation pass (net10.0)Marten.AspNetCore.Testing— 45/46 pass (one pre-existing flake inusing_strong_typed_identifiers.stream_json_hiton master — confirmed unrelated to this PR by stashing and re-running on master HEAD; tracked as a sibling closed-shape ValueTypeIdentification issue)References: #4349 (master plan — not "Closes", this just ticks two boxes).
🤖 Generated with Claude Code