C# gRPC拦截器方法 C#如何为gRPC服务添加拦截器

4次阅读

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

C# gRPC拦截器方法 C#如何为gRPC服务添加拦截器

gRPC拦截器在C#中用 ServerInterceptor 实现,不是中间件

gRPC .NET 的服务端拦截器和 ASP.NET Core 中间件完全不同:它不走 HttpContext,也不在请求管道里;而是通过继承 ServerInterceptor 类,在方法调用前后插入逻辑。如果你试图往 Startup.ConfigureProgram.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),必须同时重写对应方法:InterceptClientStreamingAsyncInterceptServerStreamingAsyncInterceptDuplexStreamingAsync,否则这些调用完全绕过你的拦截逻辑。

常见疏忽:

  • 只实现 InterceptUnaryAsync,上线后发现流式接口没打日志、没校验 Token
  • 在流式方法里直接 await continuation(...) 而没包装 IAsyncEnumerable 或处理 IServerStreamWriter,导致响应中断或内存泄漏
  • 想统一处理所有类型?可以提取公共逻辑到私有方法,但四个入口仍需分别调用,无法“一次编写四处生效”

修改请求/响应内容必须用 AsyncUnaryCall 包装

拦截器里不能直接改 requestresponse 参数——它们是只读的。要篡改数据(比如加 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 Task InterceptUnaryAsync(     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 相关或线程绑定资源

实际用的时候,最麻烦的往往不是写拦截器,而是调试——gRPC 错误不显示拦截器帧,ServerCallContext.Status 被设为 CancelledUnknown 时很难定位是哪个拦截器干的。建议每个拦截器开头加 context.RequestHeaders.TryGetValues("x-request-id", out var ids) 并打结构化日志,不然排查成本很高。

text=ZqhQzanResources