Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6f61dd8
Implement M15 Phase 1: weave MiddlewareScoping.Grpc middleware into p…
erikshafer Apr 22, 2026
f069bf9
Add user-configurable server-side gRPC exception mapping (P2)
erikshafer Apr 22, 2026
82f3edc
feat(grpc): Validate convention → Status? short-circuit for proto-fir…
erikshafer Apr 22, 2026
ca7a74f
feat(grpc): first-class bidirectional streaming for proto-first stubs
erikshafer Apr 22, 2026
eedbc24
feat(grpc): add code-first gRPC support via interface implementation …
erikshafer Apr 22, 2026
daea655
feat(grpc): implement discovery and mapping for code-first gRPC services
erikshafer Apr 22, 2026
5758981
feat(grpc): add code-first samples for unary and server-streaming
erikshafer Apr 22, 2026
bba76a8
docs(grpc): document zero-boilerplate code-first services and new sam…
erikshafer Apr 22, 2026
5eb8c84
refactor(grpc): rename RacingService to RacingGrpcService in samples
erikshafer Apr 22, 2026
28e70dd
docs(grpc): standardize terminology for code-first generated implemen…
erikshafer Apr 22, 2026
c5a699e
feat(grpc): add hand-written code-first service delegation with middl…
erikshafer Apr 22, 2026
1b2448e
Apply middleware and IChainPolicy to discovered gRPC chains
erikshafer Apr 22, 2026
d274c50
feat(grpc): generalize AddMiddleware to accept all Wolverine gRPC cha…
erikshafer Apr 22, 2026
e5b4aae
feat(grpc): add IGrpcChainPolicy for structural chain customizations …
erikshafer Apr 22, 2026
c93201c
docs(grpc): document middleware and policy system for gRPC chains
erikshafer Apr 22, 2026
6bc2dda
feat(grpc): enable middleware weaving for code-first and hand-written…
erikshafer Apr 22, 2026
d22876c
feat(grpc): serialize tests that set DynamicCodeBuilder flags to prev…
erikshafer Apr 22, 2026
eac1193
Use MessageBus() extension method in streaming handler tests
erikshafer Apr 22, 2026
4e65802
Simplify standings computation and improve client output formatting i…
erikshafer Apr 22, 2026
51493c8
Polish streaming and contracts documentation with clearer wording, ca…
erikshafer Apr 22, 2026
d1795c1
Add discovered before/after middleware weaving for code-first gRPC ch…
erikshafer Apr 23, 2026
8bcde1c
Move middleware discovery from service contract to handler types for …
erikshafer Apr 23, 2026
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
108 changes: 105 additions & 3 deletions docs/guide/grpc/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,101 @@ scroll between them and compare.
A common pragmatic split: **code-first for internal service-to-service calls** where both ends
always deploy together, **proto-first for anything the outside world consumes**.

## Code-first codegen (generated implementation)

Applying `[WolverineGrpcService]` to the **interface** itself alongside `[ServiceContract]` lets
Wolverine generate the concrete implementation for you. Wolverine discovers the interface at startup,
emits `{ServiceName}GrpcHandler` that injects `IMessageBus`, and maps it — no service class to write
or maintain.

```csharp
[ServiceContract]
[WolverineGrpcService]
public interface IGreeterCodeFirstService
{
Task<GreetReply> Greet(GreetRequest request, CallContext context = default);
IAsyncEnumerable<GreetReply> StreamGreetings(StreamGreetingsRequest request, CallContext context = default);
}

// That's the whole contract. No service class needed.

// Ordinary Wolverine handlers — no gRPC coupling
public static class GreeterHandler
{
public static GreetReply Handle(GreetRequest request)
=> new() { Message = $"Hello, {request.Name}!" };

public static async IAsyncEnumerable<GreetReply> Handle(
StreamGreetingsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (var i = 1; i <= request.Count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return new GreetReply { Message = $"Hello, {request.Name} [{i} of {request.Count}]" };
await Task.Yield();
}
}
}
```

Bootstrap is the same as the hand-written path, with one addition: if the annotated interface lives
in a separate assembly from the server (e.g. a shared `Messages` project), tell Wolverine to scan
that assembly so `GrpcGraph` can find it:

```csharp
builder.Host.UseWolverine(opts =>
{
opts.ApplicationAssembly = typeof(Program).Assembly;
opts.Discovery.IncludeAssembly(typeof(IGreeterCodeFirstService).Assembly);
});

builder.Services.AddCodeFirstGrpc();
builder.Services.AddWolverineGrpc();

// ...
app.MapWolverineGrpcServices();
```

The generated class name follows the convention `{InterfaceNameWithoutLeadingI}GrpcHandler` — so
`IGreeterCodeFirstService` → `GreeterCodeFirstServiceGrpcHandler`. The generated type implements the
contract interface and is the class that protobuf-net.Grpc maps as the service endpoint.

