gRPC saga coverage: message-identified sagas work like HTTP; header-identified gap pinned (refs #3385)#3386
Merged
Conversation
…erFxGH-3385) Pins current behavior: a saga whose id is resolved from the envelope saga-id header (no id member on the message) cannot be started or continued over a gRPC service hop, because saga-id is not propagated by the client/server propagation interceptors nor seeded onto the invoked envelope by Executor.InvokeAsync<T>. The RPC currently fails with an opaque StatusCode.Internal. Uses a standalone in-memory host (default in-memory saga persistence, no database) modeled on the ServerPropagation test fixtures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new gRPC-focused characterization test suite to lock in the current behavior described in #3385: header-identified sagas (saga id resolved from the saga-id envelope header) fail across a gRPC hop because saga-id is not propagated, and the client sees an opaque StatusCode.Internal.
Changes:
- Introduces a new
SagaOverGrpc/test fixture that hosts an in-process gRPC server and client overTestServer. - Adds code-first gRPC contracts + service that forwards requests to
IMessageBus.InvokeAsync<TResponse>()to reproduce the saga behavior. - Adds a characterization test asserting the current opaque
Internalfailure mode when starting a header-identified saga over gRPC.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Wolverine.Grpc.Tests/SagaOverGrpc/SagaOverGrpcFixture.cs | New in-process gRPC host/channel fixture wired to just the saga handler + service. |
| src/Wolverine.Grpc.Tests/SagaOverGrpc/SagaOverGrpcContracts.cs | Code-first gRPC contract and DTOs (intentionally id-less request to force envelope-based saga id resolution). |
| src/Wolverine.Grpc.Tests/SagaOverGrpc/saga_over_grpc_tests.cs | Characterization test asserting current StatusCode.Internal + saga-id resolution failure detail. |
| src/Wolverine.Grpc.Tests/SagaOverGrpc/CountingSagaGrpcService.cs | gRPC service shim that forwards to Wolverine InvokeAsync<T>() per docs. |
| src/Wolverine.Grpc.Tests/SagaOverGrpc/CountingSaga.cs | Minimal header-identified saga state/handler used to trigger the failure path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+1
to
+5
| using Grpc.Net.Client; | ||
| using Microsoft.AspNetCore.Builder; | ||
| using Microsoft.AspNetCore.TestHost; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; |
…asperFx#3386) Matches the sibling ServerPropagation fixtures, which don't import it. Verified: test project builds clean without it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JasperFxGH-3385) Mirrors Wolverine.Http.Tests.building_a_saga_and_publishing_other_messages_ from_http_endpoint over a gRPC hop: a saga whose id rides on the request DTO starts, persists, continues, and completes through the same Bus.InvokeAsync pipeline an HTTP endpoint uses. Proves Jeremy's expectation that sagas "work just like HTTP" over gRPC for the message-identified case. Asserts persistence via the in-memory saga persistor (no database), the in-memory equivalent of the HTTP test's Marten LoadAsync. Extends the existing SagaOverGrpcFixture to host both the message-identified (ReservationSaga) and header-identified (CountingSaga) scenarios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jeremydmiller
approved these changes
Jul 12, 2026
jeremydmiller
left a comment
Member
There was a problem hiding this comment.
Great no-regret groundwork — the in-memory persistor assertions keep this database-free, and the characterization test is exactly what we want pinned ahead of the GH-3385 decision. Merging.
jeremydmiller
added a commit
that referenced
this pull request
Jul 12, 2026
Adapts the HTTP + Sagas documentation for gRPC: same reservation example, all samples as mdsnippets regions sourced from the Wolverine.Grpc.Tests SagaOverGrpc suite merged in #3386. Documents message-carried saga identity as the supported path and the header-identified limitation (GH-3385). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Jul 13, 2026
A header-identified saga cannot work over a gRPC hop: the saga-id envelope header is not propagated across the call, so no id can be resolved. That is unchanged and is not what this fixes. What it fixes is the diagnostic. The caller used to get StatusCode.Internal with "Could not determine a valid saga state id for Envelope ..." -- true, but it names no cause and no remedy, and the remedy is not guessable, because the reason the id is missing is a property of the transport rather than of the message. It now maps to StatusCode.InvalidArgument with a detail that says the identity must be on the message body, why (the header does not cross the hop), how to supply it ([SagaIdentity] or a matching property), and where to read more. InvalidArgument rather than Internal is deliberate per AIP-193: the request cannot succeed as sent and retrying will not change that -- it is a caller-side contract problem, not a transient server fault. The mapping lives in WolverineGrpcExceptionMapper, which only runs at the gRPC boundary, so the core exception is untouched and non-gRPC users see no change. Message-identified sagas over gRPC already work and are unaffected (covered by #3386). Per @erikshafer's analysis on the issue, full three-point saga-id propagation is deliberately NOT taken: HTTP does not exercise the header path either, so "works like HTTP" is already satisfied, and the complexity is not warranted until someone shows a concrete need. Flips the #3386 characterization test, and updates the gRPC + Sagas docs page. Full Wolverine.Grpc.Tests 273/273. Full wolverine.slnx Release build clean. Closes #3385. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Jul 13, 2026
#3403) A header-identified saga cannot work over a gRPC hop: the saga-id envelope header is not propagated across the call, so no id can be resolved. That is unchanged and is not what this fixes. What it fixes is the diagnostic. The caller used to get StatusCode.Internal with "Could not determine a valid saga state id for Envelope ..." -- true, but it names no cause and no remedy, and the remedy is not guessable, because the reason the id is missing is a property of the transport rather than of the message. It now maps to StatusCode.InvalidArgument with a detail that says the identity must be on the message body, why (the header does not cross the hop), how to supply it ([SagaIdentity] or a matching property), and where to read more. InvalidArgument rather than Internal is deliberate per AIP-193: the request cannot succeed as sent and retrying will not change that -- it is a caller-side contract problem, not a transient server fault. The mapping lives in WolverineGrpcExceptionMapper, which only runs at the gRPC boundary, so the core exception is untouched and non-gRPC users see no change. Message-identified sagas over gRPC already work and are unaffected (covered by #3386). Per @erikshafer's analysis on the issue, full three-point saga-id propagation is deliberately NOT taken: HTTP does not exercise the header path either, so "works like HTTP" is already satisfied, and the complexity is not warranted until someone shows a concrete need. Flips the #3386 characterization test, and updates the gRPC + Sagas docs page. Full Wolverine.Grpc.Tests 273/273. Full wolverine.slnx Release build clean. Closes #3385. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #3385.
Adds gRPC saga tests that mirror the existing HTTP endpoint + saga coverage, proving sagas work over gRPC the same way they do over HTTP — and documenting the one case that doesn't (which HTTP shares). See the findings summary on #3385.
What's here
Happy path — message-identified sagas work over gRPC (green).
can_start_and_continue_a_message_identified_saga_over_grpcis the gRPC parallel ofWolverine.Http.Tests.building_a_saga_and_publishing_other_messages_from_http_endpoint: a saga whose id rides on the request DTO is started, persisted, continued, completed, and its state deleted — all through the sameBus.InvokeAsyncpipeline a[WolverinePost]endpoint uses. Persistence is asserted via the in-memory saga persistor, so no database is required.Characterization — header-identified sagas hit the same limitation HTTP has.
starting_a_header_identified_saga_over_grpc_fails_with_opaque_status_todaypins the current behavior: a saga whose id is resolved from the envelopesaga-idheader can't resolve it over a gRPC service hop (nothing carriessaga-idacross), surfacing as an opaqueStatusCode.Internal. When we land the scoped diagnostic (or full propagation) from #3385, this test's assertions flip.Notes
ServerPropagationfixtures.🤖 Generated with Claude Code