Skip to content

Polecat#46: Add Polecat.AotSmoke consumer project#106

Merged
jeremydmiller merged 2 commits into
mainfrom
feature/aot-smoke-46
May 18, 2026
Merged

Polecat#46: Add Polecat.AotSmoke consumer project#106
jeremydmiller merged 2 commits into
mainfrom
feature/aot-smoke-46

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Adds a CI-gated AOT smoke-test project (src/Polecat.AotSmoke) that references Polecat as a static-mode AOT consumer and fails the build on any IL2026/IL2046/IL2055/IL2065/IL2067/IL2070/IL2072/IL2075/IL2090/IL2091/IL2111/IL3050/IL3051 warning emitted from its compilation. Ticks off the AOT-smoke checkbox on #46.

Mirrors src/JasperFx.AotSmoke (jasperfx commit d4077d8) adapted for Polecat's surface.

What's exercised

Top-level statements in Program.cs touch:

  • IServiceCollection.AddPolecat(Action<StoreOptions>) (the main consumer DI surface)
  • PolecatProjectionOptions.Snapshot<T>(SnapshotLifecycle.Inline) (projection registration)
  • Scoped IDocumentSession resolution through ISessionFactory
  • IQuerySession.Query<Quest>().Where(...) (LINQ provider entry + extension surface)

Crucially, the project does NOT reference JasperFx.RuntimeCompiler and does NOT call services.AddRuntimeCompilation() — it's the "Static TypeLoadMode" consumer Polecat#46 promises.

The smoke runs to completion (Polecat AOT smoke OK. then return 0) — it never opens a SQL connection, so no DB container is needed in CI.

Gate verification

Temporarily added a deliberately-AOT-hostile call (JsonSerializer.Serialize((object)session)) and confirmed it broke the build on both net9.0 and net10.0:

error IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' …
error IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' …
Build FAILED.

Reverted before commit. Final dotnet build -c Release is clean (0 warnings, 0 errors) on net9.0 and net10.0; dotnet run exits 0 on both.

CI wiring

New workflow .github/workflows/aot-smoke.yml runs on push/PR to main, builds only src/Polecat.AotSmoke/Polecat.AotSmoke.csproj. Cannot piggy-back on polecat.yml because that workflow targets only Polecat.Tests.csproj — adding the project to polecat.slnx alone wouldn't gate CI.

Pre-existing Polecat IL warnings (not addressed in this PR)

dotnet build of Polecat.csproj emits 14 pre-existing IL warnings (PolecatProjectionBatch MakeGenericMethod, DocumentMapping ValueTypeInfo.ForType, StreamCompacting ISerializer.ToJson). These don't fail the smoke gate (which only promotes warnings emitted under the AotSmoke compilation context to errors), and they're real reflective surfaces not currently reached from the smoke surface. Capturing as a follow-up under #46 rather than expanding scope of this PR.

Test plan

  • dotnet build src/Polecat.AotSmoke/Polecat.AotSmoke.csproj -c Release succeeds with 0 warnings, 0 errors locally on net9.0 + net10.0
  • dotnet run --project src/Polecat.AotSmoke/Polecat.AotSmoke.csproj exits 0 on both TFMs
  • Deliberate-break verification: bad reflective call surfaced IL2026 + IL3050 as build errors, then reverted
  • CI aot-smoke workflow passes on this PR

References #46.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits May 17, 2026 22:02
New consumer-side smoke test that boots a minimal Host with AddPolecat,
registers a single self-aggregating snapshot projection, resolves an
IDocumentSession via DI, and constructs a LINQ query — without
referencing JasperFx.RuntimeCompiler. The csproj sets IsAotCompatible=true
and promotes IL2026/IL2046/IL2055/IL2065/IL2067/IL2070/IL2072/IL2075/
IL2090/IL2091/IL2111/IL3050/IL3051 to errors so any regression in
Polecat's AOT-clean surface fails the build.

