Skip to content

Integration Grpc Extension

Aryeh Citron edited this page Apr 27, 2026 · 17 revisions

Integration: gRPC Extension

Track gRPC client calls in your test diagrams using the TestTrackingDiagrams.Extensions.Grpc NuGet package. This extension intercepts all gRPC call types (unary, server-streaming, client-streaming, duplex-streaming) via the standard Grpc.Core.Interceptors.Interceptor API.

Using a shared library or abstraction layer? If your code doesn't use gRPC channels directly — e.g. it goes through a shared client library or custom abstraction — this extension won't be able to intercept the underlying calls. See Tracking Custom Dependencies for alternative approaches including RequestResponseLogger.LogPair() and TrackingProxy<T>.

Installation

dotnet add package TestTrackingDiagrams.Extensions.Grpc

Quick Start

What your production code looks like

Your production code creates gRPC clients normally — no tracking code here:

// Program.cs / Startup.cs — register gRPC clients via DI
services.AddSingleton(sp =>
{
    var channel = GrpcChannel.ForAddress("https://greeter-service:5001");
    return new Greeter.GreeterClient(channel);
});

// Or using the gRPC client factory:
services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://greeter-service:5001");
});
// GreetingService.cs — uses the client via DI (no tracking awareness)
public class GreetingService
{
    private readonly Greeter.GreeterClient _greeterClient;

    public GreetingService(Greeter.GreeterClient greeterClient)
    {
        _greeterClient = greeterClient;
    }

    public async Task<string> GetGreeting(string name)
    {
        var reply = await _greeterClient.SayHelloAsync(new HelloRequest { Name = name });
        return reply.Message;
    }
}

Adding tracking in your tests

In your test setup, replace the production gRPC client registration with a tracked version. The WithTestTracking extension wraps the channel with a GrpcTrackingInterceptor that records all gRPC calls for sequence diagrams:

// TestSetupHooks.cs or WebApplicationFactory setup
builder.ConfigureTestServices(services =>
{
    // Replace the production client with a tracked one
    services.AddSingleton(sp =>
    {
        var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var invoker = channel.WithTestTracking(new GrpcTrackingOptions
        {
            ServiceName = "GreeterService",
            CallingServiceName = "MyApi",
            Verbosity = GrpcTrackingVerbosity.Detailed,
            CurrentTestInfoFetcher = () => (TestContext.CurrentTestName, TestContext.CurrentTestId)
        });
        return new Greeter.GreeterClient(invoker);
    });
});

Or use the interceptor directly if you prefer:

var interceptor = new GrpcTrackingInterceptor(new GrpcTrackingOptions
{
    ServiceName = "GreeterService",
    CallingServiceName = "MyApi",
    CurrentTestInfoFetcher = () => (TestContext.CurrentTestName, TestContext.CurrentTestId)
});

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(interceptor);
var client = new Greeter.GreeterClient(invoker);

Key point: Your production code (controllers, services, etc.) stays unchanged. The only difference is how the gRPC client is constructed — tests wrap the channel with the tracking interceptor via DI replacement.

⚠️ SUT → downstream gRPC: The examples above work when the test thread creates and uses the gRPC client directly. If your SUT calls a downstream gRPC service during request processing (e.g. your API calls a notification service via gRPC), the client runs on the SUT's worker thread where CurrentTestInfoFetcher cannot access the test framework context. You must also set HttpContextAccessor on GrpcTrackingOptions — see Dual-Resolution Test Identity below.

Tracking Incoming gRPC Calls (Test → SUT)

If your SUT exposes a gRPC service (i.e. the test calls the SUT via gRPC), you can get rich gRPC-aware diagrams for the incoming leg too — with protobuf message deserialization, operation classification, and grpc:// URIs. Without this, incoming gRPC calls would appear as raw HTTP/2 POST requests with binary bodies.

One-liner: CreateTestTrackingGrpcClient (recommended)

The simplest approach mirrors the HTTP convenience API. This single call handles everything — ResponseVersionHandler (HTTP/2 fix), channel creation, tracking interceptor, and client construction:

using TestTrackingDiagrams.Extensions.Grpc;

