gRPC服务端拦截器需继承ServerInterceptor并在AddGrpc时显式注册,支持Unary/Streaming四类方法分别拦截,修改请求响应须重构AsyncUnaryCall,Scoped服务需通过IServiceScopeFactory创建作用域。

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










