gRPC interceptors on .NET
Note
This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. For the current release, see the .NET 8 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 8 version of this article.
Interceptors are a gRPC concept that allows apps to interact with incoming or outgoing gRPC calls. They offer a way to enrich the request processing pipeline.
Interceptors are configured for a channel or service and executed automatically for each gRPC call. Since interceptors are transparent to the user's application logic, they're an excellent solution for common cases, such as logging, monitoring, authentication, and validation.
Interceptor
type
Interceptors can be implemented for both gRPC servers and clients by creating a class that inherits from the Interceptor
type:
public class ExampleInterceptor : Interceptor
{
}
By default, the Interceptor
base class doesn't do anything. Add behavior to an interceptor by overriding the appropriate base class methods in an interceptor implementation.
Client interceptors
gRPC client interceptors intercept outgoing RPC invocations. They provide access to the sent request, the incoming response, and the context for a client-side call.
Interceptor
methods to override for client:
BlockingUnaryCall
: Intercepts a blocking invocation of an unary RPC.AsyncUnaryCall
: Intercepts an asynchronous invocation of an unary RPC.AsyncClientStreamingCall
: Intercepts an asynchronous invocation of a client-streaming RPC.AsyncServerStreamingCall
: Intercepts an asynchronous invocation of a server-streaming RPC.AsyncDuplexStreamingCall
: Intercepts an asynchronous invocation of a bidirectional-streaming RPC.
Warning
Although both BlockingUnaryCall
and AsyncUnaryCall
refer to unary RPCs, they aren't interchangeable. A blocking invocation isn't intercepted by AsyncUnaryCall
, and an asynchronous invocation isn't intercepted by a BlockingUnaryCall
.
Create a client gRPC interceptor
The following code presents a basic example of intercepting an asynchronous invocation of a unary call:
public class ClientLoggingInterceptor : Interceptor
{
private readonly ILogger _logger;
public ClientLoggingInterceptor(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<ClientLoggingInterceptor>();
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
_logger.LogInformation("Starting call. Type/Method: {Type} / {Method}",
context.Method.Type, context.Method.Name);
return continuation(request, context);
}
}
Overriding AsyncUnaryCall
:
- Intercepts an asynchronous unary call.
- Logs details about the call.
- Calls the
continuation
parameter passed into the method. This invokes the next interceptor in the chain or the underlying call invoker if this is the last interceptor.
Methods on Interceptor
for each kind of service method have different signatures. However, the concept behind continuation
and context
parameters remains the same:
continuation
is a delegate which invokes the next interceptor in the chain or the underlying call invoker (if there is no interceptor left in the chain). It isn't an error to call it zero or multiple times. Interceptors aren't required to return a call representation (AsyncUnaryCall
in case of unary RPC) returned from thecontinuation
delegate. Omitting the delegate call and returning your own instance of call representation breaks the interceptors' chain and returns the associated response immediately.context
carries scoped values associated with the client-side call. Usecontext
to pass metadata, such as security principals, credentials, or tracing data. Moreover,context
carries information about deadlines and cancellation. For more information, see Reliable gRPC services with deadlines and cancellation.
Awaiting response in client interceptor
An interceptor can await the response in unary and client streaming calls by updating the AsyncUnaryCall<TResponse>.ResponseAsync
or AsyncClientStreamingCall<TRequest, TResponse>.ResponseAsync
value.
public class ErrorHandlerInterceptor : Interceptor
{
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
{
var call = continuation(request, context);
return new AsyncUnaryCall<TResponse>(
HandleResponse(call.ResponseAsync),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task<TResponse> HandleResponse<TResponse>(Task<TResponse> inner)
{
try
{
return await inner;
}
catch (Exception ex)
{
throw new InvalidOperationException("Custom error", ex);
}
}
}
The preceding code:
- Creates a new interceptor that overrides
AsyncUnaryCall
. - Overriding
AsyncUnaryCall
:- Calls the
continuation
parameter to invoke the next item in the interceptor chain. - Creates a new
AsyncUnaryCall<TResponse>
instance based on the result of the continuation. - Wraps the
ResponseAsync
task using theHandleResponse
method. - Awaits the response with
HandleResponse
. Awaiting the response allows logic to be added after the client received the response. By awaiting the response in a try-catch block, errors from calls can be logged.
- Calls the
For more information on how to create a client interceptor, see the ClientLoggerInterceptor.cs
example in the grpc/grpc-dotnet
GitHub repository.
Configure client interceptors
gRPC client interceptors are configured on a channel.
The following code:
- Creates a channel by using
GrpcChannel.ForAddress
. - Uses the
Intercept
extension method to configure the channel to use the interceptor. Note that this method returns aCallInvoker
. Strongly-typed gRPC clients can be created from an invoker just like a channel. - Creates a client from the invoker. gRPC calls made by the client automatically execute the interceptor.
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var invoker = channel.Intercept(new ClientLoggerInterceptor());
var client = new Greeter.GreeterClient(invoker);
The Intercept
extension method can be chained to configure multiple interceptors for a channel. Alternatively, there is an Intercept
overload that accepts multiple interceptors. Any number of interceptors can be executed for a single gRPC call, as the following example demonstrates:
var invoker = channel
.Intercept(new ClientTokenInterceptor())
.Intercept(new ClientMonitoringInterceptor())
.Intercept(new ClientLoggerInterceptor());
Interceptors are invoked in reverse order of the chained Intercept
extension methods. In the preceding code, interceptors are invoked in the following order:
ClientLoggerInterceptor
ClientMonitoringInterceptor
ClientTokenInterceptor
For information on how to configure interceptors with gRPC client factory, see gRPC client factory integration in .NET.
Server interceptors
gRPC server interceptors intercept incoming RPC requests. They provide access to the incoming request, the outgoing response, and the context for a server-side call.
Interceptor
methods to override for server:
UnaryServerHandler
: Intercepts a unary RPC.ClientStreamingServerHandler
: Intercepts a client-streaming RPC.ServerStreamingServerHandler
: Intercepts a server-streaming RPC.DuplexStreamingServerHandler
: Intercepts a bidirectional-streaming RPC.
Create a server gRPC interceptor
The following code presents an example of an intercepting an incoming unary RPC:
public class ServerLoggerInterceptor : Interceptor
{
private readonly ILogger _logger;
public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation("Starting receiving call. Type/Method: {Type} / {Method}",
MethodType.Unary, context.Method);
try
{
return await continuation(request, context);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error thrown by {context.Method}.");
throw;
}
}
}
Overriding UnaryServerHandler
:
- Intercepts an incoming unary call.
- Logs details about the call.
- Calls the
continuation
parameter passed into the method. This invokes the next interceptor in the chain or the service handler if this is the last interceptor. - Logs any exceptions. Awaiting the continuation allows logic to be added after the service method has executed. By awaiting the continuation in a try-catch block, errors from methods can be logged.
The signature of both client and server interceptors methods are similar:
continuation
stands for a delegate for an incoming RPC calling the next interceptor in the chain or the service handler (if there is no interceptor left in the chain). Similar to client interceptors, you can call it any time and there's no need to return a response directly from the continuation delegate. Outbound logic can be added after a service handler has executed by awaiting the continuation.context
carries metadata associated with the server-side call, such as request metadata, deadlines and cancellation, or RPC result.
For more information on how to create a server interceptor, see the ServerLoggerInterceptor.cs
example in the grpc/grpc-dotnet
GitHub repository.
Configure server interceptors
gRPC server interceptors are configured at startup. The following code:
- Adds gRPC to the app with
AddGrpc
. - Configures
ServerLoggerInterceptor
for all services by adding it to the service option'sInterceptors
collection.
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.Interceptors.Add<ServerLoggerInterceptor>();
});
}
An interceptor can also be configured for a specific service by using AddServiceOptions
and specifying the service type.
public void ConfigureServices(IServiceCollection services)
{
services
.AddGrpc()
.AddServiceOptions<GreeterService>(options =>
{
options.Interceptors.Add<ServerLoggerInterceptor>();
});
}
Interceptors are run in the order that they're added to the InterceptorCollection
. If both global and single service interceptors are configured, then globally-configured interceptors are run before those configured for a single service.
By default, gRPC server interceptors have a per-request lifetime. Overriding this behavior is possible through registering the interceptor type with dependency injection. The following example registers the ServerLoggerInterceptor
with a singleton lifetime:
public void ConfigureServices(IServiceCollection services)
{
services.AddGrpc(options =>
{
options.Interceptors.Add<ServerLoggerInterceptor>();
});
services.AddSingleton<ServerLoggerInterceptor>();
}
gRPC Interceptors versus Middleware
ASP.NET Core middleware offers similar functionalities compared to interceptors in C-core-based gRPC apps. ASP.NET Core middleware and interceptors are conceptually similar. Both:
- Are used to construct a pipeline that handles a gRPC request.
- Allow work to be performed before or after the next component in the pipeline.
- Provide access to
HttpContext
:- In middleware, the
HttpContext
is a parameter. - In interceptors, the
HttpContext
can be accessed using theServerCallContext
parameter with theServerCallContext.GetHttpContext
extension method. This feature is specific to interceptors running in ASP.NET Core.
- In middleware, the
gRPC Interceptor differences from ASP.NET Core Middleware:
- Interceptors:
- Operate on the gRPC layer of abstraction using the
ServerCallContext
. - Provide access to:
- The deserialized message sent to a call.
- The message returned from the call before it's serialized.
- Can catch and handle exceptions thrown from gRPC services.
- Operate on the gRPC layer of abstraction using the
- Middleware:
- Runs for all HTTP requests.
- Runs before gRPC interceptors.
- Operates on the underlying HTTP/2 messages.
- Can only access bytes from the request and response streams.