Mirrors src/JasperFx.AotSmoke in the jasperfx repo. CI wiring lands in a
follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add .github/workflows/aot-smoke.yml — separate workflow that builds
src/Polecat.AotSmoke on push/PR to main. No SQL Server needed (the smoke
test never opens a connection). Runs on net9.0 + net10.0 via the project's
inherited TargetFrameworks.

Existing polecat.yml only builds Polecat.Tests so the AOT gate wouldn't
fire there even with the project in the solution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit fd6bebc into main May 18, 2026
7 checks passed
@jeremydmiller jeremydmiller deleted the feature/aot-smoke-46 branch May 18, 2026 10:50
jeremydmiller added a commit that referenced this pull request May 18, 2026
CI surfaced the broader scope of JasperFx#276 Phase 1's fail-fast contract:
any closed `SingleStreamProjection<TDoc, TId>` (or sibling) instantiated by
the live-aggregation path — `AggregateStreamAsync<T>`, `FetchLatest<T>`,
`FetchForWriting<T>`, `Projections.Snapshot<T>` — relies on the source
generator emitting a standalone `TEvolver` for the self-aggregating
document type `T`. The SG only emits when `T` is `partial` AND the
assembly references the SG analyzer.

Marks the following document types `partial`:

- Polecat.Tests:
  - Bug4197Aggregate, DeletableAggregate, StudentCourseEnrollment,
    QuestAggregate, InlineSeAggregate, InvoiceAggregate, OrderAggregate,
    Report, ScenarioQuestParty, CompositeQuestParty, QuestStats,
    QuestParty, CustomerSummary, SelfAggregatingStringQuest,
    StringQuestParty, SnapshotParty, SnapshotPartyByString,
    MonthlyAccountActivity, Payment, Payment2
- Polecat.AspNetCore.Testing:
  - StreamingQuestParty (+ wires
    JasperFx.Events.SourceGenerator as an Analyzer PackageReference)

Adds two new sections to docs/migration-guide.md:

- "Projections and self-aggregating documents must be `partial`" under
  Event-sourcing API changes — covers the fail-fast diagnostic, the
  partial requirement, the analyzer-PackageReference requirement for
  consumer assemblies, the EvolveAsync / DetermineActionAsync override
  escape hatch, and the surfaces unaffected (FlatTableProjection,
  EF Core projections).
- "Inline-lambda projection registration APIs removed" — covers the
  Project<T>(lambda) / CreateEvent / DeleteEvent / ProjectEvent
  removals from jasperfx#276 / #286 and the conventional-method
  replacement shape.

Refreshes the foundation pin table (JasperFx alpha.11→alpha.13,
JasperFx.Events alpha.4→alpha.11, SG alpha.2→alpha.4) and extends the
AOT/codegen posture section to reference #107 (reflective surface
tightening) and #106 (Polecat.AotSmoke CI gate) plus the new FEC-free
posture from #276.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 18, 2026
…#109)

* Bump JasperFx.Events to 2.0.0-alpha.11 + SourceGenerator to 2.0.0-alpha.4

JasperFx#276 Phase 1 (alpha.11) removed the FastExpressionCompiler
fallback for projection apply-method dispatch. Source-generated
dispatchers ([GeneratedEvolver]) are now the only path, with fail-fast
at registration when no generated dispatcher is found.

