Repository navigation
Carry a projection's published types on SubscriptionDescriptor (#1102) - #1106
Merged
Merged
Conversation
AggregateType only answers for an aggregation, so an event projection or a composite gave a tool nothing to link it to the documents it writes, although PublishedTypes() already knew. The descriptor is built from the non-generic ISubscriptionSource, which cannot reach that generic member, so a small IPublishesTypes interface does, the way IAggregateProjection does for the aggregate. Every IProjectionSource is one through a default implementation, so no store or third-party source has anything to change, including one that implements PublishedTypes() explicitly. The member stays declared on IProjectionSource because stores compile against it there. The wrappers were the risk, and one was wrong. ProjectionSourceWrapperBase kept its copy of the source's types behind an explicit interface implementation beside ProjectionBase's public method, which never saw them. An interface binds to that public method on anything deriving from ProjectionBase, so a scoped projection read as writing only what was in Options.StorageTypes. Assembly hid this by copying the types there as teardown targets, until a projection opted out of teardown. It is an override now. Describe() on the same wrapper also reports a newly resolved instance, which has never seen the Options configured at registration, so the wrapper supplies the list itself. The value is read at describe time on projections nothing may have validated, so it filters the void and Task return types an event projection records from its constructor, and a PublishedTypes() that throws leaves the list empty rather than losing the whole Describe() pipeline. It is sorted by full name so that a descriptor does not reorder between runs, and a payload with no such field, or a null one, reads as empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every store wraps projection sources of its own, and a store that left PublishedTypes empty would look the same as one whose projections write nothing, so the check asks each store through TryCreateUsage for one of each kind. It lives in EventProjectionRegistrationCompliance, the suite about what a projection publishes, because the in-memory store can run that one and cannot take the explorer suite's async registration. The composite and subscription facts need a daemon, so they skip there. Running it found that the in-memory store's TryCreateUsage never called Projections.Describe, so it read as a store with no projections at all. That is fisher#120 again, and Marten, Polecat and Fisher all make the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 2, 2026
erdtsieck
pushed a commit
to erdtsieck/jasperfx
that referenced
this pull request
Nov 3, 2026
…es out of teardown Three gaps in how projections describe themselves, each reproduced with a failing test first. A container-scoped source wrapper described a freshly resolved instance, so the name, version, lifecycle, event filters and shards set at registration were lost: an Inline projection reported Async with daemon metrics, and a renamed one reported a shard that does not run. The wrapper now overlays what registration decided onto that description. CompositeProjection.PublishedTypes() dropped the composite's own declarations. It now includes them after its members. Fisher's override did this for Fisher alone (fisher#63) and is now redundant. An event projection recorded void, Task and ValueTask as published types from its constructor. A generated dispatcher skips method validation and the generator drops such a method, so these reached Options.CleanUps on a projection that passed validation. They now stop at EventProjectionApplication.PublishedTypes(), which makes the descriptor-only filter from JasperFx#1106 dead code, so that is removed. Co-Authored-By: Claude Opus 5.5 <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.
Closes #1102.
SubscriptionDescriptoronly said what a projection produces throughAggregateType, which is null for event projections and composites. A tool reading the descriptor couldn't link those projections to the documents they write, even thoughPublishedTypes()already knew.What changes
SubscriptionDescriptor.PublishedTypesis a newTypeDescriptor[]: distinct, in ordinal order by full name, and never null.AggregateTypeis untouched.IPublishesTypesis a new non-generic interface, so the descriptor constructor needs no generics. EveryIProjectionSource<,>gets it through a default body, andPublishedTypes()stays declared onIProjectionSource<,>withnew, so stores compiled against it and explicit implementers keep working.ProjectionSourceWrapperBasehid its types behind an explicit interface implementation besideProjectionBase's public method, and itsDescribe()reports a freshly resolved instance that never saw the registration'sOptions. It now overrides the method and supplies the list itself.InMemoryDocumentStore.TryCreateUsagenow callsProjections.Describe. Without it the store reported no projections at all and couldn't pass the new compliance check.EventProjectionRegistrationCompliance.Decisions worth checking
IProjectionnever says. The docs say so.PublishedTypes()that throws or returns null gives an empty list rather than losing the wholeDescribe()pipeline, the same call the descriptor makes forImplementationType. That case looks the same as an empty answer.void,TaskandValueTaskare filtered out. An event projection recordsCreatereturn types in its constructor, ahead of validation, so they can appear on a projection nothing has validated yet.The issue's open question: EF Core projections
TDoc, the entity type, in Marten and Polecat. Fisher has no such classes, but an ordinary projection routed withProjectToEfCorealso returnsTDoc.EfCoreEventProjectionreturns nothing in all three stores unless something declares a type. Marten's isn't aProjectionBase, so only a registration-time teardown rule can add one.So a consumer can resolve single and multi stream projections against
DbContextentity lists, but not EF event projections. Other entities a projection touches through itsDbContextare never listed.Release note
SubscriptionDescriptor.PublishedTypesandIPublishesTypes.IProjectionSource<,>now inheritsIPublishesTypes.PublishedTypes()called through aProjectionBasereference now returns the full set. It used to return onlyOptions.StorageTypes.TryCreateUsageon the in-memory store now lists its projections.Additive on the wire: an old payload or a null field reads as empty, and an older reader ignores the field. Both are tested.
Testing
Full gate on this branch, locally on net9.0 and net10.0: 7,808 passed, 0 failed, 66 skipped. It ran before #1103 merged; this branch merges cleanly with
main, but the combination hasn't been run. Each guard was mutation-tested, and all 8 mutants were killed. One survived at first because assembly copied types intoOptions.StorageTypes, so its test was made stronger.The compliance suite ships as source, so it was validated against real stores from a local pack (
2.97.1-local.1102):event_projection_registration_compliance, net10.0 and net9.0Describefrom each store made exactly the 6 new facts fail, so the passes aren't vacuous.TypeLoadExceptionif the default body is removed.No store needs a change.
Not covered
CompositeIProjectionSourceand its scoped registrations can't be reached by a store-neutral suite. Those were checked by reading only.NAMEDATALEN100). Polecat's and Marten's wider suites weren't run, only this class.usage.EventsandRegisteredEventTypesstay empty, and it stays unenrolled from the explorer suite.🤖 Generated with Claude Code