gRPC拦截器在C#中用 ServerInterceptor
实现,不是中间件
gRPC .NET 的服务端拦截器和 ASP.NET Core 中间件完全不同:它不走
HttpContext,也不在请求管道里;而是通过继承
ServerInterceptor类,在方法调用前后插入逻辑。如果你试图往
Startup.Configure或
Program.cs的中间件链里加拦截逻辑,会完全失效。
关键点:
ServerInterceptor必须在注册 gRPC 服务时显式传入,例如
AddGrpc().AddServiceOptions<yourservice>(o => o.Interceptors.Add<yourinterceptor>())</yourinterceptor></yourservice>一个服务可添加多个拦截器,执行顺序按
Add顺序(先注册的先执行
UnaryServerHandler前置逻辑) 拦截器实例默认是单例(
Singleton生命周期),不能直接注入
Scoped服务(如
DbContext),需通过
IServiceScopeFactory手动创建作用域
InterceptUnaryAsync
是最常用入口,但别漏掉流式方法
大多数日志、鉴权、指标场景都从
InterceptUnaryAsync开始,但它只覆盖 unary(一元)调用。如果你的服务用了
stream(server-streaming、client-streaming、bidi-streaming),必须同时重写对应方法:
InterceptClientStreamingAsync、
InterceptServerStreamingAsync、
InterceptDuplexStreamingAsync,否则这些调用完全绕过你的拦截逻辑。
常见疏忽:
只实现InterceptUnaryAsync,上线后发现流式接口没打日志、没校验 token 在流式方法里直接 await
continuation(...)而没包装
IAsyncEnumerable<t></t>或处理
IServerStreamWriter<t></t>,导致响应中断或内存泄漏 想统一处理所有类型?可以提取公共逻辑到私有方法,但四个入口仍需分别调用,无法“一次编写四处生效”
修改请求/响应内容必须用 AsyncUnaryCall<tresponse></tresponse>
包装
拦截器里不能直接改
request或
response参数——它们是只读的。要篡改数据(比如加 trace-id 到响应头、脱敏请求字段),得自己构造新的
AsyncUnaryCall<tresponse></tresponse>并返回。这一步最容易出错: 调用
continuation(...)后拿到原始
AsyncUnaryCall<tresponse></tresponse>,再用
new AsyncUnaryCall<t>(responseTask, ...)</t>封装,其中
responseTask需要 await + 修改后再 return Task.FromResult(...) 如果只是读取 header(如
context.RequestHeaders),没问题;但写 header 必须在
continuation调用前用
context.ResponseTrailers.Add(...),或在
continuation返回后通过
call.ResponseHeadersAsync获取并修改(注意时机) 别在拦截器里 throw 异常后还调用
continuation,会导致重复响应或状态码冲突;应提前 return 新建的失败
AsyncUnaryCall
拦截器里访问 DI 容器要小心生命周期和线程上下文
拦截器本身是 Singleton,但 gRPC 调用是并发的,每个调用都有独立的
ServerCallContext。如果你需要
Scoped服务(比如
IHttpContextAccessor、数据库上下文),不能直接构造函数注入,而要用
IServiceScopeFactory:
private readonly IServiceScopeFactory _scopeFactory;
public MyInterceptor(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public override async Task<TResponse> InterceptUnaryAsync<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// ...
}
注意:
不要把scope.ServiceProvider存为字段——它不是线程安全的
ServerCallContext不包含
HttpContext,所以
IHttpContextAccessor在纯 gRPC 拦截器里始终为 null(除非你启用了
Grpc.AspNetCore.Server.ClientFactory并显式桥接) 异步方法里 await 的地方可能切换线程,避免在拦截器里操作 UI 相关或线程绑定资源 实际用的时候,最麻烦的往往不是写拦截器,而是调试——gRPC 错误堆栈不显示拦截器帧,
ServerCallContext.Status被设为
Cancelled或
Unknown时很难定位是哪个拦截器干的。建议每个拦截器开头加
context.RequestHeaders.TryGetValues("x-request-id", out var ids) 并打结构化日志,不然排查成本很高。 