SG alpha.4 pulls in a downstream fix
(JasperFx/jasperfx#291) for a sync-DetermineActionAsync emit bug that
surfaced on Polecat projections combining sync Apply + ShouldDelete
(StringQuestPartyProjection, QuestPartyProjection, DeletableAggregate).
Without alpha.4 the generated dispatcher fails to compile with CS8135.

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

* Verify Polecat projections register generated dispatchers (JFx#276 Phase 3)

JasperFx.Events 2.0.0-alpha.11 (#276 Phase 1) made source-generated
dispatchers the only path; DocumentStore fail-fasts on projection types
without [GeneratedEvolver]. Two consumer-side changes needed:

1. **partial class** on every projection registered through
   Projections.Add<T>(...) or Snapshot<T>(...) — so the SG can emit a
   partial override of Evolve / EvolveAsync / DetermineActionAsync.
   Affects: QuestLogProjection, MultiEventQuestLogProjection,
   SimpleEnrichmentProjection, EnrichmentCallOrderProjection,
   DbLookupEnrichmentProjection, MonthlyAccountActivityProjection,
   CompositeOrderProjection, OrderShippingNotificationProjection,
   StringQuestPartyProjection, CustomerSummaryProjection, and the
   nested InlineSeProjection (whose outer test class also becomes
   partial).

2. **SG analyzer wired into Polecat.AotSmoke.csproj** as an
   Analyzer-only PackageReference, and QuestProjection declared
   partial. The smoke gate now exercises the SG-only dispatch path
   end-to-end; without it, AotSmoke runtime throws
   InvalidProjectionException at host build.

Test-side migrations off the removed inline-lambda registration API
(JasperFx#276 / #286 dropped Project<TEvent>(lambda) +
CreateEvent/DeleteEvent/ProjectEvent overloads):
- event_projection_enrichment_tests.cs (3 projections) — converted
  constructor `Project<TEvent>((e, ops) => ...)` calls to conventional
  `public void Project(TEvent e, IDocumentSession ops)` methods.
- event_projection_tests.cs — removed QuestLogWithLambdaProjection +
  event_projection_with_lambda test; the API they exercised no longer
  exists and the conventional path is already covered by
  QuestLogProjection.

FlatTableProjection unaffected — it has its own Apply dispatch via
_handlers dictionary, doesn't use JasperFx.Events Apply discovery.
EF Core projections (EfCoreSingleStreamProjection etc.) override
DetermineActionAsync directly, also bypassing the SG-required path.

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

* Bump Polecat to 4.0.0-alpha.5

First Polecat alpha consuming the FEC-free JasperFx.Events alpha
(2.0.0-alpha.11 + SourceGenerator 2.0.0-alpha.4). All projections
registered through Polecat now go through source-generated dispatchers
exclusively — no FastExpressionCompiler fallback.

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

* Mark self-aggregating document types `partial` + migration guide

CI surfaced the broader scope of JasperFx#276 Phase 1's fail-fast contract:
any closed `SingleStreamProjection<TDoc, TId>` (or sibling) instantiated by
the live-aggregation path — `AggregateStreamAsync<T>`, `FetchLatest<T>`,
`FetchForWriting<T>`, `Projections.Snapshot<T>` — relies on the source
generator emitting a standalone `TEvolver` for the self-aggregating
document type `T`. The SG only emits when `T` is `partial` AND the
assembly references the SG analyzer.

Marks the following document types `partial`:

- Polecat.Tests:
  - Bug4197Aggregate, DeletableAggregate, StudentCourseEnrollment,
    QuestAggregate, InlineSeAggregate, InvoiceAggregate, OrderAggregate,
    Report, ScenarioQuestParty, CompositeQuestParty, QuestStats,
    QuestParty, CustomerSummary, SelfAggregatingStringQuest,
    StringQuestParty, SnapshotParty, SnapshotPartyByString,
    MonthlyAccountActivity, Payment, Payment2
- Polecat.AspNetCore.Testing:
  - StreamingQuestParty (+ wires
    JasperFx.Events.SourceGenerator as an Analyzer PackageReference)

Adds two new sections to docs/migration-guide.md:

- "Projections and self-aggregating documents must be `partial`" under
  Event-sourcing API changes — covers the fail-fast diagnostic, the
  partial requirement, the analyzer-PackageReference requirement for
  consumer assemblies, the EvolveAsync / DetermineActionAsync override
  escape hatch, and the surfaces unaffected (FlatTableProjection,
  EF Core projections).
- "Inline-lambda projection registration APIs removed" — covers the
  Project<T>(lambda) / CreateEvent / DeleteEvent / ProjectEvent
  removals from jasperfx#276 / #286 and the conventional-method
  replacement shape.

Refreshes the foundation pin table (JasperFx alpha.11→alpha.13,
JasperFx.Events alpha.4→alpha.11, SG alpha.2→alpha.4) and extends the
AOT/codegen posture section to reference #107 (reflective surface
tightening) and #106 (Polecat.AotSmoke CI gate) plus the new FEC-free
posture from #276.

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

* Split self-aggregating docs that mix Guid Id with string streams

Polecat 3.x's FEC-based live aggregator was happy to construct
SingleStreamProjection<TDoc, TId> with a TId that didn't match the
document's Id property — the FEC dispatcher just used whatever TId
the call site passed. Two tests relied on this latent flexibility:

- project_latest_with_string_key_includes_pending_events and
  project_latest_merges_committed_and_pending_for_string_key registered
  Report (Guid Id) against string-keyed streams.
- always_enforce_consistency_with_string_stream_id registered
  QuestAggregate (Guid Id) against string-keyed streams.

Polecat 4 routes through the JasperFx.Events source generator, which
keys the generated evolver on the document's Id property type. The
SG emits IGeneratedSyncEvolver<Report, Guid>, runtime instantiates
<Report, string>, and lookup fails.

Splits each affected document type into a separate string-id sibling:

- new StringReport with `string Id` for the project_latest string-key
  tests
- new StringQuestAggregate with `string Id` for the
  always_enforce_consistency string-id test

Same events, same Apply/Create methods — only the Id type differs.

Remaining CI failures on this PR (QuestParty / DeletableAggregate /
StringQuestParty / SelfAggregatingStringQuest with conventional
ShouldDelete methods routed through the assembly-registered evolver
lookup) are blocked on JasperFx/jasperfx#298 — a guard bug in
JasperFxAggregationProjectionBase.tryUseAssemblyRegisteredEvolver
short-circuits before checking IGeneratedSyncDetermineAction (the
interface the SG emits for ShouldDelete docs and which handles
ShouldDelete natively). 5-line upstream fix sketched in the issue.

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

* Bump JasperFx.Events 2.0.0-alpha.11 → alpha.12 + SourceGenerator alpha.4 → alpha.5

JasperFx.Events alpha.12 ships JasperFx/jasperfx#300 (closes #298) —
the fix for tryUseAssemblyRegisteredEvolver's guard that was
short-circuiting on HasShouldDeleteMethods() before reaching the
IGeneratedSyncDetermineAction branch (the very interface the SG emits
for self-aggregating docs with ShouldDelete, which handles deletions
natively).

Unblocks the ~25 Polecat test failures on this PR from the previous
CI run — all on the closed generic
`SingleStreamProjection<TDoc, Guid>` where TDoc was QuestParty,
StringQuestParty, or DeletableAggregate (each has a ShouldDelete
method).

JasperFx.Events.SourceGenerator alpha.5 is a coordinated version bump
that came with the alpha.12 release — the SG package itself is
unchanged from alpha.4 in any way that affects Polecat (the fix lives
in the JasperFx.Events runtime, not the SG).

Verified locally via Polecat.AotSmoke pointed at alpha.12 — bare
`opts.Projections.Add<SingleStreamProjection<DocWithShouldDelete, Guid>>(...)`
now boots cleanly. Local SQL-based test verification was blocked by a
Docker daemon issue; CI on amd64 is the authoritative run.

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

* Resolve live-aggregator TId via DocumentMapping (strong-typed-id fix)

CI surfaced 4 of 1105 test failures — all on strong-typed-id aggregates
(Payment with PaymentId wrapping Guid; Payment2 with Payment2Id wrapping
string). The closed runtime SingleStreamProjection<TDoc, TId> instances
were built with TId = the underlying *primitive* (Guid / string), but
the source generator keys its emitted IGeneratedSyncEvolver on TDoc's
actual Id property type — the *wrapper* (PaymentId / Payment2Id). The
TId mismatch tripped JasperFxAggregationProjectionBase.tryUseAssembly
RegisteredEvolver's interface-assignability check, so the dispatcher
wasn't found and the post-FEC fail-fast threw.

Two surfaces to fix:

1. **EventGraph.Build<TDoc>()** — the live-aggregation entry. Was
   hardcoding `idType = StreamIdentity == AsGuid ? typeof(Guid) : typeof(string)`,
   missing the wrapper. Now resolves via
   `new DocumentMapping(typeof(TDoc), _options).IdType`, the same shape
   PolecatProjectionOptions.AddSnapshotProjection<T>() already uses.
   This unblocks the no-explicit-registration paths
   (AggregateStreamAsync<Payment>, FetchLatest<Payment2>, etc).

2. **using_guid_based_strong_typed_id_for_aggregate_identity.cs** —
   two tests explicitly registered
   `Add<SingleStreamProjection<Payment, Guid>>(...)` instead of
   `Add<SingleStreamProjection<Payment, PaymentId>>(...)`. Worked under
   Polecat 3.x's FEC fallback (which didn't care about TId);
   under SG-only dispatch the registration must match the SG's emitted
   evolver shape. Both inline + async variants updated.

(The Payment2 string-based test file already registers with the
wrapper type Payment2Id; only the live-aggregation path needed
fixing there.)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 19, 2026
Brings docs/migration-guide.md to release-readiness. Four additions on
top of the existing scaffolding from #109 + #111:

1. **Pin table refreshed + extended**
   - JasperFx.Events alpha.12 → alpha.15 (the post-#306 SG fixes already
     baked into Polecat 4.0.0-alpha.6 from #111)
   - JasperFx.Events.SourceGenerator alpha.5 → alpha.8 (ditto)
   - Add JasperFx.RuntimeCompiler 5.0.0-alpha.4 row, with a callout
     warning consumers off the parallel stale 2.0.x line on NuGet
   - Add JasperFx.SourceGeneration 2.0.0-alpha.5 row
   - "pin all FIVE packages" → "pin all SEVEN" + lockstep callout

2. **SG-only dispatch section extended** (under the existing
   "Projections and self-aggregating documents must be `partial`"):
   - Convention-method visibility — `Apply` / `Create` / `ShouldDelete`
     must be `public` (the FEC fallback reflected over private /
     internal / protected too; the SG only sees public)
   - `[Identity]` attribute for non-conventional id-property names
   - Required-member aggregate `Create` factory recipe (preferred over
     the `new T { Required = default! }` fallback the SG emits when no
     factory is supplied)
   - Explicit cross-link to Marten's migration guide section that
     covers the same shared consumer contract

3. **Inline-lambda removal section cross-linked to Marten**.
   The migration recipe is identical between products; cross-link
   instead of forking the longer worked example.

4. **AOT publishing promoted to a top-level `### Publishing AOT`
   section** with three cross-links:
   - JasperFx's live AOT guide (https://jasperfx.github.io/codegen/aot)
   - Marten's AOT publishing walkthrough
     (https://martendb.io/configuration/aot-publishing)
   - `Polecat.AotSmoke` (#106) as the canonical in-tree consumer
     example, plus the `IsAotCompatible=true` flag on Polecat.csproj
     itself
   The existing AOT call-outs about #107 / FEC removal / #106's
   WarningsAsErrors set stay; "Publishing AOT" is the new section header
   that frames them as the consumer-side recipe.

`npm run docs-build` (VitePress) renders clean: build complete in
4.27s, exit code 0.

References [#46](#46) (master
plan — "Migration guide Polecat 3 → Polecat 4" item).

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