Skip to content

Fix SG evolver construction for required-member aggregates (marten#4542)#362

Closed
jeremydmiller wants to merge 1 commit into
mainfrom
fix/4542-required-members-primary-ctor
Closed

Fix SG evolver construction for required-member aggregates (marten#4542)#362
jeremydmiller wants to merge 1 commit into
mainfrom
fix/4542-required-members-primary-ctor

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

What changed

The source-generated evolver's Apply-only / null-snapshot branch synthesized new T { Required = default! } for any aggregate with required members. For a primary-constructor record — e.g. record DiagnostiekActiviteit(string Id) { required Guid? SubContractorId; ... } — that object initializer needs a parameterless constructor the record doesn't have, so the generated *.Evolver.g.cs failed to compile with CS7036 (or CS9035 when the id was supplied positionally).

This guards the default! object initializer on the type actually having an accessible public parameterless constructor (or being a struct). When there isn't one (a primary-ctor record), it falls back to RuntimeHelpers.GetUninitializedObject(typeof(T)), which allocates without invoking a ctor or enforcing required members; Apply then populates them. default! is now only ever emitted when there's a public no-arg ctor for the initializer to call.

Why

Addresses JasperFx/marten#4542. A required-member record aggregate projected by a partial SingleStreamProjection with conventional Create/Apply produced uncompilable generated code.

Validation

  • dotnet test src/JasperFx.Events.SourceGenerator.Tests — green (added regression tests: record-with-Create/Apply compiles; record Apply-only synthesizes via GetUninitializedObject, never default!; required-member plain class still synthesizes via the default! object initializer — Marten's sample_external-account-link pattern).
  • End-to-end against Marten release/9.0.0: a partial SingleStreamProjection over a required-member primary-ctor record fails to build with the released generator (CS7036) and both builds and runs correctly with this fix (event stream → CreateApply → loaded document has every required member set).

Relationship to #359

This converges on the same construction rule as #359 (emit default! only with a public parameterless ctor; otherwise GetUninitializedObject). #359 is broader — it also fixes marten#4543 (the nullable hint-name AddSource crash) and removes the redundant standalone-evolver emission when a partial projection already covers the aggregate.

This PR is a focused, regression-tested fix for the marten#4542 construction bug. Maintainer's call whether to land this and bring #359's marten#4543 + duplicate-evolver fixes separately, or land #359 as the superset.

…tes (marten#4542)

The Apply-only / null-snapshot branch synthesized `new T { Required = default! }`
for any aggregate with `required` members. For a primary-constructor record
(`record DiagnostiekActiviteit(string Id) { required ... }`) that object
initializer needs a parameterless ctor the record doesn't have, so the
generated evolver failed to compile with CS7036 (or CS9035 when the id was
supplied positionally).

Guard the `default!` object initializer on the type actually having an
accessible public parameterless constructor (or being a struct). When there
isn't one — a primary-ctor record — fall back to
`RuntimeHelpers.GetUninitializedObject(typeof(T))` instead, which allocates
without invoking a ctor or enforcing required members; Apply then populates
them. `default!` is now only ever emitted when there is a public no-arg ctor
for the initializer to call.

Verified end-to-end against Marten release/9.0.0: a partial
SingleStreamProjection over a required-member primary-ctor record fails to
build with the released generator and both builds and runs correctly with
this fix.

Overlaps with #359 (which converges on the same construction rule and also
addresses marten#4543 plus the redundant standalone-evolver emission); kept
here as a focused, regression-tested fix for the marten#4542 construction bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller
Copy link
Copy Markdown
Member Author

Closing in favor of #359, which is the more complete fix.

I verified empirically that this focused PR resolves only a narrower slice of marten#4542 (the partial-projection construction expression). When I added #359's own test cases to this branch, both failed:

  • partial_projection_for_required_member_aggregate_suppresses_snapshot_fallback — the faithful reporter scenario (partial projection + opts.Snapshot<T>() + the record's event ctor) still emits a redundant standalone evolver whose new T(data) event-ctor path fails with CS9035. This PR's construction-expression change doesn't touch the event-ctor path or suppress the duplicate evolver — Fix source generator evolver and hint-name failures #359's MarkSeen change does.
  • nullable_aggregate_parameter_attribute_does_not_crash_hint_name_generationCS8785 hint-name crash (marten#4543), which this PR doesn't address.

Conversely, #359 passes its own 18 tests and the 3 construction-focused tests from this PR (21/21), so it's a strict superset and converges on the same construction rule (emit default! only with an accessible public parameterless ctor; otherwise GetUninitializedObject).

Thanks @erdtsieck#359 is the one to land. Happy to contribute the 3 extra construction-focused regression tests from here if useful.

@jeremydmiller jeremydmiller deleted the fix/4542-required-members-primary-ctor branch May 23, 2026 17:45
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