On the client side, use the same interface with `channel.CreateGrpcService<T>()` — no stubs, no
`.proto` file, no code-gen step:

```csharp
using var channel = GrpcChannel.ForAddress("https://greeter.example");
var greeter = channel.CreateGrpcService<IGreeterCodeFirstService>();

var reply = await greeter.Greet(new GreetRequest { Name = "Erik" });
await foreach (var item in greeter.StreamGreetings(new StreamGreetingsRequest { Name = "Erik", Count = 5 }))
Console.WriteLine(item.Message);
```

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
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.
:::

::: warning No conflict allowed
`[WolverineGrpcService]` must appear on **either** the interface **or** a concrete implementing
class — not both. If Wolverine finds the attribute on both, it throws `InvalidOperationException`
at startup with a diagnostic identifying the conflict. This mirrors the proto-first rule that the
stub must be abstract.
:::

## Unary RPC

### Code-first
### Code-first (hand-written service class)

When you prefer explicit control over the service class — for example to add per-method logging or
to call multiple downstream services from one RPC — write the class yourself:

```csharp
[ServiceContract]
Expand All @@ -55,6 +147,14 @@ public static class PingHandler
Any class whose name ends in `GrpcService` is picked up by `MapWolverineGrpcServices()`. If the
suffix convention doesn't fit, apply `[WolverineGrpcService]` instead.

Wolverine generates a thin **delegation wrapper** around the class at startup (named
`{ClassName}GrpcHandler`). The wrapper implements the same `[ServiceContract]` interface, weaves
any `Validate` / `[WolverineBefore]` middleware defined on the service class, then calls into the
inner class — which Wolverine resolves from the DI container or constructs via
`ActivatorUtilities` if no explicit registration exists. This gives hand-written service classes
the same middleware and validation hooks available to the proto-first and generated-implementation
paths.

### Proto-first

```proto
Expand Down Expand Up @@ -88,9 +188,11 @@ type. Proto-first handlers must live on separate classes, not on the stub itself
## Server streaming