var client = factory.CreateTestTrackingGrpcClient<Program, Greeter.GreeterClient>(
    new GrpcTrackingOptions
    {
        ServiceName = "My API",
        CallingServiceName = "Caller",
        CurrentTestInfoFetcher = () => (testName, testId)
    });

If you need to dispose the underlying GrpcChannel cleanly (e.g. in [AfterScenario]), use the WithChannel variant:

var (client, channel) = factory.CreateTestTrackingGrpcClientWithChannel<Program, Greeter.GreeterClient>(
    new GrpcTrackingOptions
    {
        ServiceName = "My API",
        CallingServiceName = "Caller",
        CurrentTestInfoFetcher = () => (testName, testId)
    });

// Later, in cleanup:
channel.Dispose();

Manual: GrpcTrackingChannel.Create()

For more control (e.g. when you need a custom handler chain), use GrpcTrackingChannel.Create() directly. When testing against TestServer, wrap the handler with GrpcResponseVersionHandler to fix the HTTP/1.1 → HTTP/2 version mismatch:

using TestTrackingDiagrams.Extensions.Grpc;

// In your test setup (e.g. constructor, [BeforeScenario], etc.)
var invoker = GrpcTrackingChannel.Create(
    new GrpcResponseVersionHandler(factory.Server.CreateHandler()),
    factory.Server.BaseAddress,
    new GrpcTrackingOptions
    {
        ServiceName = "My API",
        CallingServiceName = "Caller",
        Verbosity = GrpcTrackingVerbosity.Detailed,
        CurrentTestInfoFetcher = () =>
        {
            var test = TestContext.Current.Test;
            return test is not null
                ? (test.TestDisplayName, test.UniqueID)
                : ("Unknown", "unknown");
        }
    });

// Create your typed gRPC client from the tracked invoker
var client = new Greeter.GreeterClient(invoker);

If you need to dispose the underlying GrpcChannel cleanly (e.g. in [AfterScenario]), use CreateWithChannel:

var (invoker, channel) = GrpcTrackingChannel.CreateWithChannel(
    new GrpcResponseVersionHandler(factory.Server.CreateHandler()),
    factory.Server.BaseAddress,
    new GrpcTrackingOptions
    {
        ServiceName = "My API",
        CallingServiceName = "Caller",
        CurrentTestInfoFetcher = () => (TestContext.Current.Test!.TestDisplayName, TestContext.Current.Test!.UniqueID)
    });

var client = new Greeter.GreeterClient(invoker);

// Later, in cleanup:
channel.Dispose();

There's also an extension method on HttpMessageHandler for terser syntax:

var invoker = factory.Server.CreateHandler()
    .AsGrpcTrackingCallInvoker(factory.Server.BaseAddress, grpcOptions);

What you get vs raw HTTP tracking

Aspect Without gRPC extension With GrpcTrackingChannel.Create()
Label POST SayHello or StreamOrders (server-stream)
URI http://localhost/greet.Greeter/SayHello grpc:///greet.Greeter/SayHello
Request body Binary protobuf (unreadable) JSON-formatted protobuf fields
Response body Binary protobuf JSON-formatted protobuf fields
Operation type None UnaryCall, ServerStreamingCall, etc.
Status mapping HTTP/2 status gRPC StatusCode → HTTP mapping
Dependency colour HTTP (blue) gRPC (uses DependencyCategory: "gRPC")

Important: Do not also wrap the underlying HTTP handler with TestTrackingMessageHandler or CreateTestTrackingClient() for gRPC calls — this would produce duplicate diagram entries (one from the HTTP layer, one from the gRPC interceptor). Use GrpcTrackingChannel.Create() instead of, not in addition to, HTTP tracking for the gRPC test client.

Tip: If your test project makes both HTTP and gRPC calls to the SUT, use CreateTestTrackingClient() for the HTTP client and GrpcTrackingChannel.Create() for the gRPC client. They log to the same RequestResponseLogger and will appear together in the same diagram.

How It Works

GrpcTrackingInterceptor extends Grpc.Core.Interceptors.Interceptor, overriding all five call types:

Call Type Override Tracking
Async Unary AsyncUnaryCall Full request + response
Blocking Unary BlockingUnaryCall Full request + response
Server Streaming AsyncServerStreamingCall Request + call initiation
Client Streaming AsyncClientStreamingCall Call initiation + response
Duplex Streaming AsyncDuplexStreamingCall Call initiation + response

Streaming calls are tracked at the initiation level (not per-message) to keep diagrams clean.

Supported Operations

Method Type Enum Value
Unary GrpcOperation.UnaryCall
Server Streaming GrpcOperation.ServerStreamingCall
Client Streaming GrpcOperation.ClientStreamingCall
Duplex Streaming GrpcOperation.DuplexStreamingCall

Message Serialization

Request and response messages are serialized for diagram content:

  • Protobuf IMessage: Uses Google.Protobuf.JsonFormatter.Default.Format() for JSON output
  • Other types: Falls back to .ToString()
  • Summarised verbosity: Content omitted entirely

gRPC Status Code Mapping

gRPC status codes are mapped to HTTP status codes for consistent diagram logging:

gRPC Status HTTP Status
OK 200 OK
NotFound 404 Not Found
PermissionDenied 403 Forbidden
Unauthenticated 401 Unauthorized
InvalidArgument 400 Bad Request
DeadlineExceeded 408 Request Timeout
AlreadyExists 409 Conflict
ResourceExhausted 429 Too Many Requests
Unavailable 503 Service Unavailable
Unimplemented 501 Not Implemented
Cancelled 408 Request Timeout
Other 500 Internal Server Error

Verbosity Levels

GrpcTrackingVerbosity.Summarised

Minimal output:

  • Label: Method name only (e.g. SayHello)
  • URI: grpc:///ServiceName/
  • Content: Omitted
  • Headers: Omitted

GrpcTrackingVerbosity.Detailed

Balanced output:

  • Label: Method name with streaming annotation (e.g. SayHello, StreamMessages (server-stream))
  • URI: grpc:///ServiceName/MethodName
  • Content: Serialized protobuf messages
  • Headers: Omitted

GrpcTrackingVerbosity.Raw

Full output:

  • Label: /package.Service/Method [UnaryCall]
  • URI: grpc:///package.Service/Method
  • Content: Serialized protobuf messages
  • Headers: Included (non-binary metadata entries)

Configuration Options

new GrpcTrackingOptions
{
    // Display name in diagrams (default: "GrpcService")
    ServiceName = "GreeterService",

    // Calling service name in diagrams (default: "Caller")
    CallingServiceName = "MyApi",

    // Verbosity level (default: Detailed)
    Verbosity = GrpcTrackingVerbosity.Detailed,

    // Required: provides test context
    CurrentTestInfoFetcher = () => (testName, testId),

    // Use proto service name instead of ServiceName (default: false)
    UseProtoServiceNameInDiagram = false,

    // Resolves test identity from HTTP context headers when running inside
    // the SUT's request pipeline (default: null). See dual-resolution below.
    HttpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>()
}

GrpcTrackingOptions

Property Type Default Description
ServiceName string "GrpcService" Display name in diagrams for the gRPC service
CallingServiceName string "Caller" Calling service name in diagrams
Verbosity GrpcTrackingVerbosity Detailed Verbosity level (Raw, Detailed, Summarised)
CurrentTestInfoFetcher Func<(string Name, string Id)>? null Required: provides test context
CurrentStepTypeFetcher Func<string?>? null Optional — returns the current BDD step type (Given/When/Then)
HttpContextAccessor IHttpContextAccessor? null Resolves test identity from HTTP context headers when running inside the SUT's request pipeline. See dual-resolution below.
UseProtoServiceNameInDiagram bool false Use proto service name instead of ServiceName
SetupVerbosity GrpcTrackingVerbosity? null Verbosity override for the Setup phase. See Phase-Aware Tracking
ActionVerbosity GrpcTrackingVerbosity? null Verbosity override for the Action phase. See Phase-Aware Tracking
TrackDuringSetup bool true When false, tracking is suppressed during Setup. See Phase-Aware Tracking
TrackDuringAction bool true When false, tracking is suppressed during Action. See Phase-Aware Tracking

