Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Marten.AotSmoke/Marten.AotSmoke.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@

<ItemGroup>
<ProjectReference Include="..\Marten\Marten.csproj" />
<!-- Marten.csproj sets PrivateAssets="all" on the SG analyzer so it
doesn't flow transitively. AotSmoke declares its own SG-emitted
projection shapes (marten#4486 regression guard), so it needs
the analyzer wired in locally. Same pattern as the test projects
(CoreTests, DocumentDbTests, MultiTenancyTests, ValueTypeTests). -->
<PackageReference Include="JasperFx.Events.SourceGenerator" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
107 changes: 101 additions & 6 deletions src/Marten.AotSmoke/Program.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<T>() 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
Expand All @@ -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;
Expand Down Expand Up @@ -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<T>(), 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<SmokeProjection>(ProjectionLifecycle.Inline);

// Pattern A — projection subclass with conventional Apply method
// (SG dispatches via partial-class merge).
opts.Projections.Add<ApplyProjection>(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<SelfAggregatingApply>(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<SelfAggregatingEvolve>(SnapshotLifecycle.Inline);
});

using var host = builder.Build();
Expand Down Expand Up @@ -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<ApplyDoc, Guid>
{
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;
}
}
Loading