Skip to content

gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming, Code-First Codegen, New Samples#2565

Open
erikshafer wants to merge 22 commits intoJasperFx:mainfrom
erikshafer:feature/grpc-middleware-weaving-validate-bidi-streaming
Open

gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming, Code-First Codegen, New Samples#2565
erikshafer wants to merge 22 commits intoJasperFx:mainfrom
erikshafer:feature/grpc-middleware-weaving-validate-bidi-streaming

Conversation

@erikshafer
Copy link
Copy Markdown
Contributor

@erikshafer erikshafer commented Apr 22, 2026

PR Summary — gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming, Code-First Codegen, New Samples

Branch: feature/grpc-middleware-weaving-validate-bidi-streaming
Base: main (post-#2525)


What this PR does

This is a follow-up to #2525 that closes gaps in the Wolverine gRPC adapter, adds two new
end-to-end samples, and ships the first portion of the code-first codegen path (M9):

  1. Middleware weaving for proto-first services[WolverineBefore] / [WolverineAfter] methods on a proto-first stub are now actually woven into the generated wrapper. This was the P0 correctness item from the post-gRPC Support for Wolverine HTTP Endpoints + IMessageBus.StreamAsync<T> #2525 roadmap: GrpcServiceChain.Scoping returned MiddlewareScoping.Grpc and the discovery infrastructure correctly filtered methods by scope, but AssembleTypes never emitted the frames. A user who wrote a [WolverineBefore(MiddlewareScoping.Grpc)] method got silence rather than execution.

  2. User-configurable server-side exception mappingWolverineGrpcOptions gains MapException<T>(StatusCode) and MapException<T>(Func<T, StatusCode>), letting callers override the default AIP-193 table for specific exception types without swapping out the interceptor. The fluent API matches what Wolverine uses elsewhere (cf. WolverineOptions.Policies).

  3. Validate convention (Status? short-circuit) for all three chain kinds — a static Validate(TRequest) → Status? (or ValidateAsync returning Task<Status?>) is woven into the generated wrapper before the IMessageBus.InvokeAsync call. A non-null return throws RpcException immediately; the Wolverine handler never runs. This now covers proto-first stubs, hand-written service classes, and code-first generated services. For code-first chains the Validate method lives on the Wolverine handler class (the class with Handle/HandleAsync) — the same idiom as the rest of the framework. Discovery uses assembly scanning rather than HandlerGraph because AssembleTypes fires before HandlerGraph.Compile() runs at startup.

  4. First-class bidirectional streaming for proto-first stubs — a .proto bidi RPC (stream TRequest → stream TResponse) is now code-generated rather than failing fast at startup. No new IMessageBus overload was needed; the generated wrapper uses the same per-item bridge that RacerWithGrpc already demonstrates manually in code-first.

  5. Middleware weaving for code-first and hand-written chainsAddWolverineGrpc(grpc => grpc.AddMiddleware<T>()) now correctly applies to all three chain kinds (GrpcServiceChain, CodeFirstGrpcServiceChain, HandWrittenGrpcServiceChain). The filter predicate is also broadened so it accepts any IGeneratedType rather than only HandlerChain, closing the gap where middleware registered via the gRPC-scoped path was silently dropped for non-proto-first chains.

  6. IGrpcChainPolicy for structural chain customization — a new interface alongside AddMiddleware for concerns that go beyond frame weaving: inspecting service names, overriding idempotency style, or conditionally modifying chain configuration. Apply receives all three chain kinds as typed lists, so policy implementations get full access to gRPC-specific properties without casting.

  7. Code-first codegen — zero-boilerplate interface path (M9) — applying [WolverineGrpcService] to a [ServiceContract] interface now triggers Wolverine to generate a concrete service implementation at startup. No service class is written by hand. The generated class ({InterfaceNameWithoutLeadingI}GrpcHandler) injects IMessageBus, implements the interface, and forwards each RPC to InvokeAsync<T> or StreamAsync<T> with the caller's CancellationToken extracted from CallContext via IVariableSource. This is the M9 sprint item previously listed as deferred.


Design decisions worth calling out

Middleware discovery on GrpcServiceChain

The middleware filter is MiddlewarePolicy.FilterMethods<WolverineBeforeAttribute> applied to StubType.GetMethods() with scope guard MiddlewareScoping.Grpc. Methods are sorted alphabetically before frame emission so generated source is byte-stable across JIT runs (reflection order is not guaranteed). This mirrors how HandlerChain handles middleware discovery.

Before-frames that return Status? are detected after the MethodCall is built (by inspecting call.Creates) and automatically followed by a GrpcValidateShortCircuitFrame. This is how the Validate convention composes on top of the general middleware path — it's not special-cased at discovery time.

Validate convention scoping

The hook covers all three chain kinds. The constraint — static method, Status? return, lives on the handler class — follows the same shape as the Validate convention elsewhere in the framework.

For code-first chains, the method is discovered via assembly scanning: CodeFirstGrpcServiceChain.HandlerTypesFor(requestType) walks the application assemblies looking for classes that own a Handle/HandleAsync/Consume/ConsumeAsync method whose first parameter is the RPC request type. Those handler types are then scanned for [WolverineBefore]-eligible methods (via MiddlewarePolicy.FilterMethods) in the same pass. Assembly scanning is used instead of HandlerGraph because AssembleTypes is called from the ASP.NET Core middleware pipeline, which runs before WolverineRuntime.StartAsync() compiles handler chains — reaching into HandlerGraph at that point would always return null.

Bidi streaming approach

The original roadmap (.plans/next-pr-roadmap.md §PR-C) called for a new IMessageBus.StreamAsync<TRequest, TResponse> overload as the prerequisite for first-class bidi. This PR takes a different path: the generated wrapper uses IAsyncStreamReader<T>.MoveNext(ct) + Current to loop the inbound stream and calls the existing Bus.StreamAsync<TResponse>(request, ct) per item. The handler shape is identical to server streaming — Handle(TRequest) → IAsyncEnumerable<TResponse> — so no new handler convention is required and the existing RaceStreamHandler pattern from RacerWithGrpc translates directly.

The trade-off: this approach has per-item correlation semantics (one request → N responses, interleaved in arrival order). It is the wrong fit for a long-lived session where any inbound message can reorder or affect all pending responses. For that pattern, a saga + outbound messaging remains the right model. This is documented in streaming.md.

Before-frames are not woven into bidi methods. The Validate short-circuit and any [WolverineBefore] methods require a concrete TRequest instance to be in scope when the generated method begins — which is not true for a bidi RPC whose first act is opening the IAsyncStreamReader. This is called out in the generated wrapper's XML doc, in the streaming docs, and in the Current Limitations list. Code-first services can still add per-stream validation via a manual shim.

Pure client streaming (stream TRequest → TResponse) is still not supported and still fails fast at startup with a clear error. Only BidirectionalStreaming has been promoted; ClientStreaming remains in the unsupported list.

WolverineGrpcOptions.MapException<T>

Two overloads are exposed: a StatusCode constant overload for the common case and a Func<T, StatusCode> overload for when the mapping depends on the exception's properties (e.g. map a NotFoundException to NotFound vs. Internal based on ex.IsTransient). Both register into a Dictionary<Type, Func<Exception, StatusCode>> that the interceptor checks before falling back to the static AIP-193 table. Exact-type match only — no inheritance walk — which keeps the lookup O(1) and avoids surprising inheritance-based shadowing.

Code-first codegen (M9) implementation

CodeFirstGrpcServiceChain implements ICodeFile and is registered alongside the existing proto-first chains in GrpcGraph.BuildFiles(). Key design points:

  • IVariableSource for CancellationToken — rather than hardcoding context.CancellationToken as a string in each generated frame, a CallContextCancellationTokenSource is registered on each GeneratedMethod. When any frame calls chain.FindVariable(typeof(CancellationToken)), it resolves to context.CancellationToken via MemberAccessVariable. This is idiomatic JasperFx codegen rather than string interpolation.
  • GeneratedAssembly.AddType(name, interface) — when the second argument is an interface, JasperFx's GeneratedType.Implements() is called automatically, producing a class declaration that implements the interface and creates GeneratedMethod entries for each interface method. This was confirmed as a first-class API in JasperFx before committing to the approach.
  • Method classificationCodeFirstGrpcServiceChain.DiscoverMethods inspects each interface method's return type: Task<T>Unary, IAsyncEnumerable<T>ServerStreaming. Methods not matching either shape are skipped with no error; this is intentional permissiveness so interfaces with non-RPC utility methods don't break discovery.
  • Type namingResolveTypeName strips a leading I only when followed by an uppercase letter (the conventional C# interface prefix). ImaginaryServiceContract keeps its I; ICodeFirstTestService becomes CodeFirstTestServiceGrpcHandler.
  • Conflict guardAssertNoConcreteImplementationConflicts throws InvalidOperationException at startup if a class implementing the interface also carries [WolverineGrpcService]. This prevents silent double-registration.
  • Assembly scanningGrpcGraph.FindCodeFirstServiceContracts requires both [WolverineGrpcService] AND [ServiceContract] on the interface. Interfaces with only one attribute are not picked up. When the annotated interface lives in a shared contracts assembly, callers add opts.Discovery.IncludeAssembly(typeof(IMyService).Assembly) to include it in the Wolverine scan.

New samples

Two new end-to-end samples join the existing six, both exercising the new code-first codegen path:

GreeterCodeFirstGrpc (port 5008)

The canonical M9 showcase. The server project contains only handlers — no concrete service class.
IGreeterCodeFirstService (in Messages) carries both [ServiceContract] and [WolverineGrpcService];
MapWolverineGrpcServices() discovers it and maps the generated GreeterCodeFirstServiceGrpcHandler.
The client uses channel.CreateGrpcService<IGreeterCodeFirstService>() — same interface, both sides.
Exercises unary (Greet) and server-streaming (StreamGreetings).

Closest grpc-dotnet equivalent: Coder — but the official example requires a hand-written concrete service class; this sample writes none.

ProgressTrackerWithGrpc (port 5009)

A realistic server-streaming sample also built on the zero-boilerplate codegen path. The client
submits a RunJobRequest; the handler yields JobProgress updates as it simulates work with
Task.Delay. The client demonstrates both the happy path (all steps complete) and mid-stream
cancellation: cancel after step 3 → RpcException(Cancelled) → clean "Job cancelled" message.

Closest grpc-dotnet equivalent: Progressor — but the handler has no knowledge of IServerStreamWriter<T> or any gRPC type; it's a plain IAsyncEnumerable<T> method.


Other changes

  • RacerWithGrpc sample cleanupRaceStreamHandler had a private LogStandings method (12 lines of console formatting) and a ComputeStandings LINQ helper that obscured the bidi pattern. Both removed; standings computation is now inlined. RacerClient/Program.cs groups output by snapshot using a divider on position.Position == 1.
  • RacerWithGrpc/RacerServer/RacingService.csRacingGrpcService.cs — filename now matches the class name.
  • WolverineGrpcServiceAttributeAttributeTargets expanded from Class only to Class | Interface to support the new M9 interface-annotation path.

What is NOT in this PR

  • WolverineGrpcServiceBase codegen parity — the zero-boilerplate interface path is now shipped, but hand-written WolverineGrpcServiceBase services still resolve dependencies via service location. Generating per-method code files for hand-written service classes (so they benefit from Wolverine middleware and tighter DI) remains deferred.
  • IMessageBus.StreamAsync<TRequest, TResponse> — not needed for the bidi approach taken here; remains a candidate for a later PR if a non-per-item-correlated bidi use case emerges.
  • Hybrid handler shape (HTTP + gRPC + messaging) — parked.
  • Client streaming — no adapter path, still fails fast.
  • DiscoveredBefores/DiscoveredAfters from [WolverineBefore] interface-method attributes — the assembly scan discovers handler types by matching Handle/HandleAsync method signatures, then filters those types' methods for middleware hooks. Methods on the interface itself are not scanned for attributes (JasperFx pre-creates stubs for all interface methods, so static methods on the interface would produce 0-frame stubs that fail at codegen). This is intentional: the idiom is hooks on the handler, not on the contract.

Tests

New test classes for M9 (code-first codegen):

  • code_first_codegen_integration_tests (9 tests) — round-trip unary, round-trip server streaming, cancellation propagation through unary, mid-stream cancellation, generated type name follows interface name convention, generated type implements the service contract interface, both RPC methods classified correctly, validate short-circuit throws RpcException before handler runs, validate returning null allows request through to handler.
  • code_first_codegen_discovery_tests (8 tests) — finds annotated interfaces, returns only interfaces (not concrete classes), ResolveTypeName strips leading I from conventional interface names, does not strip I when not followed by uppercase, conflict detection throws when concrete impl also has [WolverineGrpcService], Scoping property returns MiddlewareScoping.Grpc, ApplicationAssemblies is null before discovery (confirms no-throw when chain is constructed directly), Validate method on handler class has the expected static shape.

New test classes for coverage hardening (post-review):

  • wolverine_grpc_options_add_policy_tests (4 tests) — AddPolicy<T>() registers an instance, AddPolicy(instance) overload, returns this for fluent chaining, multiple policies accumulate in registration order.
  • igpc_chain_policy_discover_services_tests (2 tests) — GrpcGraph.DiscoverServices calls every registered IGrpcChainPolicy; the policy Apply receives all three chain-type lists populated with the discovered chains.
  • add_middleware_custom_filter_tests (2 tests) — AddMiddleware<T>(filter: _ => false) leaves every chain's Middleware empty; a proto-first-only filter attaches to GrpcServiceChain instances but not to code-first or hand-written chains.
  • grpc_client_streaming_fail_fast_tests (1 test) — constructing a GrpcServiceChain from a stub that declares a client-streaming RPC throws NotSupportedException at startup with an actionable message naming the offending method.

Pre-existing test classes (unchanged):

  • middleware_weaving_execution_tests — integration tests asserting that [WolverineBefore(MiddlewareScoping.Grpc)] methods actually execute during a live gRPC call, that MiddlewareScoping.Http-only methods do not, and that multiple ordered before-frames run in alphabetical order.
  • user_exception_mapping_testsMapException<T>(StatusCode) and MapException<T>(Func<>) override integration tests; fallback behavior when no override is registered.
  • grpc_validate_convention_tests — valid requests pass through, empty/blank/forbidden names short-circuit with the right StatusCode, handler does not run when validate rejects.
  • grpc_bidi_streaming_tests — single request yields N replies, multiple requests produce interleaved replies in order, zero requests produce zero replies, chain correctly classifies the bidi method.
  • hand_written_grpc_chain_tests — delegation wrapper is generated, middleware is woven, inner service is resolved via ActivatorUtilities, conflict guard throws on double-annotation, Scoping property returns MiddlewareScoping.Grpc (1 test added to hand_written_chain_discovery_tests).
  • scope_discovery_tests / policy_leak_tests / type_name_disambiguation_testsAddMiddleware scoping, no leakage into messaging handler chains, type-name collisions between chain kinds are disambiguated.
  • streaming_handler_support (existing, fixed) — tests were calling host.Services.GetRequiredService<IMessageBus>() from root provider; IMessageBus is registered as scoped. Changed all 6 occurrences to host.MessageBus() which wraps the singleton IWolverineRuntime.

Sample verification scorecard (dotnet run server + client, checked against expected output):

Sample Contract style RPC shape Result
PingPongWithGrpc Code-first (hand-written) Unary
PingPongWithGrpcStreaming Code-first (hand-written) Server streaming
GreeterProtoFirstGrpc Proto-first Unary + server streaming + error
GreeterCodeFirstGrpc Code-first (generated impl) Unary + server streaming
GreeterWithGrpcErrors Proto-first Unary + rich error details
ProgressTrackerWithGrpc Code-first (generated impl) Server streaming + mid-stream cancellation
OrderChainWithGrpc Proto-first Unary (cross-service chain)
RacerWithGrpc Code-first (hand-written) Bidirectional streaming

Docs

Updated pages:

  • docs/guide/grpc/index.md — sample count updated to 8; both new samples added to the tip table; [WolverineGrpcService] API reference expanded to describe both the interface (codegen) and class (existing) uses; Roadmap updated to distinguish the shipped interface path from the still-deferred WolverineGrpcServiceBase codegen parity.
  • docs/guide/grpc/contracts.md — new top-level section "Code-first codegen (zero-boilerplate)" covering the interface annotation pattern, IncludeAssembly bootstrap, generated class naming, client-side usage, and the conflict guard warning. Existing code-first subsection headings renamed to "Code-first (hand-written service class)" to distinguish the two paths.
  • docs/guide/grpc/handlers.md — "Discovery and codegen" updated from two passes to three, adding the new interface-based generated pass. Validate convention section added (proto-first only; rules, generated wrapper snippet, tip for field-level detail via google.rpc.BadRequest).
  • docs/guide/grpc/streaming.md — bidi is a first-class section with a generated wrapper example alongside the code-first manual bridge; gRPC-internals preamble trimmed; back-pressure paragraph replaced with a :::tip explaining await Task.Yield(); cancellation section simplified from a numbered list of gRPC internals to a single paragraph; Current Limitations updated; Related section updated to include ProgressTrackerWithGrpc.
  • docs/guide/grpc/handlers.md — Middleware and Policies section added documenting all three registration paths (Validate inline, AddMiddleware<T>, IGrpcChainPolicy); "throw domain exceptions" promoted to a :::tip callout; observability section simplified (removed ASP.NET Core Activity mechanics walk-through).
  • docs/guide/grpc/contracts.md:::warning added for bidi being silently skipped on the generated-implementation path; :::warning added for the [WolverineGrpcService] conflict rule (interface + class both annotated).
  • docs/guide/grpc/samples.md — count updated to 8; both new samples added to the overview table with shape/style/grpc-dotnet comparisons; full detailed sections added for GreeterCodeFirstGrpc and ProgressTrackerWithGrpc.

…roto-first gRPC service chain codegen

[WolverineBefore(MiddlewareScoping.Grpc)] and [WolverineAfter(MiddlewareScoping.Grpc)] methods on proto-first
stubs are now woven into every generated gRPC service wrapper. Before-frames execute before bus dispatch;
after-frames execute after (unary awaits the result so afters run before the return; server-streaming afters
run after the IAsyncEnumerable loop drains). MessageHandlers-scoped methods are correctly excluded.
WolverineGrpcOptions.MapException<T>(StatusCode) lets callers override the built-in AIP-193
Exception → StatusCode table per exception type. User-registered mappings are consulted after
the rich-status pipeline and before the default table. Inheritance is walked most-derived first;
later registrations win for the same type.

Resolves the last open item in Current Limitations from the gRPC docs.
…st stubs

Static Validate/ValidateAsync methods returning Grpc.Core.Status? on a
proto-first stub are woven into the generated wrapper. A non-null return
throws RpcException before the Wolverine handler runs.

Adds GrpcValidateShortCircuitFrame to GrpcServiceChain.AssembleTypes,
a dedicated test proto + fixture (ValidateConventionFixture), and docs
in handlers.md describing the convention, rules, and generated shape.
Wolverine now code-generates the bidi loop for proto-first services.
Mark a stub with [WolverineGrpcService] and write a standard streaming
handler (Handle(TRequest) → IAsyncEnumerable<TResponse>); the generated
wrapper does the while/MoveNext outer loop automatically.

Client-streaming (stream TRequest → TResponse) is still unsupported and
still fails fast at startup with a clear error.

Before-frames and the Validate short-circuit are not woven into bidi
methods — they require a concrete TRequest before the loop begins.
…codegen

Wolverine can now automatically generate concrete implementations for gRPC
[ServiceContract] interfaces marked with [WolverineGrpcService].

This "Option C" workflow forwards interface methods directly to the Wolverine
message bus:
- Unary Task<TResponse> methods map to IMessageBus.InvokeAsync<T>()
- Server-streaming IAsyncEnumerable<TResponse> methods map to IMessageBus.StreamAsync<T>()

Includes support for protobuf-net.Grpc's CallContext and provides automatic
validation to prevent conflicts between generated and hand-written
implementations of the same contract.
@erikshafer erikshafer changed the title gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming WIP: gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming Apr 22, 2026
@erikshafer erikshafer changed the title WIP: gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming WIP: gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidirectional Streaming Apr 22, 2026
Enables automatic discovery and registration of code-first gRPC service contracts. Wolverine now scans for interfaces decorated with both [ServiceContract] and [WolverineGrpcService], generates their implementations at startup, and maps them to the routing pipeline.

Includes comprehensive integration tests for unary and server-streaming patterns, cancellation propagation, and diagnostic descriptions.
Adds two new sample project sets demonstrating Wolverine's code-first gRPC integration with protobuf-net.Grpc:

- GreeterCodeFirstGrpc: A basic example of unary and server-streaming RPCs using shared interfaces and static handlers.
- ProgressTrackerWithGrpc: A more advanced server-streaming example showing how to handle progress updates and client-side cancellation.
…ples

Updates the gRPC documentation to cover the new generated code-first path where Wolverine implements ServiceContract interfaces automatically.

- Adds a comprehensive comparison between code-first and proto-first styles.
- Documents the three-pass discovery process (hand-written code-first, generated code-first, and proto-first).
- Adds detailed walkthroughs for the GreeterCodeFirstGrpc and ProgressTrackerWithGrpc samples, including server-streaming cancellation patterns.
- Updates the road map to reflect that the zero-boilerplate path now utilizes the full JasperFx codegen pipeline.
Renames the service implementation in the RacerWithGrpc sample for better clarity and consistency with other gRPC service naming conventions.
…tations

Updates gRPC documentation and code comments to replace "zero-boilerplate codegen" and "Option C" phrasing with "generated implementation" for better clarity and consistency.

Also simplifies technical descriptions regarding exception mapping and clarifies that middleware weaving is currently bypassed for bidirectional streaming methods.
…eware weaving

Introduces `HandWrittenGrpcServiceChain` to generate thin delegation wrappers for concrete code-first gRPC services (classes ending in `GrpcService` that implement a `[ServiceContract]` interface).

The generated wrapper implements the same interface, weaves `Validate`/before/after middleware (excluding bidi methods where no single request is in scope), then delegates to an `ActivatorUtilities`-resolved instance of the user's class.

Supports all three RPC shapes: unary, server-streaming, and bidirectional streaming. Includes comprehensive discovery tests, integration tests via `HandWrittenChainFixture`, and validation short-circuit tests.
Extends `GrpcGraph.DiscoverServices` to accept `WolverineGrpcOptions` and apply registered middleware and `IChainPolicy` implementations to proto-first and hand-written gRPC chains after discovery.
…in types

Changes the `AddMiddleware` signature from `Func<GrpcServiceChain, bool>?` to `Func<IChain, bool>?` to support filtering across proto-first (`GrpcServiceChain`) and hand-written (`HandWrittenGrpcServiceChain`) chains.

Introduces a default `IsGrpcChain` predicate that matches both chain types, excluding code-first chains until they achieve full middleware parity (P3). Expands XML documentation to clarify filtering patterns and chain-type participation.
…during bootstrapping

Introduces `IGrpcChainPolicy` to provide typed access to all three gRPC chain kinds (proto-first, code-first, hand-written) during discovery. Analogous to `IHttpPolicy` for HTTP endpoints.

Policies are registered via `WolverineGrpcOptions.AddPolicy<T>()` or `AddPolicy(IGrpcChainPolicy)` and applied immediately before code generation. Includes `LambdaGrpcChainPolicy` for inline policy registration.
Adds comprehensive documentation for the three middleware/policy attachment patterns:

- Inline `Validate`/`Before`/`After` methods on service classes (per-service)
- `opts.AddMiddleware<T>()` for gRPC-scoped middleware with optional chain filtering
- `opts.AddPolicy<T>()` / `IGrpcChainPolicy` for structural chain customization

Includes API reference table entries and clarifies that global `Policies.AddMiddleware<T>()` intentionally does not reach gRPC chains.
… gRPC chains

Extends `CodeFirstGrpcServiceChain` and `HandWrittenGrpcServiceChain` to fully participate in the middleware pipeline registered via `opts.AddMiddleware<T>()`. Frames are now cloned per method to preserve independent mutable state.

Introduces `ModifyCodeFirstGrpcServiceChainAttribute` for per-interface customization, mirroring `ModifyGrpcServiceChainAttribute` for proto-first services. Updates documentation and API reference to reflect middleware support across all three chain kinds (proto-first, code-first generated, hand-written).
@erikshafer erikshafer changed the title WIP: gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidirectional Streaming gRPC: Middleware Weaving, Validate Convention, User Exception Mapping, Bidi Streaming, Code-First Codegen, New Samples Apr 22, 2026
…ent transport breakage

Introduces `GrpcSerialTestsCollection` and applies `[Collection("GrpcSerialTests")]` to tests that manipulate `DynamicCodeBuilder.WithinCodegenCommand`, preventing parallel execution with transport compliance tests.

When the static flag is set during host startup, `applyMetadataOnlyModeIfDetected()` forces `DurabilityMode.MediatorOnly`, breaking transport operations like `SendAsync` and `RequireResponse`.
Replaces `host.Services.GetRequiredService<IMessageBus>()` with the cleaner `host.MessageBus()` extension method across all streaming handler acceptance tests.
TL;DR: All 6 occurrences fixed. The root cause: we wrote the streaming tests resolving IMessageBus directly from the root IServiceProvider, but IMessageBus is registered as AddScoped<IMessageBus, MessageContext>() — ASP.NET's scope validation rejects this. The fix is host.MessageBus(), which creates a MessageBus (wrapping the singleton IWolverineRuntime) from the root provider — that's exactly what the existing host extension helpers do.
…n racer sample

Removes `ComputeStandings` and `LogStandings` helper methods in favor of inline LINQ. Adds visual divider in client to separate each full standings snapshot and improves position display formatting.
…ncellation flow, and bidi caveats

Simplifies verbose explanations in `streaming.md` around back-pressure, cancellation propagation, and server/bidi handler shapes. Adds tip explaining why `await Task.Yield()` appears in examples. Clarifies that bidi methods are skipped silently on the code-first generated-implementation path.

In `contracts.md`, adds warning that bidirectional streaming is unsupported for generated implementations. In `handlers.md`, adds tip recommending domain exceptions over `RpcException` and condenses observability section to focus on natural `Activity.Current` propagation.
…ains

Extends `CodeFirstGrpcServiceChain` to discover and weave static `Validate`/`Before`/`After` methods declared on the `[ServiceContract]` interface, following the same naming conventions and attributes as proto-first and hand-written chains.

Introduces `DiscoveredBefores` and `DiscoveredAfters` properties that scan the service contract interface for qualifying static methods, filtered per-RPC-method by request type to prevent a `Validate(OrderRequest)` from firing inside an `InvoiceRequest` RPC.

Adds `GrpcValidateShortCircuitFrame` to emit early-return logic when a `Validate` method produces a non-null `Status?`. Includes integration tests verifying `ICodeFirstValidatedService.Validate` short-circuits invalid requests and allows valid requests through to the handler.
…code-first gRPC chains

Aligns code-first gRPC chains with Wolverine's handler idiom: static `Before`/`Validate`/`After` hooks now live on the handler class (the type containing `Handle`/`HandleAsync`/`Consume`/`ConsumeAsync` for the request message), not on the `[ServiceContract]` interface.

Replaces `DiscoveredBefores`/`DiscoveredAfters` with `HandlerTypesFor(requestType)`, which scans `ApplicationAssemblies` (set by `GrpcGraph.DiscoverServices`) for handler types per RPC method. Assembly scanning is necessary because `AssembleTypes` fires before `HandlerGraph.Compile()` runs.

Moves `ICodeFirstValidatedService.Validate` to `SubmitHandler.Validate` and updates tests to verify handler-based discovery instead of interface-based discovery.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant