Skip to content

fix(#4966): natural key extractor handles a two-arg static evolve method#528

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/4966-natural-key-update-extractor
Jul 17, 2026
Merged

fix(#4966): natural key extractor handles a two-arg static evolve method#528
jeremydmiller merged 1 commit into
mainfrom
fix/4966-natural-key-update-extractor

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Problem

A natural key that changes via an update event goes unrecorded. Given a self-aggregating aggregate whose [NaturalKeySource] methods are:

[NaturalKeySource] public static Product Create(ProductRegistered e) => new() { Code = new(e.ProductCode) };
[NaturalKeySource] public static Product Apply(ProductCodeChanged e, Product current) => current with { Code = new(e.NewProductCode) };

the two-arg Apply evolve method was silently skipped during natural-key discovery, so no NaturalKeyEventMapping was built for ProductCodeChanged. The mt_natural_key_X lookup table only ever recorded the create event's key and never the changed key — on live inline append or rebuild — leaving FetchLatest-by-natural-key unable to find the stream under its new key.

Reported against a projection rebuild in marten#4966.

Root cause

buildExtractor's static-factory branch matched any static method on the aggregate returning the aggregate type, but composed a one-argument Expression.Call. A two-arg evolve method threw ArgumentException while building the call, which the surrounding catch { /* silently skip */ } swallowed.

Fix

The static-on-aggregate branch now builds one argument per parameter:

  • the event parameter receives the raw event data (converted),
  • a TDoc parameter (the prior aggregate) receives a fresh default aggregate — mirroring the existing instance-method branch.

Parameters that can't be satisfied from event data alone (IEvent<T> metadata, or a doc type with no public parameterless ctor) fall through to the existing property-matching path, so no previously working shape regresses. All 91 EventTests projection tests pass.

Verification

  • New NaturalKeySourceDiscoveryTests.discovers_natural_key_source_on_static_evolve_method_that_changes_the_key.
  • Proven end-to-end against Marten with a local package: the marten#4966 repro fails on 2.28.0 ("should not be null but was") and passes with this fix.

Ships as 2.28.1. The superseding Marten PR (retaining the original repro author) bumps to it and closes marten#4966.

🤖 Generated with Claude Code

A natural key that CHANGES via an update event goes unrecorded: the aggregate's
`[NaturalKeySource]` evolve method — `static TDoc Apply(TEvent e, TDoc current)`
— was silently skipped during natural-key discovery, so no NaturalKeyEventMapping
was built for the update event. The `mt_natural_key_X` lookup table therefore only
ever recorded the CREATE event's key and never the changed key (on live append OR
rebuild), leaving FetchLatest-by-natural-key unable to find the stream under its new
key. Reported against a projection rebuild in marten#4966.

Root cause: buildExtractor's static-factory branch matched any static method on the
aggregate returning the aggregate type but built a one-argument call, so a two-arg
evolve method threw ArgumentException while composing the Expression.Call and was
swallowed by the surrounding catch. Now the branch builds one argument per parameter:
the event parameter receives the raw event data (converted); a TDoc parameter (the
prior aggregate) receives a fresh default aggregate, mirroring the instance-method
branch. Parameters that can't be satisfied from event data alone (IEvent<T> metadata,
or a doc type with no public parameterless ctor) fall through to the existing
property-matching path, so no previously working shape regresses.

Adds NaturalKeySourceDiscoveryTests coverage for the evolve shape and bumps to 2.28.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKKBPsFJ83jd2o4hyGJnj6
@jeremydmiller
jeremydmiller merged commit 03c05a7 into main Jul 17, 2026
1 check passed
@jeremydmiller
jeremydmiller deleted the fix/4966-natural-key-update-extractor branch July 17, 2026 20:29
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