Repository navigation
Code-first gRPC client streaming: generate the IAsyncEnumerable<TRequest> -> Task<TResponse> shape - #3500
Merged
jeremydmiller merged 2 commits intoJul 19, 2026
Conversation
…cEnumerable<TRequest>) methods Follow-up to JasperFx#3459, closing its documented code-first gap. ClassifyMethod now recognizes the client-streaming shape as CodeFirstMethodKind.ClientStreaming (bidi stays cleanly skipped, pinned by test); the new ForwardCodeFirstClientStreamFrame forwards the inbound stream straight to IMessageBus.StreamAsync<TRequest, TResponse> with no stream adapter (protobuf-net.Grpc already hands the method an IAsyncEnumerable<TRequest>). Tenant detection is woven into client-streaming methods declaring a CallContext, matching proto-first; middleware/Validate stay deliberately unwoven. Manifest + capabilities surface the descriptors with the per-item element type. Fixes two stale doc comments (IGrpcChainPolicy "P3" remark, WolverineGrpcOptions default-filter doc), extends GreeterCodeFirstGrpc with CollectGreetings (run-verified) plus a README, and updates the gRPC guide, message-bus doc anchors, and CHANGELOG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR closes the remaining code-first (protobuf-net.Grpc) generation gap for client-streaming gRPC methods by recognizing the IAsyncEnumerable<TRequest> -> Task<TResponse> contract shape, generating the forwarding implementation to IMessageBus.StreamAsync<TRequest, TResponse>, extending endpoint manifest/capabilities reporting, and updating docs/samples/tests accordingly — all within Wolverine.Grpc and its surrounding documentation/sample surface.
Changes:
- Extend code-first method classification/codegen to support client-streaming (
Task<TResponse> Name(IAsyncEnumerable<TRequest>[, CallContext])) and keep bidi explicitly skipped. - Add client-streaming coverage to endpoint manifest/capabilities, and weave tenant detection for code-first client streaming when
CallContextis present (while continuing to skip before/after middleware + Validate for streaming shapes). - Expand tests and docs + sample updates to reflect the new generated shape and remaining limitation (code-first bidi still hand-written).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Wolverine.Grpc/WolverineGrpcOptions.cs | Update middleware filter doc to include code-first chains. |
| src/Wolverine.Grpc/IGrpcChainPolicy.cs | Correct policy doc to reflect code-first participation in the middleware pipeline. |
| src/Wolverine.Grpc/GrpcEndpointManifest.cs | Surface code-first client-streaming endpoints with GrpcRpcStreamKind.ClientStreaming. |
| src/Wolverine.Grpc/CodeFirstGrpcServiceChain.cs | Classify client-streaming shape; generate forwarding frame; weave tenant detection for client streaming; skip middleware weaving for that shape. |
| src/Wolverine.Grpc.Tests/MultiTenancy/TenantEchoContracts.cs | Add code-first client-streaming contract + handler for tenant detection coverage. |
| src/Wolverine.Grpc.Tests/MultiTenancy/tenant_detection_codegen_tests.cs | Add structural + e2e assertions for client-streaming tenant detection. |
| src/Wolverine.Grpc.Tests/grpc_endpoint_manifest_3265.cs | Extend manifest assertions to include code-first client-streaming descriptor behavior. |
| src/Wolverine.Grpc.Tests/grpc_capabilities_descriptor_source_3267.cs | Add capability assertions for code-first client-streaming origin reporting. |
| src/Wolverine.Grpc.Tests/CodeFirstCodegen/ICodeFirstTestService.cs | Add code-first client-streaming RPC and related DTOs + orphan contract shape. |
| src/Wolverine.Grpc.Tests/CodeFirstCodegen/CodeFirstTestHandlers.cs | Add handler that folds a streamed request into a single response and exercises fault/cancel paths. |
| src/Wolverine.Grpc.Tests/CodeFirstCodegen/code_first_codegen_tests.cs | Add classification, e2e, cancellation/fault/empty-stream, orphan/unimplemented, and “no adapter in generated source” assertions. |
| src/Samples/GreeterCodeFirstGrpc/Server/GreeterHandler.cs | Add client-streaming handler that folds a stream into a summary reply. |
| src/Samples/GreeterCodeFirstGrpc/README.md | New README documenting generated code-first unary/server/client streaming sample. |
| src/Samples/GreeterCodeFirstGrpc/Messages/IGreeterCodeFirstService.cs | Add code-first client-streaming contract method + summary DTO. |
| src/Samples/GreeterCodeFirstGrpc/Client/Program.cs | Demonstrate client-streaming call from a plain IAsyncEnumerable<TRequest>. |
| docs/guide/messaging/message-bus.md | Update streaming link anchor for client streaming section. |
| docs/guide/grpc/streaming.md | Document code-first client-streaming generation path and remaining bidi limitation. |
| docs/guide/grpc/samples.md | Update sample matrix and walkthrough to include code-first client streaming. |
| docs/guide/grpc/multi-tenancy.md | Update tenant-detection limitations to include code-first client-streaming support. |
| docs/guide/grpc/index.md | Update “Current Limitations” to reflect code-first bidi-only gap. |
| docs/guide/grpc/contracts.md | Update contract-shape warning to bidi-only and fix client-streaming references. |
| CHANGELOG.md | Add changelog entry for code-first client-streaming generation support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…lement type, not the bus message The class doc claimed the request type is always the published Wolverine message; for client-streaming the bus message is IAsyncEnumerable<TRequest> and the manifest deliberately surfaces the per-item element type. Addresses Copilot review feedback on PR 3500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikshafer
marked this pull request as ready for review
July 19, 2026 16:38
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.
Follow-up to #3459, which added proto-first client streaming (
stream TRequest → TResponse) via the newICommandBus.StreamAsync<TRequest, TResponse>overload and left one generated-path gap documented as "Not in scope": the code-first (protobuf-net.Grpc) implementation generator still skippedIAsyncEnumerable<TRequest>-parameter methods. This PR closes that gap — all three generatable code-first shapes (unary, server-streaming, client-streaming) are now code-generated, and the change is contained toWolverine.Grpcplus docs/sample. No core changes: the bus primitive from #3459 already does everything the generated body needs.The classifier
CodeFirstGrpcServiceChain.ClassifyMethodpreviously rejected any streamed first parameter via itsIsClasscheck (interfaces aren't classes), which is why the shape was cleanly skipped rather than misclassified. The new rule branches on the first parameter before the concrete-DTO check:Task<TResponse> Name(IAsyncEnumerable<TRequest>[, CallContext])→ newCodeFirstMethodKind.ClientStreaming(appended enum member, same append-only caution asGrpcRpcStreamKindin Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload #3459)IAsyncEnumerable<TResponse> Name(IAsyncEnumerable<TRequest>[, CallContext])— the bidi shape — still returns null, and there's now a test pinning that it stays skipped, not misclassified.The generated body
The pleasant surprise from #3459 repeats, but stronger: on the code-first path no stream adapter is needed at all. Proto-first receives
IAsyncStreamReader<TRequest>and needs theWolverineGrpcStreamAdapters.ReadAllAsyncshim; protobuf-net.Grpc hands a code-first method the inbound stream asIAsyncEnumerable<TRequest>directly — exactly what the bus overload consumes. The newForwardCodeFirstClientStreamFrameemits a one-liner:There's a test asserting
ReadAllAsyncdoes not appear in the generated source, so the adapter can't quietly creep in.CancellationTokenresolves through the existingCallContextCancellationTokenSourcevariable source, same as the unary/server-streaming frames.Tenant detection: woven, closing a parity gap
The old unary-only restriction on code-first tenant detection existed because server-streaming methods return
IAsyncEnumerable<T>directly and can't await the detection call. Client streaming returnsTask<TResponse>, so the generated body can await detection before the drain — this PR weavesDetectGrpcTenantIdFrameinto client-streaming methods that declare aCallContextparameter, with the sameForceAwaitforcing rule as unary. That gives code-first parity with proto-first (where every shape carries aServerCallContext). Covered by a structural test (detection appears exactly once per RPC method in the generated source) and an end-to-end test that disables the runtime propagation interceptor so only the woven frame can move the header value.Middleware and Validate: deliberately NOT woven
Matching the documented proto-first rationale — before-frames (including the
Validate → Status?short-circuit) need a singleTRequestin scope when the method begins, which the streaming signature doesn't provide. The weaving block inAssembleTypesnow skips client-streaming methods entirely (befores, discovered afters, and global postprocessors). I considered a context-only-middleware carve-out (weaving befores whose parameters don't include the request) and rejected it: it would make hook applicability depend on parameter lists in a way neither path does today, and proto-first would then be the inconsistent one.Everything around the edges
GrpcRpcStreamKind.ClientStreaming,RequestType= the per-item element type (the actual bus message isIAsyncEnumerable<TRequest>), response unwrapped fromTask<TResponse>— exact parity with what Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload #3459 did for proto-first.ClientStreamingServerHandleroverride from Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload #3459 is shape-based and applies to both contract styles; the mid-drain-fault test (InvalidOperationException→FailedPrecondition) proves it. Bidi interception stays deferred and its doc comment stays honest.CreateGrpcService<T>()with a plainIAsyncEnumerable<TRequest>.IGrpcChainPolicyclaimed code-first chains "do not yet participate in the Chain<> middleware pipeline (P3)" — false since the middleware weaving landed;WolverineGrpcOptions.AddMiddlewaresaid the default filter matches "every proto-first and hand-written" chain whileIsGrpcChainmatches all three kinds.Breaking surface
None breaking. New public surface:
CodeFirstMethodKind.ClientStreaming(appended). Behavior change worth noting: a code-first contract method with the client-streaming shape that was previously ignored will now be generated and mapped — if such a method exists with no matching handler, startup still succeeds (pinned by test) and calls fail withUnimplemented(the bus'sNotSupportedExceptionthrough the default mapping) instead of gRPC's unmapped-method error.Tests, sample, docs
Wolverine.Grpc.Testsgrows 281 → 294, all passing: classification (client-streaming with/withoutCallContext, bidi-skip pin), e2e round-trip, empty stream, mid-drain fault, cancellation, no-handlerUnimplemented, orphan-contract construction-succeeds, no-adapter source assertion, tenant detection (structural + e2e), manifest + capabilities descriptors. CoreTests unchanged (no core code touched) and green on net9.0 + net10.0;wolverine.slnxRelease build clean.GreeterCodeFirstGrpcgainsCollectGreetings(IAsyncEnumerable<GreetRequest>) → GreetingSummarymirroring the proto-first sample, plus a README it never had. Verified by actually running server + client:Docs:
streaming.mdrestructures the client-streaming section into proto-first/code-first subsections and drops the "code-first not generated" warning; the "proto-first only" limitation bullets inindex.md/streaming.mdare replaced by the one remaining honest limitation (code-first bidi);contracts.md's shape-comparison warning now lists all three generated code-first shapes;multi-tenancy.mdupdates the unary-only claim;samples.mdtable + walkthrough; renamed-anchor fixes incontracts.md/samples.md/message-bus.md; CHANGELOG entry following the Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload #3459 precedent.🤖 Generated with Claude Code