Repository navigation
Server-side tenant id detection for Wolverine.Grpc (GH-3368) - #3382
Merged
Merged
Conversation
Mirrors Wolverine.Http's ITenantDetectionPolicies for gRPC services: - New WolverineGrpcOptions.TenantId (IGrpcTenantDetectionPolicies) with IsRequestHeaderValue (case-insensitive metadata), IsClaimTypeNamed (HttpContext.User), DetectWith/DetectWith<T>, DefaultIs, and AssertExists (RpcException InvalidArgument, the gRPC mapping of HTTP's 400) - DetectGrpcTenantIdFrame declares the 'tenantId' codegen variable (PersistenceConstants.TenantIdVariableName) that Marten/Polecat session-opening frames consume, and applies the detected tenant to the scoped IMessageBus before InvokeAsync/StreamAsync — structurally, without relying on the ambient IMessageContext - Woven as the built-in GrpcTenantIdDetection IGrpcChainPolicy across all three chain flavors: proto-first (all RPC shapes), code-first and hand-written (unary methods with a CallContext parameter; hand-written delegation wrappers apply the tenant via the request-scoped IMessageContext since they carry no bus field) - Zero-config default: with no explicit TenantId configuration and PropagateEnvelopeHeaders=true, detect the 'tenant-id' metadata header the Wolverine gRPC client interceptor stamps — Wolverine-to-Wolverine hops round-trip the tenant with no server config; suppressed by PropagateEnvelopeHeaders=false, replaced by any explicit strategy - Docs: new guide/grpc/multi-tenancy page, nav entry, client.md now describes propagation as round-trip with both server-side mechanisms - Tests: e2e header/claim/custom/fallback/assert-exists detection, zero-config default, interceptor-disabled isolation, structural assertions on generated source for all three chain flavors Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Closes GH-3368
Builds on #3369 (the runtime
WolverineGrpcServicePropagationInterceptor): this adds the codegen-level server-side tenant id detection mirroring Wolverine.Http'sITenantDetectionPolicies, so Marten/Polecat session-opening frames and the forwarded envelope get the tenant structurally — without relying on the ambientIMessageContext— plus claims and custom detection that the interceptor cannot do.What's here
API (
WolverineGrpcOptions.TenantId,IGrpcTenantDetectionPolicies)AssertExists()maps to gRPC statusInvalidArgument— the canonical (AIP-193) equivalent of the 400 Bad Request Wolverine.Http returns for the same condition; documented in the new docs page, including whenUnauthenticated/PermissionDeniedvia regular authorization is the better fit.Detection frame across all three chain flavors
GrpcTenantIdDetection : IGrpcChainPolicyis registered by theWolverineGrpcOptionsconstructor (exactly likeWolverineHttpOptionsregisters itsTenantIdDetection) and marks every discovered chain. Each chain'sAssembleTypesweaves a per-methodDetectGrpcTenantIdFramethat:tenantIdvariable (PersistenceConstants.TenantIdVariableName) consumed byOpenMartenSessionFrame/OpenPolecatSessionFrame,AssertExists(),IMessageBusbeforeInvokeAsync/StreamAsync(proto-first + code-first), or to the request-scopedIMessageContextvia the wrapper'sIServiceProvider(hand-written delegation wrappers, which carry no bus field).Coverage by flavor: proto-first = all RPC shapes (they all end in
ServerCallContext); code-first & hand-written = unary methods declaring aCallContextparameter (the only route toServerCallContextthere;IAsyncEnumerable-returning methods can't host the awaited detection call). Fallback for the uncovered shapes is the #3369 interceptor, and the limits are documented.Zero-config default
With no explicit
TenantIdconfiguration andPropagateEnvelopeHeaders == true(default), the policy auto-registersIsRequestHeaderValue("tenant-id")— the exact header the Wolverine gRPC client interceptor stamps — so a Wolverine→Wolverine hop round-trips the tenant with zero server config. Explicit configuration replaces the default;PropagateEnvelopeHeaders = falsesuppresses it. Surfaced explicitly viaZeroConfigDefaultAppliedand a comment line in the generated source.Docs
docs/guide/grpc/multi-tenancy.md(modeled on the HTTP page): three detection modes, zero-config default, interplay with the propagation interceptor,InvalidArgumentmapping rationale, scope/limitations..vitepress/config.mts; section index updated.client.mdpropagation section now describes the round-trip story with both server-side mechanisms (interceptor + codegen detection).Tests (
src/Wolverine.Grpc.Tests/MultiTenancy)ClaimsPrincipal,DetectWith<T>custom strategy, strategy ordering,DefaultIsfallback,AssertExistsrejection (InvalidArgument+ detail message) and pass-through, missing-tenant flow, zero-config round-trip, and explicit detection with the interceptor disabled (proving the codegen path is independent of gRPC: server-side envelope propagation + correct false docs/sample claims (GH-3368) #3369). Non-tenant-idheaders are used wherever the interceptor could otherwise mask the result.tenantIdcodegen variable name is pinned;PropagateEnvelopeHeaders = falsesuppresses the default; hand-written wrapper e2e through the ambient context.AddPolicyunit tests updated:Policiesnow always contains the built-in detection policy (same situation as HTTP's always-registered policies).Deviations from a literal HTTP mirror (and why)
AssembleTypes, not inserted intochain.Middleware: gRPC chains clone middleware frames per generated RPC method (CloneFrames), which only knowsConstructorFrame/MethodCalland would share a single detection frame instance (mutableNext) across N methods. The policy marks the chain; each flavor creates one frame per method with the right context expression (contextvscontext.ServerCallContext).DetectWith<T>()is deferred: HTTP configuresTenantIdinsideMapWolverineEndpoints()when the container exists; gRPC options are configured duringAddWolverineGrpc()service registration, so the type is recorded andQuickBuild-resolved at policy-apply time.AssertExists()throwsRpcException(InvalidArgument)instead of writing a 400 ProblemDetails.Verification
Wolverine.Grpc.Tests(net9.0): 271/271 passing (255 existing + 16 new)dotnet build wolverine.slnx -c Release: 0 errors, 0 warnings🤖 Generated with Claude Code