Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Streaming

Wolverine covers unary, server streaming, and bidirectional streaming natively. Pure client streaming (stream TRequest → TResponse) has no adapter yet and fails fast at startup in proto-first mode with a clear diagnostic error.

This page covers server and bidirectional streaming in depth, plus cancellation and current gaps.

Server streaming (first-class)

Server streaming is the natural fit for Wolverine: handlers return IAsyncEnumerable<T> and Wolverine's IMessageBus.StreamAsync<T> feeds the stream through the gRPC transport without buffering.

Service shim

csharp
// Code-first
public IAsyncEnumerable<PongReply> PingStream(PingStreamRequest request, CallContext context = default)
    => Bus.StreamAsync<PongReply>(request, context.CancellationToken);
csharp
// Proto-first — Wolverine generates this wrapper for you; shown for illustration.
public override async Task StreamGreetings(StreamGreetingsRequest request,
    IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
    await foreach (var reply in _bus.StreamAsync<HelloReply>(request, context.CancellationToken))
    {
        await responseStream.WriteAsync(reply, context.CancellationToken);
    }
}

Handler

The handler is identical for both contract styles — it's just a Wolverine handler that returns IAsyncEnumerable<T> with an [EnumeratorCancellation] token:

csharp
public static async IAsyncEnumerable<PongReply> Handle(
    PingStreamRequest request,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    for (var i = 0; i < request.Count; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        yield return new PongReply { Echo = $"{request.Message}:{i}" };
        await Task.Yield();
    }
}

Each yield return is one gRPC message frame. Back-pressure from a slow client propagates naturally through the IAsyncEnumerable<T> chain — no extra plumbing needed.

Why await Task.Yield() appears in the examples

A streaming handler that only yield returns synchronous values can be compiled by the C# compiler into a synchronous state machine, which blocks the calling thread on each item. await Task.Yield() forces a genuine async yield point and prevents that. You can drop it when your handler already awaits real I/O (a database call, an HTTP request, etc.).

Bidirectional streaming

Wolverine supports bidirectional streaming for both proto-first (generated wrapper) and code-first (manual bridge) contracts. In both cases the handler shape is the same as server streaming: Handle(TRequest) → IAsyncEnumerable<TResponse>. The bidi loop — read one request, stream its responses, repeat — is handled for you.

Proto-first (generated wrapper)

Declare a bidi RPC in your .proto file and mark your stub with [WolverineGrpcService]:

proto
service RacingService {
  rpc Race (stream RacerUpdate) returns (stream RacePosition);
}
csharp
[WolverineGrpcService]
public abstract class RacingServiceStub : RacingService.RacingServiceBase;

Wolverine generates the bridge at startup:

csharp
// Generated by Wolverine
public override async Task Race(
    IAsyncStreamReader<RacerUpdate> requestStream,
    IServerStreamWriter<RacePosition> responseStream,
    ServerCallContext context)
{
    while (await requestStream.MoveNext(context.CancellationToken))
    {
        var request = requestStream.Current;
        await foreach (var item in _bus.StreamAsync<RacePosition>(request, context.CancellationToken))
        {
            await responseStream.WriteAsync(item, context.CancellationToken);
        }
    }
}

The handler shape is identical to server streaming — one request message in, a stream of response messages out:

csharp
public async IAsyncEnumerable<RacePosition> Handle(
    RacerUpdate update,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    foreach (var position in ComputeCurrentStandings(update))
    {
        cancellationToken.ThrowIfCancellationRequested();
        yield return position;
    }
    await Task.Yield();
}

Before-middleware and Validate hooks

Before-frames (including the Validate → Status? short-circuit) are not woven into bidi methods in the generated wrapper. They require a single TRequest instance to be in scope when the method begins, which doesn't exist for a bidi RPC before the first item arrives. Use code-first with a manual shim if you need per-stream authentication or validation before the loop.

Code-first (manual bridge)

Code-first services receive the IAsyncEnumerable<TRequest> directly from protobuf-net.Grpc. The WolverineGrpcServiceBase path lets you write the outer loop by hand — useful when you need control over per-request error handling or logging. The RacerWithGrpc sample uses this pattern:

csharp
[WolverineGrpcService]
public class RacingGrpcService(IMessageBus bus) : IRacingService
{
    public async IAsyncEnumerable<RacePosition> RaceAsync(
        IAsyncEnumerable<RacerUpdate> updates,
        CallContext context = default)
    {
        await foreach (var update in updates.WithCancellation(context.CancellationToken))
        {
            await foreach (var position in bus.StreamAsync<RacePosition>(update, context.CancellationToken))
            {
                yield return position;
            }
        }
    }
}

Per-item correlation

Both the generated and manual patterns work well when requests and responses have a per-item correlation (one command → N response items). For long-lived sessions where any incoming message can affect global response ordering, a saga + outbound messaging stays the better model.

Cancellation

When a client cancels or disconnects, gRPC sets ServerCallContext.CancellationToken / CallContext.CancellationToken. Wolverine's service shims pass that token into Bus.InvokeAsync / Bus.StreamAsync and thread it through to the handler's CancellationToken parameter ([EnumeratorCancellation] on streaming handlers). Any cancellationToken.ThrowIfCancellationRequested() or awaited operation in your handler trips promptly, and the resulting OperationCanceledException maps to StatusCode.Cancelled automatically (see Error Handling).

WARNING

If your handler spawns background work via Task.Run(...) without passing the CancellationToken, that work won't be cancelled when the client disconnects. The gRPC frame stops flowing immediately but your detached tasks keep running. Always thread the token through.

Current limitations

  • Pure client streaming (stream TRequest → TResponse) has no adapter path yet. In proto-first mode, a service whose .proto declares this shape fails fast at startup with a diagnostic error — it's not silently skipped. If you need this today, implement the service method by hand without the Wolverine shim, or reshape the contract to server streaming + a final summary response.
  • Before-middleware and Validate hooks are not woven into bidi methods in the proto-first generated wrapper. Use code-first with a manual shim for per-stream authentication or request-level validation before the loop begins.
  • Back-pressure is cooperative, not flow-controlled by default. HTTP/2 provides windowing, but if your handler produces faster than your client consumes and your DTOs are large, memory usage can spike before backpressure propagates. For large payloads, consider chunking at the contract level (smaller messages) rather than relying on transport-level flow control alone.
  • Exception timing: an exception thrown before the first yield return surfaces on the client via the trailers as expected. An exception thrown mid-stream surfaces as a trailer after messages the client has already received — well-behaved clients must still check the final status even after consuming messages successfully. Server-side, the OpenTelemetry activity for the handler is marked Error in both cases (including cancellation) — the activity stays open until the stream fully drains or faults, so dashboards reflect the real terminal state rather than the moment the handler returned the IAsyncEnumerable<T>.
  • Handlers — where the CancellationToken comes from and how the service shim forwards.
  • Error Handling — how OperationCanceledException becomes StatusCode.Cancelled and how to attach rich details to errors that terminate a stream.
  • SamplesPingPongWithGrpcStreaming, ProgressTrackerWithGrpc, and RacerWithGrpc are the canonical streaming walkthroughs, covering server streaming (hand-written), server streaming (generated + cancellation), and bidirectional streaming respectively.

Released under the MIT License.