Skip to content

[Task] Grpc middleware pipeline #21

Description

@rian-be

Summary

Ensure plugin contributed middleware can be injected into the gRPC pipeline, not only the HTTP pipeline.

ASP.NET Core gRPC does not use HttpContext based middleware the same way as HTTP, it relies on interceptors (Grpc.Core.Interceptors.Interceptor). This sub issue defines how PluginMiddleware , gated by its Transport, maps onto the gRPC pipeline, how PipelinePosition is interpreted for gRPC, and how the host handles HTTP only middleware on the gRPC transport.

Goal

Allow plugins to contribute cross cutting behavior (auth, logging, correlation ids, etc.) to the gRPC transport with the same declarative model used for HTTP, while keeping the contract explicit about what is and is not supported.

The implementation must reuse existing gRPC interceptor infrastructure no custom gRPC pipeline engine and no second interceptor framework should be introduced.

Background

A plugin written for HTTP middleware typically depends on HttpContext. In gRPC:

  • the request context is ServerCallContext, not HttpContext,
  • interceptors compose as call chain (not as six native insertion points),
  • and some HTTP only middleware cannot be adapted.

Without an explicit contract, plugins either break on gRPC or silently do nothing. The host must define deterministic, documented behavior driven by the declared transport, never by reflection guesses.

Scope

C7. Middleware in gRPC pipeline

The host must support injecting plugin middleware into the gRPC pipeline for entries declared with Transport = Grpc:

  • compose PipelinePosition as AuthKit semantic positions into the gRPC interceptor chain (not as six native ASP.NET Core points),
  • resolve the interceptor from the request service provider, mirroring ,
  • and handle HTTP only middleware explicitly (skip with warning) instead of attempting an automatic HttpContext bridge.

C7. Middleware in gRPC Pipeline

Registration model (transport gated)

Plugins declare PluginMiddleware. The Transport field selects the target; the host never inspects a middleware to guess its transport:

// HTTP only behavior
new PluginMiddleware(typeof(MyHttpMiddleware), PipelinePosition.BeforeAuthentication,
    Transport = AuthKitTransport.Http);

// gRPC native behavior
new PluginMiddleware(typeof(MyGrpcInterceptor), PipelinePosition.BeforeAuthentication,
    Transport = AuthKitTransport.Grpc);

For gRPC, MiddlewareType should be concrete Grpc.Core.Interceptors.Interceptor subclass. If unified plugin contract is desired, the host may offer very thin IAuthKitGrpcInterceptor adapter that it wraps into an Interceptor but this must not become second interceptor framework, the canonical accepted type is Interceptor.

PipelinePosition is semantic, not native

PipelinePosition values are AuthKit semantic positions, not native ASP.NET Core gRPC insertion points. The gRPC interceptor chain forms single call pipeline, the host is responsible for mapping each semantic position onto the correct place relative to the host's own interceptors.

PipelinePosition gRPC meaning
BeforeRouting before call handling begins
AfterRouting after endpoint/method resolution
BeforeAuthentication before the host authentication interceptor
AfterAuthorization after the host authorization interceptor
BeforeEndpoints directly before the service method
AfterEndpointExecution post processing after the service method

These are AuthKit level semantics and must be documented as such they are not one to one with native ASP.NET Core pipeline stages.

Host composition and ordering

The host collects every PluginMiddleware with Transport = Grpc, then:

  1. group by PipelinePosition,
  2. sort by deterministic ordering (Order, then stable PluginId, then DeclarationIndex),
  3. compose into the gRPC interceptor chain, interleaving plugin interceptors with the host's own interceptors.

Example composition:

BeforeRouting -> Plugin C
AfterRouting -> Plugin A
BeforeAuthentication -> (host authentication interceptor)
  ...
AfterAuthorization -> Plugin B
BeforeEndpoints - Plugin D
  gRPC service method
AfterEndpointExecution  -> Plugin D (post processing)
                        -> Plugin B (post processing)

This yields predictable, host driven behavior, the plugin only declares position and order.

AfterEndpointExecution (post processing)

AfterEndpointExecution means the interceptor performs post-processing after the downstream call - which is the natural gRPC interceptor shape:

public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
    TRequest request, ServerCallContext context,
    UnaryServerMethod<TRequest, TResponse> continuation)
{
    var response = await continuation(request, context); // downstream call
    // post processing here
    return response;
}

It is not separate "next interceptor" inserted after the method it is the tail of the same interceptor that wrapped the call.

HttpContext dependent middleware (no reflection, no auto-bridge)

The host must not detect HttpContext usage via reflection (eg. scanning method bodies) - that is fragile and unreliable. Instead:

  • PluginMiddleware with Transport = Http (or default) that targets IAuthKitMiddleware / AuthKitMiddlewareBase / convention middleware is skipped on the gRPC transport with an explicit warning.
  • PluginMiddleware with Transport = Grpc whose MiddlewareType is an Interceptor (or the thin IAuthKitGrpcInterceptor adapter) is executed on gRPC.
  • An optional IAuthKitGrpcMiddlewareAdapter may be registered explicitly by plugin that wants to share logic between transports; the host never auto creates this bridge.

