Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@

## Unreleased

### WolverineFx (core)

- **New `IMessageBus.InvokeStreamAsync<TRequest, TResponse>` primitive for streaming requests.**
The mirror image of `StreamAsync<T>`: a caller hands one handler invocation an
`IAsyncEnumerable<TRequest>` stream of messages and awaits a single `TResponse`. The handler
declares `IAsyncEnumerable<TRequest>` as its message type
(`Task<TResponse> Handle(IAsyncEnumerable<TRequest> messages, CancellationToken token)`) and
consumes the stream incrementally — nothing is materialized by the framework. Local invocation
only; a missing handler fails fast with a `NotSupportedException` naming the expected signature.
Cascading messages and `DeliveryOptions` work as with any invoked handler. See the
[message bus guide](https://wolverinefx.net/guide/messaging/message-bus.html#streaming-requests).
Note: this adds two members to `ICommandBus`, which is source-breaking for custom
`IMessageBus`/`ICommandBus` implementors (same precedent as the original `StreamAsync` addition).

### WolverineFx.Grpc

- **Proto-first client-streaming RPCs (`stream TRequest → TResponse`) are now code-generated.**
A `[WolverineGrpcService]` stub declaring the fourth canonical gRPC shape no longer fails fast at
startup — Wolverine generates a wrapper that adapts the inbound `IAsyncStreamReader<TRequest>` to
`IAsyncEnumerable<TRequest>` and forwards it to the new `IMessageBus.InvokeStreamAsync` for a
single response. Tenant-id detection applies to client-streaming methods; before/after middleware
and the `Validate` convention are not woven (same constraint as bidirectional streaming). The
server-side exception interceptor now also translates exceptions from client-streaming handlers
per AIP-193, and `IGrpcEndpointManifest` surfaces the new `GrpcRpcStreamKind.ClientStreaming`
descriptors. The code-first (protobuf-net.Grpc) path is unchanged and still skips this shape. See
the [gRPC streaming guide](https://wolverinefx.net/guide/grpc/streaming.html).

### WolverineFx.Http

- **New `openapi` command for build-time OpenAPI generation without starting the host.**
Expand Down
38 changes: 35 additions & 3 deletions docs/guide/grpc/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ await foreach (var item in greeter.StreamGreetings(new StreamGreetingsRequest {
The [GreeterCodeFirstGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterCodeFirstGrpc)
sample demonstrates this end-to-end. See [Samples](./samples#greetercodefirstgrpc) for a walkthrough.

::: warning Bidirectional streaming is not supported on the generated-implementation path
::: warning Bidirectional and client streaming are not supported on the generated-implementation path
The generated implementation recognises **unary** (`Task<TResponse>`) and **server streaming**
(`IAsyncEnumerable<TResponse>`) method shapes. An interface method with an `IAsyncEnumerable<TRequest>`
*parameter* (bidirectional streaming) is silently skipped — no startup error, but the method will
not be mapped. Use a hand-written service class for bidi RPCs on code-first contracts.
*parameter* (bidirectional or client streaming) is silently skipped — no startup error, but the
method will not be mapped. Use a hand-written service class for bidi or client-streaming RPCs on
code-first contracts. Proto-first stubs code-generate all four shapes, including
[client streaming](./streaming#client-streaming-proto-first).
:::

::: warning No conflict allowed
Expand Down Expand Up @@ -266,6 +268,36 @@ declaration changes. Cancellation from the client propagates into the handler's
in both styles, so mid-stream cancellation cleanly unwinds. For the broader streaming story
(bidirectional, limitations, timing) see [Streaming](./streaming).

## Client streaming (proto-first)

A `stream TRequest → TResponse` RPC is code-generated on the proto-first path only. The handler
receives the whole inbound stream as `IAsyncEnumerable<TRequest>` — the message type Wolverine's
[`IMessageBus.InvokeStreamAsync`](/guide/messaging/message-bus.html#streaming-requests) dispatches
on — and returns the single response:

```proto
service Greeter {
rpc CollectGreetings (stream HelloRequest) returns (GreetingSummary);
}
```

```csharp
public static class GreeterHandler
{
public static async Task<GreetingSummary> Handle(
IAsyncEnumerable<HelloRequest> requests,
CancellationToken cancellationToken)
{
var count = 0;
await foreach (var request in requests.WithCancellation(cancellationToken)) count++;
return new GreetingSummary { Count = count };
}
}
```

See [Streaming — Client streaming](./streaming#client-streaming-proto-first) for the generated
wrapper shape and middleware caveats.

## Mixing both in one host

Nothing stops you from running both styles together. The registration order doesn't matter — call
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/grpc/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ public Task<OrderReply> PlaceOrder(PlaceOrderRequest request, CallContext contex
- `ValidateAsync` returning `Task<Status?>` is also supported when the check is asynchronous.
- Validate is matched **per request type**: a `Validate(PlaceOrderRequest)` does not fire for
RPC methods whose first parameter is a different request type on the same service class.
- Validate is not woven for **bidirectional streaming** methods — there is no single request
instance in scope before the streaming loop begins.
- Validate is not woven for **bidirectional or client streaming** methods — there is no single
request instance in scope before the stream is consumed.
- Validation runs **before** any `[WolverineBefore]` middleware that is not itself a validate hook.

::: tip
Expand Down
17 changes: 10 additions & 7 deletions docs/guide/grpc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ gRPC gives you another edge protocol for the same handlers. Benefits:

- **Strongly-typed contracts** shared across .NET and non-.NET services via `.proto` files, or code-first
contracts that never leave C#.
- **Streaming** first-class — plays naturally with Wolverine's [`IMessageBus.StreamAsync<T>`](/guide/messaging/message-bus.html#streaming-responses).
- **Streaming** first-class — server and bidirectional streaming play naturally with Wolverine's
[`IMessageBus.StreamAsync<T>`](/guide/messaging/message-bus.html#streaming-responses), and client
streaming with [`IMessageBus.InvokeStreamAsync`](/guide/messaging/message-bus.html#streaming-requests).
- **Wolverine handler reuse** — the same handler can back a REST endpoint, an async message, and a
gRPC call without duplication.
- **Canonical error semantics** — ordinary .NET exceptions thrown by a handler are mapped to the
Expand All @@ -32,8 +34,8 @@ building:
can pick (or mix) them.
- [Error Handling](./errors) — the default AIP-193 exception → `StatusCode` table plus the opt-in
`google.rpc.Status` pipeline for rich, structured details.
- [Streaming](./streaming) — server streaming today, bidirectional via a manual bridge, and the
shape of the cancellation contract.
- [Streaming](./streaming) — server, client, and bidirectional streaming, and the shape of the
cancellation contract.
- [Typed gRPC Clients](./client) — `AddWolverineGrpcClient<T>()`, the Wolverine-flavored wrapper over
`Grpc.Net.ClientFactory` that adds envelope-header propagation and `RpcException` → typed-exception
translation on the consuming side.
Expand Down Expand Up @@ -89,7 +91,7 @@ and comparisons to the official `grpc-dotnet` examples.
| [PingPongWithGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/PingPongWithGrpc) | Code-first **unary** | `[ServiceContract]` + `WolverineGrpcServiceBase` forwarding to a plain handler |
| [PingPongWithGrpcStreaming](https://github.com/JasperFx/wolverine/tree/main/src/Samples/PingPongWithGrpcStreaming) | Code-first **server streaming** | Handler returning `IAsyncEnumerable<T>`, forwarded via `Bus.StreamAsync<T>` |
| [GreeterCodeFirstGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterCodeFirstGrpc) | Code-first **generated implementation** | `[WolverineGrpcService]` on an interface — Wolverine generates the service class; no concrete class written |
| [GreeterProtoFirstGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterProtoFirstGrpc) | **Proto-first** unary + server streaming + exception mapping | Abstract `[WolverineGrpcService]` stub subclassing a generated `*Base` + handlers |
| [GreeterProtoFirstGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterProtoFirstGrpc) | **Proto-first** unary + server streaming + client streaming + exception mapping | Abstract `[WolverineGrpcService]` stub subclassing a generated `*Base` + handlers |
| [RacerWithGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/RacerWithGrpc) | Code-first **bidirectional streaming** | Per-update bridge: client `IAsyncEnumerable<TReq>` → `Bus.StreamAsync<TResp>` for each item |
| [GreeterWithGrpcErrors](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterWithGrpcErrors) | Code-first **rich error details** | FluentValidation → `BadRequest` plus inline `MapException` → `PreconditionFailure`, with a client that unpacks both |
| [ProgressTrackerWithGrpc](https://github.com/JasperFx/wolverine/tree/main/src/Samples/ProgressTrackerWithGrpc) | Code-first **server streaming + cancellation** | Realistic job-progress stream: handler yields `JobProgress` updates; client cancels mid-stream |
Expand Down Expand Up @@ -123,9 +125,10 @@ and comparisons to the official `grpc-dotnet` examples.

## Current Limitations

- **Pure client streaming** (`stream TRequest → TResponse`) has no out-of-the-box adapter path yet.
Proto-first stubs that declare this shape fail fast at startup with a clear error rather than
silently skipping. Bidirectional streaming is fully supported — see [Streaming](./streaming).
- **Client streaming is proto-first only.** The code-first (protobuf-net.Grpc)
generated-implementation path does not recognize the `IAsyncEnumerable<TRequest> → Task<TResponse>`
shape — implement those methods by hand against `IMessageBus.InvokeStreamAsync`. All four RPC
shapes are code-generated for proto-first stubs — see [Streaming](./streaming).

## Roadmap

Expand Down
5 changes: 3 additions & 2 deletions docs/guide/grpc/multi-tenancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,6 @@ Core authorization, which will surface as `Unauthenticated`/`PermissionDenied`.
`CallContext` parameter — that's the only route to the underlying `ServerCallContext` and its
request metadata. Methods without one fall back to the runtime interceptor.
- Code-first **server-streaming** methods (returning `IAsyncEnumerable<T>` directly) can't host
the async detection step; proto-first server-streaming and bidirectional methods (returning
`Task`) are fully covered.
the async detection step; proto-first server-streaming, client-streaming, and bidirectional
methods are fully covered — every proto-first RPC shape ends with a `ServerCallContext`
parameter, which is all detection needs.
24 changes: 14 additions & 10 deletions docs/guide/grpc/samples.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ changes is whether your business code knows about gRPC.
| [PingPongWithGrpc](#pingpongwithgrpc) | Unary | Code-first (hand-written) | [Greeter](https://github.com/grpc/grpc-dotnet/tree/master/examples#greeter) |
| [PingPongWithGrpcStreaming](#pingpongwithgrpcstreaming) | Server streaming | Code-first (hand-written) | [Counter](https://github.com/grpc/grpc-dotnet/tree/master/examples#counter) |
| [GreeterCodeFirstGrpc](#greetercodefirstgrpc) | Unary + server streaming | Code-first (generated) | [Coder](https://github.com/grpc/grpc-dotnet/tree/master/examples#coder) |
| [GreeterProtoFirstGrpc](#greeterprotofirstgrpc) | Unary + server streaming + exception mapping | Proto-first | [Greeter](https://github.com/grpc/grpc-dotnet/tree/master/examples#greeter) |
| [GreeterProtoFirstGrpc](#greeterprotofirstgrpc) | Unary + server streaming + client streaming + exception mapping | Proto-first | [Greeter](https://github.com/grpc/grpc-dotnet/tree/master/examples#greeter) |
| [RacerWithGrpc](#racerwithgrpc) | Bidirectional streaming | Code-first (hand-written) | [Racer](https://github.com/grpc/grpc-dotnet/tree/master/examples#racer) |
| [GreeterWithGrpcErrors](#greeterwithgrpcerrors) | Unary + rich error details | Code-first (hand-written) | (no direct equivalent — closest is Greeter + a custom interceptor) |
| [ProgressTrackerWithGrpc](#progresstrackerwithgrpc) | Server streaming + cancellation | Code-first (generated) | [Progressor](https://github.com/grpc/grpc-dotnet/tree/master/examples#progressor) |
Expand Down Expand Up @@ -195,22 +195,26 @@ cancellation story are identical on the wire; only the authoring model differs.
Layout: [`src/Samples/GreeterProtoFirstGrpc/`](https://github.com/JasperFx/wolverine/tree/main/src/Samples/GreeterProtoFirstGrpc)
with `Messages`, `Server`, `Client`.

A proto-first sample that exercises three things at once: unary RPC, server streaming, and the
default exception → `StatusCode` mapping. The `.proto` declares `SayHello` (unary) and
`StreamGreetings` (server streaming); Grpc.Tools generates `Greeter.GreeterBase`; a single line
hands the rest to Wolverine:
A proto-first sample that exercises four things at once: unary RPC, server streaming, client
streaming, and the default exception → `StatusCode` mapping. The `.proto` declares `SayHello`
(unary), `StreamGreetings` (server streaming), and `CollectGreetings` (client streaming);
Grpc.Tools generates `Greeter.GreeterBase`; a single line hands the rest to Wolverine:

```csharp
[WolverineGrpcService]
public abstract class GreeterGrpcService : Greeter.GreeterBase;
```

That's the whole service. Wolverine generates the concrete `GreeterGrpcHandler` wrapper at
startup, and the `Handle` methods on `GreeterHandler` supply the behaviour.

**What to copy**: the abstract-stub pattern for proto-first, plus how throwing
`ArgumentException` / `KeyNotFoundException` / `InvalidOperationException` from a handler yields
the matching gRPC `StatusCode` on the client (see [Error Handling](./errors)).
startup, and the `Handle` methods on `GreeterHandler` supply the behaviour — including the
client-streaming handler, which receives the whole inbound stream as
`IAsyncEnumerable<HelloRequest>` and folds it into one `GreetingSummary` (see
[Streaming — Client streaming](./streaming#client-streaming-proto-first)).

**What to copy**: the abstract-stub pattern for proto-first, the
`IAsyncEnumerable<TRequest> → Task<TResponse>` handler shape for client streaming, plus how
throwing `ArgumentException` / `KeyNotFoundException` / `InvalidOperationException` from a handler
yields the matching gRPC `StatusCode` on the client (see [Error Handling](./errors)).

### Compared to grpc-dotnet's Greeter

Expand Down
Loading
Loading