Skip to content

Accept *Async suffix on saga method names (#2578)#2593

Merged
jeremydmiller merged 1 commit intomainfrom
bugfix/2578-saga-async-method-names
Apr 26, 2026
Merged

Accept *Async suffix on saga method names (#2578)#2593
jeremydmiller merged 1 commit intomainfrom
bugfix/2578-saga-async-method-names

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Fixes #2578.

The bug

HandlerDiscovery already treats saga methods named StartAsync / HandleAsync / OrchestrateAsync / ConsumeAsync / StartOrHandleAsync / NotFoundAsync as valid — it strips the Async suffix when matching against the canonical name list (HandlerDiscovery.cs:54-55). But SagaChain.findByNames matched on strict string equality, so async-suffixed methods were:

  • discovered into the handler graph (so the saga thinks it handles the message),
  • silently dropped from StartingCalls / ExistingCalls / NotFoundCalls,
  • compiled into a generated chain that constructs the saga but never invokes the user's method,
  • persisted with Saga.Id == Guid.Empty,
  • failed on insert with ArgumentOutOfRangeException: You must define the saga id when using the lightweight saga storage.

The reporter's observation: the failure mode is uniquely confusing because the documentation already mentions StartAsync as if it works (docs/guide/durability/sagas.md:374).

The fix

SagaChain.findByNames now matches both the bare name and its async-suffixed twin:

return Handlers
    .Where(x => x.HandlerType.CanBeCastTo<Saga>()
                && methodNames.Any(n =>
                    x.Method.Name == n || x.Method.Name == n + "Async"))
    .ToArray();

This is option 1 from the issue's proposed fixes — making SagaChain symmetric with HandlerDiscovery's convention.

Tests

Bug_2578_saga_async_method_names covers both halves:

Discovery-levelStartingCalls / ExistingCalls / NotFoundCalls are populated for *Async methods across every saga convention (StartAsync, HandleAsync, OrchestrateAsync, ConsumeAsync, StartOrHandleAsync, NotFoundAsync).

End-to-endStartAsync and HandleAsync are actually invoked by the generated handler, saga state mutates correctly, and persists in InMemorySagaPersistor.

8 new tests. All 8 fail on main, all 8 pass with this change. All 1352 other CoreTests still pass (1360 total).

Docs

docs/guide/durability/sagas.md "Method Conventions" section now documents the *Async variants alongside the existing convention table, with a small code sample.

Test plan

  • New Bug_2578_* tests fail without the fix
  • New Bug_2578_* tests pass with the fix
  • All other saga-related CoreTests pass (87 total)
  • Full CoreTests pass (1360 total)

🤖 Generated with Claude Code

HandlerDiscovery has always treated saga methods named StartAsync /
HandleAsync / OrchestrateAsync / ConsumeAsync / NotFoundAsync as valid —
it strips the Async suffix when matching against the canonical method
name list. But SagaChain.findByNames matched on strict string equality,
so async-suffixed methods were discovered into the handler graph and
then silently dropped from StartingCalls / ExistingCalls / NotFoundCalls.
The generated chain would construct the saga but never invoke the user's
method, leaving Saga.Id == Guid.Empty and throwing on insert with a
confusing "must define the saga id" message.

Fix: SagaChain.findByNames now accepts both the bare name (e.g. "Start")
and its async-suffixed twin (e.g. "StartAsync"), making it symmetric
with HandlerDiscovery.

Tests: Bug_2578_saga_async_method_names exercises both halves —
discovery-level (StartingCalls / ExistingCalls / NotFoundCalls populated
for *Async methods across Start, Handle, Orchestrate, Consume,
StartOrHandle, NotFound) and end-to-end (StartAsync / HandleAsync are
actually invoked, saga state mutates and persists). 8/8 fail on main,
8/8 pass with this change. All other 1352 CoreTests still pass.

Docs: docs/guide/durability/sagas.md "Method Conventions" section now
documents that every name accepts the *Async variant for Task-returning
methods.

Co-Authored-By: Claude Opus 4.7 (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.

SagaChain.findByNames does not strip Async suffix; saga StartAsync / HandleAsync are discovered but silently skipped

1 participant