Same handler shape on both sides — `IAsyncEnumerable<T>` plus `[EnumeratorCancellation]`. The
difference is purely in how the contract is declared.
difference is purely in how the contract is declared. For the generated-implementation path, the
interface method simply returns `IAsyncEnumerable<T>` — see the [Code-first codegen](#code-first-codegen-generated-implementation)
section above.

### Code-first
### Code-first (hand-written service class)

Return `IAsyncEnumerable<T>` from the contract method. protobuf-net.Grpc recognises the return type
as a server-streaming RPC and wires the transport for you.
Expand Down
28 changes: 27 additions & 1 deletion docs/guide/grpc/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ through to the default table.

`WolverineGrpcExceptionInterceptor` is registered automatically by `AddWolverineGrpc` and applies to
both code-first and proto-first services. It translates ordinary .NET exceptions thrown by handlers
into `RpcException` with the canonical status code from the table below:
into `RpcException` with the canonical status code from the table below. See
[Overriding the default table](#overriding-the-default-table) if the defaults don't match your domain model.

| Exception | gRPC Status Code |
|-------------------------------|-------------------------|
Expand Down Expand Up @@ -48,6 +49,31 @@ public static OrderReply Handle(GetOrder request, IOrderStore store)
Throwing `RpcException` directly remains the escape hatch for status codes or trailers not in
either table.

## Overriding the default table

Call `MapException<TException>(StatusCode)` on `WolverineGrpcOptions` inside `AddWolverineGrpc` to
register application-specific overrides. User-registered mappings are checked after the opt-in
`google.rpc.Status` rich-error pipeline and before the built-in AIP-193 table, so they always win
over the defaults:

```csharp
builder.Services.AddWolverineGrpc(opts =>
{
// Map a custom domain exception that the default table doesn't know about
opts.MapException<OrderNotFoundException>(StatusCode.NotFound);

// Override a default: treat TimeoutException as ResourceExhausted instead of DeadlineExceeded
opts.MapException<TimeoutException>(StatusCode.ResourceExhausted);
});
```

Inheritance is respected — a mapping for a base class also matches derived types unless a more
specific mapping is registered. When multiple registrations target the same exception type, the
last one wins.

For structured, field-level error payloads (validation failures, `google.rpc.Status` details), see
the [Rich error details](#rich-error-details-opt-in) section below.

## Rich error details (opt-in)

The default mapping table produces a single `StatusCode` and a message string — enough for
Expand Down
177 changes: 157 additions & 20 deletions docs/guide/grpc/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,33 @@ itself doesn't know which of the three invoked it.
- **Middleware applies identically.** `UseFluentValidation()`, open/generic middleware, saga
middleware, etc., all run on the gRPC path because Wolverine is invoking the handler through the
same pipeline it always does.
- **The transport concerns stay at the edge.** You won't see `RpcException` inside a handler, and
you shouldn't throw one there either — throw the domain exception and let the interceptor
translate it.
- **The transport concerns stay at the edge.** You won't see `RpcException` inside a handler.

::: tip Throw domain exceptions, not RpcException
Throw ordinary .NET exceptions from handlers — `ArgumentException`, `KeyNotFoundException`, your
own domain types. The interceptor translates them to the right `StatusCode` automatically.
Throwing `RpcException` directly from a handler does work (it passes through unchanged), but it
couples the handler to the gRPC transport and prevents it from being reused over messaging or HTTP.
:::

## Discovery and codegen

`MapWolverineGrpcServices()` does two passes when the host starts:
`MapWolverineGrpcServices()` does four passes when the host starts:

1. **Code-first**: any class whose name ends in `GrpcService` (or that carries
`[WolverineGrpcService]`) is picked up and mapped via protobuf-net.Grpc's routing.
2. **Proto-first**: any abstract class carrying `[WolverineGrpcService]` and subclassing a
1. **Code-first (hand-written with wrapper)**: any concrete class whose name ends in `GrpcService` (or
that carries `[WolverineGrpcService]`) and implements a `[ServiceContract]` interface gets a
generated **delegation wrapper**. Wolverine emits `{ClassName}GrpcHandler` that implements the same
contract interface, weaves any `Validate` / `[WolverineBefore]` middleware, then delegates each call
to the inner class via `ActivatorUtilities`. The inner class does not need an explicit DI registration.
2. **Code-first (generated implementation)**: any **interface** carrying both `[WolverineGrpcService]`
and `[ServiceContract]` triggers code generation. Wolverine emits a concrete
`{InterfaceNameWithoutLeadingI}GrpcHandler` that implements the interface, injects `IMessageBus`,
and forwards each RPC to `InvokeAsync<T>` or `StreamAsync<T>`. No service class is written by hand.
3. **Proto-first**: any abstract class carrying `[WolverineGrpcService]` and subclassing a
generated `{Service}Base` triggers codegen. Wolverine emits a concrete
`{ProtoServiceName}GrpcHandler` that overrides each RPC and forwards to `IMessageBus`.
4. **Direct mapping**: any remaining concrete `GrpcService`-named class that wasn't claimed by a
delegation wrapper in pass 1 is mapped directly via protobuf-net.Grpc's routing.

Both paths feed into the same generated-code pipeline used by Wolverine's messaging and HTTP
adapters, so your gRPC services show up in the standard diagnostics:
Expand All @@ -92,22 +106,145 @@ shows the exact generated override and the handler method each RPC forwards to.
[`codegen-preview`](/guide/command-line#codegen-preview) for the full set of accepted identifiers
(bare proto service name, stub class name, or short `-g` alias).

## Observability
## Validate convention

Proto-first stubs and hand-written code-first service classes support a lightweight
**pre-handler validation** hook. Add a static `Validate` (or `ValidateAsync`) method to your
class that accepts the request message and returns `Status?`. Wolverine weaves this into the
generated wrapper: if `Validate` returns a non-null `Status`, the call is rejected with an
`RpcException` **before** the handler (or inner service) runs.

```csharp
// Works on both proto-first stubs and hand-written code-first classes.
public class OrderGrpcService : IOrderService
{
// Return null → delegation continues normally.
// Return a Status → RpcException is thrown; inner service is never invoked.
public static Status? Validate(PlaceOrderRequest request)
{
if (string.IsNullOrWhiteSpace(request.CustomerId))
return new Status(StatusCode.InvalidArgument, "CustomerId is required");

return null;
}

public Task<OrderReply> PlaceOrder(PlaceOrderRequest request, CallContext context = default)
=> Bus.InvokeAsync<OrderReply>(request, context.CancellationToken);
}
```

The generated wrapper looks roughly like this:

```csharp
// Generated by Wolverine at startup
public Task<OrderReply> PlaceOrder(PlaceOrderRequest request, CallContext context = default)
{
var status = OrderGrpcService.Validate(request);
if (status.HasValue)
throw new RpcException(status.Value);

var inner = ActivatorUtilities.GetServiceOrCreateInstance<OrderGrpcService>(_serviceProvider);
return inner.PlaceOrder(request, context);
}
```

### Rules

- The method must be **static** and live directly on the service class (or proto-first stub).
- The return type must be `Grpc.Core.Status?` (nullable). A non-nullable `Status` return type is not
recognised as the validate hook.
- `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.
- Validation runs **before** any `[WolverineBefore]` middleware that is not itself a validate hook.

::: tip
For `InvalidArgument` rejections that carry field-level detail, consider returning a
`google.rpc.BadRequest` via a rich-status provider instead of (or in addition to) the `Validate`
hook — that way the client can surface per-field errors. See [Error Handling](./errors).
:::

## Middleware and Policies

Wolverine's gRPC adapter preserves `Activity.Current` across the boundary between the ASP.NET Core
gRPC pipeline and the Wolverine handler pipeline. Concretely:
There are three ways to attach cross-cutting behaviour to Wolverine gRPC chains — choose based on
how targeted or structural the concern is.

- ASP.NET Core's hosting diagnostics starts an inbound activity (`Microsoft.AspNetCore.Hosting.HttpRequestIn`)
and extracts any W3C `traceparent` header the client sent.
- The gRPC service method (code-first or proto-first generated wrapper) invokes
`IMessageBus.InvokeAsync` / `IMessageBus.StreamAsync` on the same ExecutionContext, so
`Activity.Current` is still the hosting activity when Wolverine starts its own handler span.
- Wolverine's `WolverineTracing.StartExecuting` inherits `Activity.Current` as parent, which means
every handler activity lives under the same TraceId as the inbound gRPC request.
### Inline methods on the service class (per-service)

The lightest-weight option. Add a static `Validate`, `Before`, or `After` method directly to your
proto-first stub or hand-written service class. Wolverine weaves it into the generated wrapper for
that one service automatically — no registration required. See [Validate convention](#validate-convention)
for the full rules and an example.

### `opts.AddMiddleware<T>()` (gRPC-scoped, all chains)

For middleware that should apply across multiple gRPC services but not leak into Wolverine's
messaging handler pipeline, register it via `AddWolverineGrpc`:

```csharp
builder.Services.AddWolverineGrpc(grpc =>
{
// Applied to every gRPC chain (proto-first, code-first generated, hand-written) at codegen time.
grpc.AddMiddleware<GrpcAuthMiddleware>();

// Narrow to a single chain kind with the optional filter:
grpc.AddMiddleware<OrderValidationMiddleware>(
c => c is GrpcServiceChain g && g.ProtoServiceName == "Orders");
});
```

The middleware class follows the same `Before` / `After` / `Finally` method-name conventions
as Wolverine's HTTP and messaging middleware. Wolverine weaves the calls into the generated service
wrapper at startup — no runtime overhead after boot.

::: tip
`opts.Policies.AddMiddleware<T>()` (the global Wolverine path) intentionally does **not** reach
gRPC chains — its filter is `HandlerChain`-only. Use `grpc.AddMiddleware<T>()` inside
`AddWolverineGrpc(...)` for gRPC-targeted middleware.
:::

### `opts.AddPolicy<T>()` / `IGrpcChainPolicy` (structural customization)

For changes that go beyond middleware weaving — inspecting service names, overriding idempotency
styles, or conditionally modifying chain configuration — implement `IGrpcChainPolicy` and register
it via `AddPolicy`:

```csharp
public class IdempotentOrdersPolicy : IGrpcChainPolicy
{
public void Apply(
IReadOnlyList<GrpcServiceChain> protoFirstChains,
IReadOnlyList<CodeFirstGrpcServiceChain> codeFirstChains,
IReadOnlyList<HandWrittenGrpcServiceChain> handWrittenChains,
GenerationRules rules,
IServiceContainer container)
{
foreach (var chain in protoFirstChains.Where(c => c.ProtoServiceName == "Orders"))
chain.Idempotency = IdempotencyStyle.GetOrPost;
}
}

builder.Services.AddWolverineGrpc(grpc =>
{
grpc.AddPolicy<IdempotentOrdersPolicy>();
// or directly: grpc.AddPolicy(new IdempotentOrdersPolicy());
});
```

`IGrpcChainPolicy.Apply` receives all three chain kinds as typed lists, so policy implementations
get full access to gRPC-specific properties (`ProtoServiceName`, `ServiceContractType`, etc.)
without casting. It is called after `AddMiddleware<T>()` weaving, during the same bootstrapping
pass as handler and HTTP chain policies.

## Observability

The result: a single trace covers the full request, from inbound gRPC to every Wolverine handler it
invokes, with no additional wiring. If you expose an OpenTelemetry pipeline for other Wolverine
transports, it will pick up gRPC traffic for free.
The service shim invokes `IMessageBus` on the same `ExecutionContext` as the inbound gRPC request,
so `Activity.Current` propagates naturally — every Wolverine handler span becomes a child of the
ASP.NET Core hosting span. A single trace covers the full call with no extra wiring. If you already
export an OpenTelemetry pipeline for Wolverine messaging handlers, it picks up gRPC traffic
automatically.

### Registering OpenTelemetry

Expand Down
Loading
Loading