Polecat#46: Tighten reflective surface annotations from class-level to call-site#107
Merged
Conversation
Replace class-level [UnconditionalSuppressMessage] on PolecatCompositeProjection and PolecatProjectionOptions with method-level [RequiresDynamicCode] + [RequiresUnreferencedCode] on the two Snapshot<T> entry points — the only reflective surface in each class. Both call CloseAndBuildAs<ProjectionBase> which internally uses Type.MakeGenericType to close SingleStreamProjection<,> over (T, T.Id) and resolves T's Id property via DocumentMapping reflection. AOT smoke test migrated from opts.Projections.Snapshot<Quest>(...) to opts.Projections.Add<QuestProjection>(ProjectionLifecycle.Inline) with a concrete SingleStreamProjection<Quest, Guid> subclass — the registration shape AOT consumers must use. The reflective shortcut is now correctly flagged at the call site instead of silently silenced. References #46. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace class-level [UnconditionalSuppressMessage] on PolecatLinqQueryProvider (4 attrs) and EventLinqQueryProvider (2 attrs) with per-method [RequiresDynamicCode] + [RequiresUnreferencedCode] on every method that hosts a reflective call site: - CreateQuery(Expression) — MakeGenericType + Activator.CreateInstance on PolecatLinqQueryable<>. Suppress IL2046/IL3051 at the impl with a call-site UnconditionalSuppressMessage since IQueryProvider's interface method isn't annotated in the BCL. - ExecuteAsync<TResult> — routes to handler invocations that close generic handler types via MakeGenericType. - ExecuteGroupByAsync / InvokeGroupByListHandlerAsync — GroupByListHandler<>. - ExecuteGroupJoinAsync / InvokeJoinHandlerAsync — Func<,,> + JoinListHandler<,,>. - InvokeListHandlerAsync / InvokeScalarListHandlerAsync / InvokeProjectionHandlerAsync / InvokeOneResultHandlerAsync — selector + handler types over the document type, plus MethodInfo.Invoke on HandleAsync and reflective GetProperty on Task<>.Result. - FindOriginalProperty / FindDeepestMemberExpression / RebuildMemberAccess — reflect over the document type's properties/fields when rebuilding join order-by member access. [RequiresUnreferencedCode] only (no MakeGenericType). IPolecatAsyncQueryProvider.ExecuteAsync<TResult> picks up matching attributes so the interface contract is precise; EventLinqQueryProvider (the only other implementer) gets the same per-method treatment. References #46. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace class-level [UnconditionalSuppressMessage] for IL2026/IL3050 with method-level [UnconditionalSuppressMessage] on InsertEventAsync — the only method in DocumentSessionBase that hosts the [RequiresUnreferencedCode] / [RequiresDynamicCode] surface (two Serializer.ToJson(object) call sites for @event.Data and @event.Headers). Method-level UnconditionalSuppressMessage (rather than RUC/RDC promotion) is the chip's documented fallback when the propagation chain would reach the public interface surface — in this case IDocumentSession.SaveChangesAsync, which is outside the scope of #46's reflective-callsite tightening. The AOT escape hatch remains: AOT consumers supply a source-generator-backed ISerializer; the default reflective STJ ISerializer is the only thing that trips RUC/RDC on these call sites. References #46. 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
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>
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
Replaces class-level
[UnconditionalSuppressMessage]umbrellas on Polecat's reflective surfaces with method-level annotations that name the exact contract. DownstreamIsAotCompatible=trueconsumers now see a precise punch-list instead of a wall of silenced classes. Ticks the "Audit reflective surfaces that aren't covered byPolecat.CodeGeneration. Annotate or migrate." checkbox on #46.Polecat.AotSmoke(from #102/#106) still builds clean — the gate confirms no AOT regression.Before/after
Projections/PolecatCompositeProjection.csSnapshot<T>Projections/PolecatProjectionOptions.csSnapshot<T>+AddSnapshotProjection<T>Linq/PolecatLinqQueryProvider.csCreateQuery,ExecuteAsync,ExecuteGroupBy/ExecuteGroupJoin, allInvoke*Handler*Async,FindOriginalProperty,FindDeepestMemberExpression,RebuildMemberAccess)Events/Linq/EventLinqQueryProvider.cs(sibling)Internal/DocumentSessionBase.csUnconditionalSuppressMessageonInsertEventAsyncLinq/IPolecatAsyncQueryProvider.cs(propagation)ExecuteAsync<TResult>so impls matchWhy method-level
UnconditionalSuppressMessageon DocumentSessionBase instead of RUC/RDC?The reflective surface in DocumentSessionBase is two
Serializer.ToJson(object)call sites inInsertEventAsync(a private method). Promoting [RUC]/[RDC] up the call chain would propagate toIDocumentSession.SaveChangesAsync— a public interface change beyond #46's scope. Per the chip's style guide, method-levelUnconditionalSuppressMessageis the documented fallback when "no method-level annotation expresses the constraint cleanly" — in this case "cleanly" includes "without widening scope to the public surface." Justifications name the exact call sites.EventLinqQueryProvider wasn't in the chip's primary four but came in scope because adding RUC/RDC to
IPolecatAsyncQueryProvider.ExecuteAsync(required to matchPolecatLinqQueryProvider's impl annotation) cascaded an IL2046 mismatch on the only other implementer. Tightening both impls + the interface in one go is cleaner than leaving the sibling with a stale class-level umbrella.Verification
dotnet build src/Polecat/Polecat.csproj -c Release— clean (same 7 pre-existing IL warnings asmain, all in files outside this PR's scope; no new warnings introduced)dotnet build src/Polecat.AotSmoke/Polecat.AotSmoke.csproj -c Release— clean (0 warnings, 0 errors on net9.0 + net10.0); smoke surface migrated fromopts.Projections.Snapshot<Quest>(...)(now correctly RUC/RDC) to the AOT-safeopts.Projections.Add<QuestProjection>(ProjectionLifecycle.Inline)pattern with a concreteSingleStreamProjection<Quest, Guid>subclassdotnet build src/Polecat.EntityFrameworkCore.Tests/Polecat.EntityFrameworkCore.Tests.csproj -c Release— clean (only pre-existing CS8767 nullability noise unrelated to AOT)dotnet run --project src/Polecat.AotSmoke -c Release— exits 0 on both net9.0 + net10.0Local test suite couldn't be run end-to-end — the Docker SQL Server image is
linux/amd64and the host isarm64, so emulated connection handshakes time out before tests can even open a connection (every test fails atConnectionSource.DetectNativeJsonSupport()withConnection Timeout Expired, handshake=3881ms). All test failures are environmental; the changes are annotation-only with no runtime semantics, so the existing CI run on amd64 is the authoritative verification.Out of scope (per chip's "Don't widen scope")
Pre-existing class-level suppressions remain in:
DocumentStore.{EventStoreExplorer,ProjectionReplay}.cs,AdvancedOperations.cs,AdvancedSqlResultReader.cs,Serialization/Serializer.cs,Events/EventGraph.cs,Events/EventOperations.cs,Events/Daemon/PolecatEventLoader.cs,Events/Internal/PcEventsRowReader.cs,Internal/QuerySession.cs,Internal/DocumentProvider*.cs,Internal/Batching/*.cs,Storage/DocumentMapping*.cs,Linq/{NonStaleDataExtensions,SoftDeletes/SoftDeletedExtensions,Metadata/*,Selectors/DeserializingSelector,Joins/JoinResultSelectorRewriter,Parsing/*,QueryHandlers/*,PolecatQueryableExtensions,LinqExtensions}.cs,Patching/*.cs,Projections/{PolecatProjectionStorage,Flattened/{StatementMap,EventDeleter}}.cs,PolecatConfigurationExpression.cs,PolecatStoreServiceCollectionExtensions.cs. A follow-up chip can sweep these.Also out of scope: migrating reflective patterns to
GenericFactoryCache/ source generators (separate Polecat#46 checkbox under the cold-start pillar).Commit shape
ae97c3b— Projections cluster (PolecatCompositeProjection + PolecatProjectionOptions) + AotSmoke surface migration112da8b— LINQ providers (PolecatLinqQueryProvider + IPolecatAsyncQueryProvider + EventLinqQueryProvider sibling)b1de9f6— DocumentSessionBaseReferences #46.
🤖 Generated with Claude Code