diff --git a/src/Marten.AotSmoke/Marten.AotSmoke.csproj b/src/Marten.AotSmoke/Marten.AotSmoke.csproj index fcd3133345..0932706288 100644 --- a/src/Marten.AotSmoke/Marten.AotSmoke.csproj +++ b/src/Marten.AotSmoke/Marten.AotSmoke.csproj @@ -40,6 +40,12 @@ + + diff --git a/src/Marten.AotSmoke/Program.cs b/src/Marten.AotSmoke/Program.cs index 9fc613dee3..db7071ff76 100644 --- a/src/Marten.AotSmoke/Program.cs +++ b/src/Marten.AotSmoke/Program.cs @@ -1,4 +1,6 @@ -// AOT smoke test (marten#4349 / JasperFx/jasperfx#213). +// AOT smoke test (marten#4349 / JasperFx/jasperfx#213) plus +// dispatcher-coverage regression guard (marten#4486 / marten#4471 +// projection audit). // // This program touches a representative cross-section of the AOT-clean // Marten surface for a Static-TypeLoadMode consumer. The csproj sets @@ -12,11 +14,29 @@ // - services.AddMarten(opts => { ... }) — the DI-facing entry point. // - One document type with an Id property — exercises the closed-shape // IDocumentStorage / IIdentification path (post-#4404, no Roslyn emit). -// - One event type + one SingleStreamProjection<,> registration — the -// primary projection-apply surface that Marten 9 routes through -// JasperFx.Events.SourceGenerator's [GeneratedEvolver] discovery -// (Options.Projections.DiscoverGeneratedEvolvers is called at startup -// in DocumentStore.cs). +// - Four projection shapes covering both JasperFx.Events.SourceGenerator +// emission patterns × Apply-vs-Evolve method conventions: +// +// Pattern A — projection subclass; SG emits a partial declaration +// merged into the user's class. +// SmokeProjection — explicit Evolve override (deliberate SG bypass) +// ApplyProjection — conventional Apply method (SG dispatches) +// +// Pattern B — self-aggregating aggregate; SG emits a sibling +// XEvolver class plus [assembly: GeneratedEvolver(typeof(X), +// typeof(XEvolver))] registration. Aggregate type does NOT need +// to be partial. +// SelfAggregatingApply — conventional Apply method on the aggregate +// SelfAggregatingEvolve — explicit Evolve method on the aggregate +// +// ProjectionOptions.SingleStreamProjection() calls +// source.AssembleAndAssertValidity() at registration time. Post-#276 +// that throws InvalidProjectionException whenever the SG didn't emit +// a dispatcher for the shape — so a regression in the SG's discovery +// rules trips at `dotnet run` here and the program exits non-zero. +// (The build itself doesn't catch a missing emission — the SG silently +// skips when its preconditions don't hold; the runtime check is the +// sentinel.) // - StoreOptions.GeneratedCodeMode = TypeLoadMode.Static — pins the // consumer profile that AOT-publishing apps would set. // - Host.CreateApplicationBuilder + Build + DI resolution of @@ -31,6 +51,9 @@ // - JasperFx.RuntimeCompiler / services.AddRuntimeCompilation() — Marten 9 // retired that path (#4454 / #4461). This smoke deliberately does not // pull it back in. +// - Exhaustive enumeration of every projection shape in the test +// libraries — the static inventory at audit/projections-inventory.md +// covers that. This program is the regression sentinel. using JasperFx.CodeGeneration; using JasperFx.Events; @@ -75,7 +98,30 @@ // discovery (DocumentStore.cs calls DiscoverGeneratedEvolvers at // startup post-#4454). Inline lifecycle keeps the smoke test free of // the async-daemon surface, which has its own AOT story. + // + // Each Add<>/Snapshot<> call below routes through + // ProjectionOptions.SingleStreamProjection(), which calls + // source.AssembleAndAssertValidity() and throws post-#276 if the SG + // didn't emit a dispatcher for the shape. Build failure here is the + // regression-guard signal (marten#4486). + + // Pattern A — projection subclass with deliberate Evolve override + // (the user takes responsibility for dispatch; no SG emission expected). opts.Projections.Add(ProjectionLifecycle.Inline); + + // Pattern A — projection subclass with conventional Apply method + // (SG dispatches via partial-class merge). + opts.Projections.Add(ProjectionLifecycle.Inline); + + // Pattern B — self-aggregating aggregate with conventional Apply + // method on the aggregate type itself. SG dispatches via a sibling + // SelfAggregatingApplyEvolver class plus assembly attribute. + opts.Projections.Snapshot(SnapshotLifecycle.Inline); + + // Pattern B — self-aggregating aggregate with explicit Evolve method + // on the aggregate type itself (still SG-dispatched because the + // emission targets the aggregate's Evolve signature directly). + opts.Projections.Snapshot(SnapshotLifecycle.Inline); }); using var host = builder.Build(); @@ -109,3 +155,52 @@ public override SmokeDoc Evolve(SmokeDoc? snapshot, Guid id, IEvent e) return snapshot; } } + +// Pattern A — projection subclass with conventional Apply method. The SG +// emits a partial declaration containing an Evolve override that dispatches +// `e.Data` against Apply(SmokeEvent, ApplyDoc). Source MUST be partial; if +// it isn't, the SG silently skips emission (no CS0260 — the SG checks for +// partial first) and the runtime fail-fast in +// JasperFxAggregationProjectionBase.AssembleAndAssertValidity() throws +// InvalidProjectionException at Projections.Add<>() time. The program's +// non-zero exit is therefore the regression-guard signal — the build alone +// does not catch this. +public sealed partial class ApplyDoc +{ + public Guid Id { get; set; } + public int Count { get; set; } +} + +public sealed partial class ApplyProjection : SingleStreamProjection +{ + public void Apply(SmokeEvent ev, ApplyDoc snapshot) => snapshot.Count += ev.Delta; +} + +// Pattern B — self-aggregating aggregate with conventional Apply method +// directly on the aggregate type. The SG emits a sibling +// SelfAggregatingApplyEvolver class plus an +// [assembly: GeneratedEvolver(typeof(SelfAggregatingApply), +// typeof(SelfAggregatingApplyEvolver))] registration. The aggregate type +// does NOT need to be partial — the dispatcher is independent of the +// source declaration. +public sealed class SelfAggregatingApply +{ + public Guid Id { get; set; } + public int Count { get; set; } + public void Apply(SmokeEvent ev) => Count += ev.Delta; +} + +// Pattern B — self-aggregating aggregate with explicit Evolve method +// directly on the aggregate. SG emission still applies; the Evolve method +// is the dispatch entry point the emitted XEvolver class calls. +public sealed class SelfAggregatingEvolve +{ + public Guid Id { get; set; } + public int Count { get; set; } + + public SelfAggregatingEvolve Evolve(IEvent e) + { + if (e.Data is SmokeEvent inc) Count += inc.Delta; + return this; + } +}