Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload#3459
Open
erikshafer wants to merge 3 commits into
Open
Proto-first gRPC client streaming via new StreamAsync<TRequest, TResponse> overload#3459erikshafer wants to merge 3 commits into
erikshafer wants to merge 3 commits into
Conversation
Adds the fourth canonical gRPC RPC shape (stream TRequest -> TResponse) to the proto-first codegen path, backed by a new core streaming-input primitive: - IMessageBus.InvokeStreamAsync<TRequest, TResponse>(IAsyncEnumerable<TRequest>) dispatches a whole inbound stream to one handler invocation keyed on the closed IAsyncEnumerable<TRequest> message type, awaiting a single response. Local invocation only; reuses the existing Executor.InvokeAsync path. - GrpcServiceChain drops the client-streaming fail-fast, classifies onto a new ClientStreamingMethods list, and emits ForwardClientStreamToMessageBusFrame. WolverineGrpcStreamAdapters.ReadAllAsync bridges IAsyncStreamReader<T> to IAsyncEnumerable<T> (the stock extension collides across Grpc.Core.Api and Grpc.Net.Common). - The server exception interceptor now covers client-streaming handlers, and IGrpcEndpointManifest surfaces GrpcRpcStreamKind.ClientStreaming. - GreeterProtoFirstGrpc gains a CollectGreetings RPC (plus the runtime compilation reference the sample lost in the 6.0 Roslyn decoupling), docs updated across the gRPC guide, and CHANGELOG gets its first gRPC entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new core message-bus primitive to support stream-in / single-response request handling (IAsyncEnumerable<TRequest> → Task<TResponse>), and wires proto-first gRPC codegen to use it so client-streaming RPCs (stream TRequest → TResponse) are now generated end-to-end (alongside unary, server-streaming, and bidi).
Changes:
- Introduces
ICommandBus.InvokeStreamAsync<TRequest, TResponse>(IAsyncEnumerable<TRequest>, …)(+DeliveryOptionsoverload) and implements it inMessageBus(local-only dispatch with a clear no-handler failure). - Extends proto-first gRPC wrapper generation to forward client-streaming methods via a new
WolverineGrpcStreamAdapters.ReadAllAsync()bridge (avoidsReadAllAsyncambiguity between Grpc.Core.Api and Grpc.Net.Common). - Updates endpoint manifests, exception interception, tests, samples, docs, and changelog to reflect/support client streaming.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/Wolverine/Transports/Sending/SendingEnvelopeLifecycle.cs | Implements the new InvokeStreamAsync overloads by delegating to the inner bus. |
| src/Wolverine/TestMessageContext.cs | Adds InvokeStreamAsync support to the test bus/context for stubbing and assertions. |
| src/Wolverine/Runtime/MessageBus.cs | Implements InvokeStreamAsync with explicit dispatch on typeof(IAsyncEnumerable<TRequest>) and a local-only guard. |
| src/Wolverine/IMessageBus.cs | Adds the new InvokeStreamAsync overloads to ICommandBus (and thus IMessageBus). |
| src/Wolverine/Configuration/GrpcEndpointManifest.cs | Extends GrpcRpcStreamKind with ClientStreaming and updates descriptor docs. |
| src/Wolverine.Grpc/WolverineGrpcStreamAdapters.cs | Adds Wolverine-owned ReadAllAsync adapter to avoid CS0433 ambiguity in generated code. |
| src/Wolverine.Grpc/WolverineGrpcExceptionInterceptor.cs | Intercepts client-streaming RPCs for exception translation (keeps bidi deferred). |
| src/Wolverine.Grpc/GrpcServiceChain.cs | Enables proto-first client-streaming codegen and emits the new forward frame. |
| src/Wolverine.Grpc/GrpcEndpointManifest.cs | Surfaces proto-first client-streaming endpoints in the gRPC endpoint manifest. |
| src/Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj | Adds the new proto for client-streaming test coverage. |
| src/Wolverine.Grpc.Tests/ProtoFirst/proto_first_grpc_tests.cs | Adds a proto-first client-streaming round-trip test and updates classification assertions. |
| src/Wolverine.Grpc.Tests/GrpcClientStreaming/Protos/grpc_client_stream_test.proto | New client-streaming test contract (stream NumberRequest → SumReply). |
| src/Wolverine.Grpc.Tests/GrpcClientStreaming/grpc_client_streaming_tests.cs | New end-to-end suite validating forwarding + cancellation behavior. |
| src/Wolverine.Grpc.Tests/GrpcClientStreaming/CollectStub.cs | New proto-first stub to trigger Wolverine wrapper generation for client streaming. |
| src/Wolverine.Grpc.Tests/GrpcClientStreaming/CollectHandler.cs | New handler folding an inbound stream into a single response. |
| src/Wolverine.Grpc.Tests/GrpcClientStreaming/ClientStreamingFixture.cs | New in-proc gRPC host fixture for client-streaming tests. |
| src/Wolverine.Grpc.Tests/GrpcBidiStreaming/Protos/grpc_bidi_test.proto | Updates test proto comments now that client-streaming is supported. |
| src/Wolverine.Grpc.Tests/GrpcBidiStreaming/grpc_bidi_streaming_tests.cs | Inverts prior “fail-fast” assertions into “construction succeeds + classified” tests. |
| src/Wolverine.Grpc.Tests/grpc_endpoint_manifest_3265.cs | Adds manifest assertions for proto-first client-streaming descriptors. |
| src/Wolverine.Grpc.Tests/grpc_capabilities_descriptor_source_3267.cs | Extends capabilities coverage to include client-streaming origins. |
| src/Testing/CoreTests/TestMessageContextTests.cs | Adds unit coverage that TestMessageContext records and responds to InvokeStreamAsync. |
| src/Testing/CoreTests/Acceptance/invoke_stream_async_support.cs | New core acceptance suite covering stream invocation semantics and edge cases. |
| src/Samples/GreeterProtoFirstGrpc/Server/Server.csproj | Fixes sample runtime codegen dependency via Wolverine.RuntimeCompilation reference. |
| src/Samples/GreeterProtoFirstGrpc/Server/GreeterHandler.cs | Adds a client-streaming handler folding requests into GreetingSummary. |
| src/Samples/GreeterProtoFirstGrpc/Server/GreeterGrpcService.cs | Updates sample stub docs to include client streaming support. |
| src/Samples/GreeterProtoFirstGrpc/README.md | Documents the new client-streaming sample call and behavior. |
| src/Samples/GreeterProtoFirstGrpc/Messages/Protos/greeter.proto | Adds CollectGreetings (stream HelloRequest) → GreetingSummary. |
| src/Samples/GreeterProtoFirstGrpc/Client/Program.cs | Demonstrates client streaming from the sample client. |
| docs/guide/samples.md | Updates sample listing text to mention server + client streaming. |
| docs/guide/messaging/message-bus.md | Adds “Streaming Requests” documentation for InvokeStreamAsync. |
| docs/guide/grpc/streaming.md | Adds proto-first client-streaming guide section and updates limitations. |
| docs/guide/grpc/samples.md | Updates GreeterProtoFirstGrpc documentation to include client streaming. |
| docs/guide/grpc/multi-tenancy.md | Updates multi-tenancy notes to include proto-first client streaming coverage. |
| docs/guide/grpc/index.md | Updates overview and limitations to reflect client streaming support (proto-first). |
| docs/guide/grpc/handlers.md | Updates Validate caveat to include client-streaming methods. |
| docs/guide/grpc/contracts.md | Updates generated-implementation limitations + adds client-streaming section. |
| CHANGELOG.md | Adds release notes for the new bus primitive and proto-first client-streaming codegen. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Contributor
Author
|
Conform the streaming-request primitive to the existing StreamAsync<T> overload family rather than introducing a new method-name family. Arity disambiguates the two directions (one type argument streams responses out, two stream requests in), and the exception interceptor comment that originally deferred client streaming spoke of "the matching IMessageBus overloads" - this restores that recorded intent. Test file/class/method names follow suit (streaming_request_support). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikshafer
marked this pull request as ready for review
July 18, 2026 17:57
…decoupling Documentation comb-through of every gRPC surface (guide pages, sample READMEs, secondary mentions), verifying each claim against source: - Eight gRPC sample servers crashed at startup with 'No IAssemblyGenerator is registered' since the 6.0 runtime-codegen decoupling; add the Wolverine.RuntimeCompilation ProjectReference (mirroring the fix GreeterProtoFirstGrpc/Server got in JasperFx#3459). All nine servers boot. - samples.md + RacerWithGrpc README still said StreamAsync<TRequest, TResponse> hadn't landed; it has, as the client-streaming primitive, not a bidi loop. Reworded both. - errors.md now states which call shapes the exception interceptor covers (unary, server-, client-streaming; bidi deferred). - contracts.md/handlers.md claimed hand-written delegation wrappers are named {ClassName}GrpcHandler; the GrpcService suffix is stripped first (HandWrittenGrpcServiceChain.ResolveTypeName). - guide/samples.md listed only six of the eight gRPC samples; added GreeterCodeFirstGrpc and ProgressTrackerWithGrpc rows. - handlers.md codegen-preview identifier list matched to the CLI. Wolverine.Grpc.Tests 281/281, full wolverine.slnx Release build clean. Co-Authored-By: Claude Fable 5 <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.
Closes the gap the gRPC work has been carrying since the streaming milestones (#2525, #2565): client streaming (
stream TRequest → TResponse) was classified byGrpcServiceChainand then deliberately fail-fasted — the error text said "no adapter path yet", andWolverineGrpcExceptionInterceptorhad a matching "deferred pending the matchingIMessageBusoverloads" note. This PR is that deferred work: the missing bus overload plus the codegen that consumes it. All four canonical RPC shapes are now generated on the proto-first path.The core primitive
ICommandBusgains the inverse overload ofStreamAsync<T>— same name, per that comment's original "overloads" intent; arity separates the two directions (one type argument streams responses out, two stream requests in):The pleasant surprise: almost no new machinery was needed. Handler discovery already accepts
IAsyncEnumerable<TRequest>as a message type (first-parameter rule, no filter excludes closed generic interfaces), the chain compiles untouched, andMessageFrame's cast is legal. The only thing actually missing was dispatch —message.GetType()on a stream is a compiler-generated iterator type, so the new method keysRuntime.FindInvoker(typeof(IAsyncEnumerable<TRequest>))explicitly and then reusesExecutor.InvokeAsync<T>verbatim (envelope pooling, correlation, tracing, cascading flush all inherited). Handlers consume the live stream incrementally — nothing is materialized. Local invocation only, same contract asStreamAsync; a missing handler throws aNotSupportedExceptionthat spells out the expected handler signature instead of falling into remote routing.I considered the alternative of draining to
TRequest[]inside the generated wrapper and riding the existingInvokeAsync<T>— zero core change, andT[]is already a routable message type via the batching subsystem. Rejected it for semantics: the handler couldn't see item 1 until item N arrived, memory scales with stream length, andHandle(TRequest[])would silently overload the batching subsystem's meaning of that signature.Heads-up on the one breaking edge: two new
ICommandBusmembers, so customIMessageBusimplementors need a recompile-plus-implement (same shape as whenStreamAsynclanded). In-repo that wasTestMessageContextandSendingEnvelopeLifecycle; called out in the CHANGELOG.The gRPC side
AssertNoUnsupportedStreamingKindsis deleted outright — with client streaming generatable there are no unsupported kinds left.ClientStreamingMethodsjoins the classified lists, and the newForwardClientStreamToMessageBusFrameemits a one-liner override:WolverineGrpcStreamAdapters.ReadAllAsyncexists because the stockGrpc.Core.AsyncStreamReaderExtensionsis declared by both Grpc.Core.Api and Grpc.Net.Common — generated code calling it is instantly CS0433 in any app referencing both. Wolverine's own trivial adapter sidesteps the collision.Before/after middleware and the
Validateconvention are not woven into client-streaming methods — same rationale as bidi, no singleTRequestin scope. Tenant detection is covered (it only needs the trailingServerCallContext, which every proto-first shape has).WolverineGrpcExceptionInterceptorgains theClientStreamingServerHandleroverride its own doc comment was deferring — without it, a cancelled client-stream surfaced asUnknowninstead ofCancelled(there's a test pinning that now). Bidi interception stays deferred; its wrapper streams responses incrementally, so trailing translation is a different problem.GrpcRpcStreamKind.ClientStreamingis appended (append-only, in case CritterWatch persists values) and the endpoint manifest surfaces the descriptors:RequestTypeis the per-item element type, response unwrapped fromTask<TResponse>.Tests, sample, docs
GrpcClientStreamingend-to-end suite +invoke_stream_async_supportcore acceptance suite (empty stream, mid-drain fault, cancellation, cascading tuple, no-handler error,DeliveryOptions). The old fail-fast tests are inverted into construction-succeeds assertions. CoreTests 1931/1931 on net9.0 + net10.0, Wolverine.Grpc.Tests 281/281,wolverine.slnxRelease build clean.GreeterProtoFirstGrpcgainsCollectGreetings (stream HelloRequest) → GreetingSummaryacross proto/handler/client/README, verified by running server + client. While doing that I found the sample crashing at startup on 6.0 — it never picked up theWolverineFx.RuntimeCompilationreference during the Roslyn decoupling (the F# samples did). Fixed here for this sample; the other gRPC sample servers likely share the defect and are left for a follow-up sweep.streaminggets a full client-streaming section; the "unsupported" statements inindex/streaming/contractsare gone),message-bus.mdgets a "Streaming Requests" section beside "Streaming Responses", and the CHANGELOG gets its first gRPC entries.Not in scope
The code-first (protobuf-net.Grpc) generated-implementation path still skips
IAsyncEnumerable<TRequest>-parameter methods — and its classifier would call that shape bidi regardless of return type. Documented as a limitation; fixing it is a separate, smaller change if anyone asks for it.Smoke numbers, for flavor (Release, localhost, one connection): ~88k items/sec sustained through a 100k-item stream at a flat ~11 µs/item, and a complete 2-item client stream costs 212 µs vs 203 µs for a bare unary
SayHello— the whole generated-wrapper + bus hop adds single-digit microseconds over the cheapest possible RPC.🤖 Generated with Claude Code