Repository navigation
Build a missing aggregate through its constructor; let a supertype Create own the null-snapshot case (#1087) - #1090
Conversation
…eate own the null-snapshot case (#1087) The null-snapshot branch built an aggregate with required members as `new T { Required = default!, ... }`, which overwrote member initializers such as `public required List<string> Emails { get; set; } = [];` with null. Marten 8 ran the constructor, so the initializers ran. - `required` is compile-time only: emit `(T)Activator.CreateInstance(typeof(T))!` so the constructor and initializers run (trim/AOT safe, typeof literal) - emit `new T()` when the parameterless constructor is [SetsRequiredMembers] - drop Apply-fallback arms already matched by an earlier Create for a base type or interface (e.g. a catch-all `Create(object)`), which were unreachable (CS8120) and broke the consumer build; the Create wins, as it did in Marten 8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ace of #1088's copied initializers #1088 fixed the same regression by copying self-contained required-member initializers into the object initializer. Activator.CreateInstance(typeof(T)) runs the constructor and every initializer, so it also covers initializers that need a using or a member of the aggregate (Guid.NewGuid(), a constant) and members set in the constructor body. The initializer-copy helpers are removed. #1088's fixture is kept; its two emitted-text tests now assert construction through the constructor, and EventTests checks at runtime that a Guid.NewGuid() initializer, a constant and a constructor-body assignment all survive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main, which brought in #1088's fix for the same regression. #1088 copies a required member's initializer into the object initializer when that initializer can be copied safely. This PR builds the aggregate with #1088's 13-member fixture is kept in Thanks to Kenneth Skovhede for #1088 and for that fixture. 🤖 Generated with Claude Code |
Fixes #1087
Problem
For an Apply-only event on an aggregate that does not exist yet, the source generator built the aggregate as
new T { Required = default!, ... }whenever it hadrequiredmembers. That overwrote initializers likepublic required List<string> Emails { get; set; } = [];withnull, soApplythrew. Marten 8 ran the constructor, so initializers ran. A catch-allCreate(object)intended to take control of that case made every Apply-fallback arm unreachable instead (CS8120, an error inside generated code).Fix
BuildAggregateConstructorExpression:requirednew T()new T()[SetsRequiredMembers]new T { X = default!, … }new T()requiredmembersnew T { X = default!, … }(T)Activator.CreateInstance(typeof(T))!GetUninitializedObjectrequiredis compile-time only, soActivator.CreateInstanceruns the constructor and every initializer, as Marten 8 did; with atypeofliteral it stays trim/AOT safe. Only paid on the first event for a missing aggregate.Null-snapshot
switch: Apply-fallback arms already matched by an earlierCreatefor a base type or interface are no longer emitted. Fixes CS8120 forCreate(object)and for an interfaceCreate(IDocEvent)+Apply(Renamed)whereRenamed : IDocEvent. TheCreatewins, as in Marten 8, soCreate(object _) => nullignores events for documents that don't exist.Behaviour change: an Apply-only event on a missing aggregate now sees initializer values where it used to see
null.Not in scope (follow-up):
Create(IEvent)recognition, and a constructor taking the event as a creation on the projection path.Tests
RequiredMemberConstructionTests(generator): 4 tests, all failing on the old generatorBug_1087_required_members_on_missing_aggregate(EventTests, real generated evolver): initializers survive,Create(object) => nullyields no snapshot, Apply still runs on an existing aggregate; the file does not compile on the old generator🤖 Generated with Claude Code