No automatic HttpContext -> ServerCallContext bridge is provided by default, because partial/fake HttpContext creates unsafe expectations (Request.Path, Response, Connection, full pipeline next, etc.).

Default behavior matrix:

IAuthKitMiddleware (Transport = Http) -> gRPC: SKIP + explicit warning
Interceptor (Transport = Grpc) -> gRPC: EXECUTE
explicit IAuthKitGrpcMiddlewareAdapter -> gRPC: EXECUTE (plugin declared)

Streaming support

gRPC is not limited to unary RPCs. The interceptor must work for all four call patterns:

  • Unary -> Unary
  • Unary -> Stream (server streaming)
  • Stream -> Unary (client streaming)
  • Stream -> Stream (duplex streaming)

Because the contract reuses Grpc.Core.Interceptors.Interceptor, the base class already provides the corresponding overloads (UnaryServerHandler, ClientStreamingServerHandler, ServerStreamingServerHandler, DuplexStreamingServerHandler). No custom streaming pipeline is required this must be covered by the acceptance and integration checks.

Validation (C6 extension)

The contract validator is extended to recognize gRPC interceptor registrations:

  • Transport = Grpc entries must have MiddlewareType that is public, concrete, non abstract, non generic Interceptor subclass,
  • abstract Interceptor subclasses, open/closed generic Interceptor subclasses, and arbitrary non Interceptor types are rejected,
  • with the same explicit, plugin naming error messages used for HTTP middleware.

Architecture

                 IAuthKitPlugin
                       |
                       v
                PluginMiddleware
                       |
             +---------+---------+
             |                   |
           HTTP                gRPC
             |                   |
     IAuthKitMiddleware      Interceptor
             |                   |
       ASP.NET middleware    gRPC interceptor chain

            PipelinePosition (shared semantic abstraction)
                       |
             +---------+---------+
             |                   |
            HTTP                gRPC
             |                   |
      native middleware      interceptor chain
        positions            (host composes)

PipelinePosition remains the common abstraction; the host maps it onto each transport's pipeline. The plugin does not need to know how the host technically builds the pipeline.


Backward Compatibility

This sub issue is additive.

  • Plugins that only target HTTP are unaffected.
  • IAuthKitPlugin is unchanged gRPC support is an additional host capability selected via Transport.
  • Existing HTTP middleware continues to work for the HTTP transport on gRPC it is skipped with warning.
  • DevTokens must continue to compile and load successfully; if it has no gRPC middleware, nothing changes.

Validation

C7. Middleware in gRPC Pipeline

  • gRPC PluginMiddleware entries require Transport = Grpc with an Interceptor MiddlewareType.
  • PipelinePosition is documented as AuthKit semantic positions composed into the interceptor chain (not six native points).
  • Host groups by PipelinePosition, sorts deterministically, and composes the interceptor chain.
  • gRPC interceptors resolve from the request service provider (single scope).
  • HTTP only middleware (Transport = Http) is skipped on gRPC with an explicit warning.
  • No reflection based HttpContext detection and no automatic HttpContext -> ServerCallContext bridge.
  • Contract validator covers gRPC interceptor registrations.
  • Ordering is deterministic across plugins.

Integration

  • gRPC sample plugin can contribute an Interceptor.
  • HTTP sample plugin is unaffected on the HTTP transport and is skipped with warning on gRPC.
  • Interceptor works for unary, server streaming, client streaming, and duplex streaming RPCs.
  • DevTokens continues to load.
  • PluginContractValidator passes (including non Interceptor gRPC type being rejected).

Acceptance Criteria

  • Plugin middleware is supported on the gRPC transport via Interceptor subclasses, selected by Transport = Grpc.
  • PipelinePosition is mapped as AuthKit semantic positions into the gRPC interceptor chain deterministically and documented as such.
  • gRPC interceptors can use scoped services from the request service provider (single scope).
  • HTTP only middleware is explicitly skipped on gRPC with warning it is never silently no op'd or auto bridged.
  • The contract validator covers gRPC interceptor registrations (concrete Interceptor, non generic, non abstract).
  • Streaming RPCs (unary, server streaming, client streaming, duplex) are supported via the base Interceptor overloads.
  • Existing HTTP behavior is unchanged.
  • DevTokens continues to compile and load successfully.

Non Goals

This sub issue does not include:

  • a second AuthKit interceptor framework (we reuse Grpc.Core.Interceptors.Interceptor),
  • an automatic HttpContext -> ServerCallContext bridge,
  • reflection based transport detection,
  • new authentication/authorization semantics,
  • changing PipelinePosition values,
  • or custom gRPC pipeline engine.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1Core operationadditiveAdditive, non-breaking changearea/abstractionsAuthKit.Plugins.Abstractions contractcontractChanges the plugin contractsub-taskChild task of an epic

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions