gRPC #2525 follow-up: Correctness Gaps#2542
Merged
jeremydmiller merged 6 commits intoJasperFx:mainfrom Apr 20, 2026
Merged
Conversation
…otent registration Implement comprehensive middleware scoping tests for proto-first gRPC services, verifying that `[WolverineBefore(MiddlewareScoping.Grpc)]` attaches while `[WolverineBefore(MiddlewareScoping.MessageHandlers)]` is excluded. Add policy leak guards preventing handler-only middleware from crossing into gRPC chains. Make `AddWolverineGrpc()` idempotent via marker pattern to prevent duplicate interceptor registration. Introduce `WolverineGrpcOptions` for gRPC-specific middleware policy analogous to `WolverineHttpOptions`. - Add MiddlewareScopingFixture test harness with GreeterMiddlewareTestStub - Add scope_discovery_tests verifying DiscoveredBefores/DiscoveredAfters filtering - Add policy_leak_tests pinning HandlerChain-only filter boundary - Add middleware_scoping_fixture_smoke_tests for unary and server-streaming round trips - Add add_wolverine_grpc_idempotency_tests preventing duplicate registrations - Add WolverineGrpcOptions with AddMiddleware<T>(filter) for gRPC chain targeting - Add GrpcServiceChain.DiscoveredBefores/DiscoveredAfters for Phase-1 codegen - Add WolverineGrpcMarker singleton guard for repeat AddWolverineGrpc() calls - Add middleware_scoping_test.proto for test-only GreeterMiddlewareTest service
…ethodCall invariant tests Sort `DiscoveredBefores` and `DiscoveredAfters` ordinally by method name so Phase-1 generated gRPC service source is byte-stable across runs (reflection order is unspecified). Add `frame_cloning_spike_tests` pinning the invariant that two `MethodCall` instances built from one `MethodInfo` do not share mutable `Arguments[]` or `Creates` collections — if caching ever broke this, one RPC's argument resolution would contaminate siblings during codegen. - Add OrderBy(m => m.Name, StringComparer.Ordinal) to DiscoveredBefores/DiscoveredAfters - Add frame_cloning_spike_tests verifying Arguments[] and Creates are per-instance - Add discovered_befores/afters_are_ordinally_sorted_for_deterministic_codegen tests - Update XML doc comments explaining ordinal-sort and byte-stability guarantees
…s examples, and document client-side error mapping Remove experimental/branch references from feature status. Replace `describe-routing` with `codegen-preview --grpc` in diagnostics examples and expand explanation of handler discovery debugging. Document client-side `MapRpcException` override in limitations section. Clarify middleware scoping implementation status and provide workaround guidance until M15 codegen lands. - Remove feature/grpc-and-streaming-support branch reference from index.md - Update diagnostics commands in handlers.md with codegen-preview examples - Add cross-reference to codegen-preview CLI documentation - Expand Current Limitations section with client-side error mapping note - Clarify middleware scoping status and interim interceptor workaround - Update roadmap to reflect deferred M15 and code-first codegen work
…ng fixes, and typed-enumerable cascade optimization Implement stable hash-based disambiguation for proto services sharing simple names (e.g., `Greeter` in two bounded contexts) so `AttachTypesSynchronously` deterministically resolves unique handler types. Fix streaming handler execution-finished tracking and activity status to fire only after enumeration completes or throws mid-stream. Cache `IAsyncEnumerable<T>` cascade MethodInfo per message type to eliminate repeated reflection. - Add GrpcGraph.DisambiguateCollidingTypeNames applying stable hash suffixes to colliding TypeName values - Add GrpcServiceChain.ApplyDisambiguatedTypeName for post-discovery rewrite - Add type_name_disambiguation_tests verifying stability, idempotence, and collision-free preservation - Move ExecutionFinished and activity status calls after MoveNextAsync loop in Executor/TracingExecutor - Add mid_stream_throw_marks_activity_status_error test pinning error propagation - Add MessageContext._typedEnumerableCascadeMethods ImHashMap cache for IAsyncEnumerable<T> detection - Replace GetInterfaces + MakeGenericMethod cascade hot path with cached MethodInfo lookup
Clarify that the OpenTelemetry activity for server-streaming handlers remains open until the `IAsyncEnumerable<T>` fully drains or faults, ensuring dashboards reflect the real terminal state (including mid-stream exceptions and cancellation) rather than the moment the handler returned the enumerable. - Expand exception timing documentation in streaming.md with activity status behavior - Note that activity is marked `Error` for both pre-yield and mid-stream exceptions - Explain deferred activity completion aligns telemetry with actual stream lifecycle
This was referenced Apr 21, 2026
Closed
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.
fix(grpc): #2525 correctness gaps — PR-A
Closes four correctness gaps from #2525 plus Phase 0 of the
MiddlewareScoping.Grpcfix.Target release: Wolverine 5.31.x. or Wolverine 5.32.x
Test state: 115/115
Wolverine.Grpc.Testsgreen · 7/7streaming_handler_supportgreen (incl. new mid-stream throw → OTel Error assertion) · 19/19 cascade-related CoreTests green · clean build, 0 errors (pre-existing VSTHRD warnings only).Scope: 1125 insertions / 66 deletions across 24 files. Framework-side delta is ~165 lines across 6 source files; the rest is tests, fixtures, and two updated docs pages.
Contents
AddWolverineGrpc()idempotent (P0)IAsyncEnumerable<T>cascading reflection cache (P1)MiddlewareScoping.GrpcgroundworkChanges at a glance
AddWolverineGrpc()idempotentIAsyncEnumerable<T>reflection cacheWolverineGrpcOptions.AddMiddleware<T>§1.2 Streaming OTel + success-log fire post-iteration (P0)
Bug.
TracingExecutor.StreamCoreAsynclogged_messageSucceededand closed the activity the moment the handler returned theIAsyncEnumerable<T>, before the consumer iterated. A cancellation or mid-stream throw closed the span with Ok status — real failures were invisible on dashboards.Fix. Moved success/error signals to fire after iteration. C# CS1626 forbids
yield returninside atry/catch, so switched to a manualMoveNextAsyncloop where the try/catch wraps only the enumerator advance andyield return current;sits outside. Applied the same structural fix toExecutor.StreamCoreAsync.Test. New
mid_stream_throw_marks_activity_status_errorasserts activity status goes Error when iteration throws. Existing cancellation and faulting-stream tests continue to pass.Files.
Runtime/Handlers/TracingExecutor.cs,Runtime/Handlers/Executor.cs,Testing/CoreTests/Acceptance/streaming_handler_support.cs.§1.3
AddWolverineGrpc()idempotent (P0)Bug. Double-calling
AddWolverineGrpc()stackedWolverineGrpcExceptionInterceptortwice and double-registeredGrpcGraph. Library extensions calling it defensively paid double log + translation overhead per unhandled exception.Fix. Marker-singleton guard (
WolverineGrpcMarker) matching the patternUseGrpcRichErrorDetails/UseFluentValidationalready use. Plus a new overload:Test.
add_wolverine_grpc_idempotency_tests.cs— 8 tests pinning single-registration invariants across interceptor, graph, options, and the new overload.Files.
Wolverine.Grpc/WolverineGrpcExtensions.cs,Wolverine.Grpc/WolverineGrpcExceptionInterceptor.cs(2-line XML-doccrefdisambiguation — the bare cref became ambiguous with the new overload).§1.4 Generated gRPC type-name uniqueness (P1)
Bug.
GrpcServiceChain.TypeName = "{ProtoServiceName}GrpcHandler". Two assemblies shipping aGreeterproto — tests + production, or two bounded contexts — emitGreeterGrpcHandlerinto the sameWolverineHandlerschild namespace.AttachTypesSynchronouslypicks whicheverGetExportedTypes()returns first; that order is unspecified across assemblies.Fix. Mirrors
HandlerGraph.cs:370-383's post-hoc qualifier-based disambiguation (the issue #2004 pattern) but uses a stable hash ofStubType.FullNameas the qualifier — gRPC stub simple names aren't reliably unique. Runs once at the tail ofDiscoverServices, never on a hot path; zero additional reflection.Tests.
type_name_disambiguation_tests.cs— 3 tests: collisions get hashed suffixes, disambiguation is idempotent + process-stable, single-chain input preserves default name. Collision stubs areinternal abstractwithout[WolverineGrpcService], soGetExportedTypes()andIsProtoFirstStubboth skip them — they don't leak into adjacent test suites.Files.
Wolverine.Grpc/GrpcGraph.cs,Wolverine.Grpc/GrpcServiceChain.cs(TypeNamenowget; private set;plus internalApplyDisambiguatedTypeNameseam used only by the pass).§1.5 Typed
IAsyncEnumerable<T>cascading reflection cache (P1)Bug. In
MessageContext.EnqueueCascadingAsync, every cascading non-enumerable message didmessage.GetType().GetInterfaces()(allocates) plusMakeGenericMethodon hit. Both per-call, on a hot path.Fix. Single
ImHashMap<Type, MethodInfo?>keyed on message type:Chose
ImHashMapfor codebase idiom (matchesWolverineMessageNaming.cs:128+ five other per-type cache sites — lock-free, copy-on-write, write-rare). Keyed on message type rather than element type because message-type keying eliminates both reflection ops on hit and negative-caches plain cascading messages (the common fall-through path). The only remaining reflection isMethodInfo.Invoke— Expression-compiled delegates could eliminate it, but not worth the diff today.Tests. Existing
typed_async_enumerable_cascades_items_via_regular_invokeplus 19 cascade-related CoreTests exercise the path; all green.Files.
Runtime/MessageContext.cs.M15 Phase 0 —
MiddlewareScoping.GrpcgroundworkBug.
MiddlewareScoping.Grpcshipped in #2525 as a public enum member with attribute-level tests but no execution path. A handler annotated[WolverineBefore(MiddlewareScoping.Grpc)]compiled, tests passed, and the method never ran on any RPC call.Phase 0 lands the architecture-independent groundwork so the service-wide-vs-per-RPC design question (see Review ask #1) can be resolved without blocking the release window. This PR lands what both options need; Phase 1 is a focused follow-up.
What Phase 0 ships:
GrpcServiceChain— new lazyDiscoveredBefores/DiscoveredAftersreusingMiddlewarePolicy.FilterMethods<T>so scope filtering matches the canonicalChain.ApplyImpliedMiddlewareFromHandlerspath. Ordinal-sorted for byte-stable codegen; reference-equal caching.WolverineGrpcOptions.AddMiddleware<T>mirroringWolverineHttpOptions.AddMiddleware. InternalMiddlewarePolicy, typedAddMiddleware<T>with optional per-chain filter. Singleton options registered via the §1.3 idempotency guard.policy_leak_tests.cs—IPolicies.AddMiddleware<T>must not attach toGrpcServiceChain(thechain is HandlerChainfilter atWolverineOptions.Policies.cs:208-216is load-bearing).frame_cloning_spike_tests.cs— twoMethodCalls from the sameMethodInfoare distinct instances with per-instanceArguments[]+Creates. Phase 1's per-RPC argument resolution depends on this non-sharing invariant.No production behavior changes today.
MapWolverineGrpcServicesis unchanged; nothing consumesDiscoveredBeforesat emission time yet. End-user gRPC middleware behavior is byte-identical tomain.Tests. 20/20 green: 8 idempotency + 7 scope_discovery + 2 frame_cloning + 2 smoke + 1 policy_leak.
Files.
Wolverine.Grpc/WolverineGrpcOptions.cs(new)Wolverine.Grpc/WolverineGrpcExtensions.cs(+overload)Wolverine.Grpc/GrpcServiceChain.cs(+discovery properties)Wolverine.Grpc.Tests/Wolverine.Grpc.Tests.csproj(proto + folder globs)Wolverine.Grpc.Tests/GrpcMiddlewareScoping/*(12 new fixture/test files; folder nameGrpcMiddlewareScoping/— notMiddlewareScoping/— to avoid shadowing the enum)Docs updated in-PR
docs/guide/grpc/index.md— drops the "experimental / feature branch" framing from the info block now that gRPC Support for Wolverine HTTP Endpoints + IMessageBus.StreamAsync<T> #2525 is merged. Adds a Current Limitations bullet honestly disclosing the Phase 0/Phase 1 state ofMiddlewareScoping.Grpc: the attribute compiles and is scope-filtered correctly but doesn't weave yet; recommend custom gRPC interceptor until M15 Phase 1 lands. Expands the exception-mapping limitation to note that client-sideWolverineGrpcClientOptions.MapRpcExceptionalready supports per-client override.docs/guide/grpc/handlers.md— replaces staledotnet run -- describe-routingexample withwolverine-diagnostics codegen-preview --grpc Greeterand forward-links the CLI page.docs/guide/grpc/streaming.md— extends the "Exception timing" limitation bullet to note that the server-side OpenTelemetry activity now stays open until the stream fully drains or faults, and is markedErrorfor both mid-stream throws and cancellation.Review asks
GrpcHandler) or per-RPCGrpcRpcChainwith its ownHandlerChain-like lifecycle (Option B)? Working default if no response: Option A (smaller blast radius; gRPC isn't on NuGet yet so a wrong-A → B refactor is cheap, but a wrong-B permanently reshapes chain topology). Full analysis:.plans/grpc-streaming/M15-middleware-scoping-implementation.md§6.ImHashMap<Type, MethodInfo?>for codebase idiom (matches 5 existing sites). Flag if you'd ratherConcurrentDictionaryfor this one.GetDeterministicHashCode()onStubType.FullName(mirrorsHandlerChain). Flag if you'd prefer a different qualifier (e.g., assembly name).Not in this PR (by design)
[WolverineOnException]/[WolverineFinally]discovery onGrpcServiceChain— similar Phase 0 treatment; separate PR.MiddlewareScoping.Grpc— walkthrough/examples deferred to Phase 1 so they describe real execution. Current Limitations disclosure ships now.MapToExceptiondefault posture,IGrpcStatusDetailsProviderordering,Google.Api.CommonProtosfootprint) — each wants its own decision thread and ships as small follow-ups..plans/next-pr-roadmap.md.