Skip to content

Discover EventProjection published types semantically, not syntactically - #611

Merged
jeremydmiller merged 2 commits into
mainfrom
feat/semantic-published-type-discovery
Aug 2, 2026
Merged

Discover EventProjection published types semantically, not syntactically#611
jeremydmiller merged 2 commits into
mainfrom
feat/semantic-published-type-discovery

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes the trap that cost compliance wave 2 (#608) a red test. It is far worse for users than it was for suite authors.

The trap

AggregateAnalyzer.DiscoverDocumentTypesFromMethodBodies matched only GenericNameSyntax. So an explicit ApplyAsync override that wrote

operations.Store<AuditRecord>(record);   // registers AuditRecord
operations.Store(record);                // registers NOTHING

behaved completely differently for two spellings of the same call — and nothing tells you. It compiles, and it does not fail at runtime either: the store provisions that document's storage on demand, so only the ahead-of-time surfaces come up short (schema creation, known document types, rebuild teardown). Original context in marten#4166.

The fix

Bind the invocation. The type argument is on the method symbol whether or not it was written down, so both spellings give the same answer. The syntactic path stays as a fallback for trees that do not bind (mid-edit, broken code), so nothing regresses when the semantic model has nothing to say.

Receiver identification is deliberate rather than name-based: the call must bind to the projection's own TOperations — directly, through an interface, or as an extension method's first parameter — so an unrelated Store method on some other object produces neither a registration nor a diagnostic. There is a test for exactly that.

A hole the semantic path made visible

IsFrameworkType cannot decide whether a type is registrable. object and string render through ToDisplayString() as their C# keywords, not as System.Object / System.String, so a name-prefix test lets them straight through — Store<object>(...) was registering object as a published document type. Registrability is now a SpecialType / TypeKind question, with IsFrameworkType only as the last check.

New diagnostic: JFXEVT005 (Info)

Covers what is left after the fix — a call that binds to the projection's own session but whose document type cannot be named: object, dynamic, an open type parameter.

Info rather than Warning, on purpose. Registration is not always required — the type may be registered through store options, and storage is provisioned on demand regardless — so this must not break a TreatWarningsAsErrors build over a perfectly legitimate call.

Verification

  • JasperFx.Events.SourceGenerator.Tests 30/30, with four new cases: explicit spelling, inferred spelling, the unregistrable case, and the unrelated-receiver case.
  • EventTests 657 / 0 / 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde

jeremydmiller and others added 2 commits August 2, 2026 14:26
Closes the trap that cost compliance wave 2 (#608) a red test and is far worse
for users than for suite authors.

AggregateAnalyzer.DiscoverDocumentTypesFromMethodBodies matched only
GenericNameSyntax, so an explicit ApplyAsync override that wrote

    operations.Store<AuditRecord>(record);

registered AuditRecord as a published type, while the equally valid

    operations.Store(record);

compiled and registered nothing at all. Nothing at the call site says so, and
nothing fails at runtime either: the store provisions that document's storage on
demand, so only the ahead-of-time surfaces come up short -- schema creation,
known document types, rebuild teardown. See marten#4166 for the original.

Binding the invocation gives the same answer for both spellings, because the type
argument is on the method symbol whether or not it was written down. The
syntactic path stays as a fallback for trees that do not bind (mid-edit, broken
code), so nothing regresses when the semantic model has nothing to say.

Also fixes a hole the semantic path made visible: IsFrameworkType cannot decide
whether a type is registrable, because object and string render through
ToDisplayString() as their C# keywords rather than System.Object / System.String,
so a name-prefix test let them through. Store<object>(...) was registering
`object` as a published document type. Registrability is now a SpecialType /
TypeKind question, and IsFrameworkType is only the last check.

New JFXEVT005 (Info) covers what is left: a call that binds to the projection's
own session but whose document type cannot be named -- object, dynamic, an open
type parameter. Info rather than Warning on purpose. Registration is not always
required (the type may be registered through store options, and storage is
provisioned on demand regardless), so this must not break a
TreatWarningsAsErrors build over a legitimate call.

Receiver identification is deliberate rather than name-based: the call must bind
to the projection's own TOperations (directly, through an interface, or as an
extension method's first parameter), so an unrelated Store method on some other
object produces neither a registration nor a diagnostic.

JasperFx.Events.SourceGenerator.Tests 30/30 with four new cases covering both
spellings, the unregistrable case, and the unrelated-receiver case.
EventTests 657/0/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde
The published-type discovery change ships in JasperFx.Events.SourceGenerator, so
it needs its own version. The publish workflow pushes with --skip-duplicate: run
at an already-published version it skips every package and still reports success,
so a shippable change merged without a bump goes out as nothing at all.
@jeremydmiller
jeremydmiller merged commit 8289e4f into main Aug 2, 2026
1 check passed
@jeremydmiller
jeremydmiller deleted the feat/semantic-published-type-discovery branch August 2, 2026 20:19
jeremydmiller added a commit that referenced this pull request Aug 6, 2026
…ide (marten#5192) (#637)

* fix(generator): register published types via a PublishedTypes() override (marten#5192)

An EventProjection's discovered published document types were registered by emitting
`public MyProjection() { RegisterPublishedType(...); }` into the user's partial class.
A constructor is the wrong extension point, and it failed two ways.

It is illegal on a type that declares a primary constructor --
`partial class MyProjection(ILogger logger) : EventProjection` -- because C# requires
every other constructor to chain through the primary one, so the generated file broke
the build outright with CS8862. That is what marten#5192 reported.

Worse, and silent: a projection that takes dependencies has to be registered through
Marten's AddProjectionWithServices, so the container calls the dependency-taking
constructor and the generated parameterless one never ran. Published types went
unregistered, which also left #626's teardown registration -- it reads
PublishedTypes() -- with nothing to register.

Emit an override of the virtual ProjectionBase.PublishedTypes() instead. It does not
care how the instance was constructed, and chaining through base.PublishedTypes()
keeps hand-written RegisterPublishedType calls and Options.StorageTypes flowing. The
generator yields when the author already wrote their own override.

Both emission sites are covered: EmitEventProjectionTypeRegistrationPartial (an
ApplyAsync override) and the conventional-method path.

Neither defect was reachable before 2.38.0: discovery was syntactic, so only an
explicit `Store<Doc>(x)` produced a registration and the far more common `Store(doc)`
produced none. #611 made discovery semantic and both spellings started
emitting the constructor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde

* 2.42.1

---------

Co-authored-by: Claude Opus 5 (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