UseProtoServiceNameInDiagram

When true, the proto service name from the method definition (e.g. greet.Greeter) is used as the service name in diagrams instead of the configured ServiceName. Useful when tracking multiple gRPC services through a single interceptor.

Dual-Resolution Test Identity (HttpContextAccessor)

When tracking gRPC calls made by the SUT to a downstream gRPC service (SUT-to-downstream direction), the gRPC client runs inside the SUT's request pipeline on a worker thread — not on the test thread. The CurrentTestInfoFetcher delegate typically fails here because test framework execution context (e.g. ScenarioExecutionContext.CurrentScenario) is not available on the SUT's worker thread.

v2.25.2+: Set HttpContextAccessor on GrpcTrackingOptions to enable dual-resolution. The interceptor first checks HTTP request headers (test-tracking-current-test-name / test-tracking-current-test-id) propagated by TestTrackingMessageHandler, then falls back to CurrentTestInfoFetcher:

services.AddSingleton(sp =>
{
    var options = new GrpcTrackingOptions
    {
        ServiceName = "Notification Service (gRPC)",
        CallingServiceName = "MyApi",
        Verbosity = GrpcTrackingVerbosity.Detailed,
        CurrentTestInfoFetcher = () => GetCurrentTestInfo(),
        HttpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>()
    };

    var invoker = new GrpcResponseVersionHandler(testServerHandler)
        .AsGrpcTrackingCallInvoker(new Uri("http://localhost"), options);

    return new NotificationGrpc.NotificationGrpcClient(invoker);
});

Important: Without HttpContextAccessor, gRPC calls made inside the SUT's request pipeline will not appear in per-test reports because CurrentTestInfoFetcher cannot resolve test identity on the SUT's worker thread. The calls will still be intercepted but logged with "Unknown" test identity.

ITrackingComponent

GrpcTrackingInterceptor implements ITrackingComponent and auto-registers with TrackingComponentRegistry:

  • ComponentName: "GrpcTrackingInterceptor ({ServiceName})"
  • WasInvoked: true after first call
  • InvocationCount: Total calls processed (all call types)

URI Scheme

Verbosity Format Example
Summarised grpc:///ServiceName/ grpc:///greet.Greeter/
Detailed grpc:///ServiceName/MethodName grpc:///greet.Greeter/SayHello
Raw grpc:///FullMethodPath grpc:////greet.Greeter/SayHello

GrpcResponseVersionHandler

When testing gRPC services in-process via TestServer (e.g. from WebApplicationFactory), TestServer returns HTTP/1.1 responses, but gRPC requires HTTP/2. Without fixing this, the gRPC client throws RpcException with status Internal.

GrpcResponseVersionHandler is a DelegatingHandler that copies the request version onto the response, resolving the mismatch:

// Wrap TestServer's handler for gRPC compatibility
var handler = new GrpcResponseVersionHandler(factory.Server.CreateHandler());

Note: You do not need to use GrpcResponseVersionHandler directly if you use CreateTestTrackingGrpcClient — it is applied automatically.

Activity Diagrams & Flamecharts

gRPC calls tracked by GrpcTrackingInterceptor automatically appear in Activity Diagrams and Flamecharts. The interceptor:

  1. Creates a System.Diagnostics.Activity span (via ActivitySource("TestTrackingDiagrams.Grpc")) around each gRPC call — for async unary calls, the span covers the full call duration from request to response
  2. Populates ActivityTraceId, ActivitySpanId, and Timestamp on all logged request/response entries
  3. Lazily starts the InternalFlowActivityListener to capture spans into the span store
  4. Injects a traceparent metadata header into outgoing gRPC calls so server-side ASP.NET Core spans share the same TraceId — this connects the client-side gRPC span to the server-side processing span in Activity Diagrams

The "TestTrackingDiagrams.Grpc" and "Grpc.Net.Client" activity sources are included in WellKnownAutoInstrumentationSources, so gRPC spans are captured automatically under the default AutoInstrumentation granularity — no extra configuration required.

No additional configuration is required — if your report includes Activity Diagrams or Flamecharts, gRPC calls will appear alongside HTTP calls automatically.

See Also

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally