graphql订阅在c#中基于websocket长连接实现,需显式启用usewebsockets()、配置反向代理、返回iasyncenumerable或eventstream、匹配协议(graphql-ws或升级至v13+支持双协议),并监听诊断事件捕获错误。

GraphQL订阅在C#里不是靠轮询实现的
HotChocolate 的 Subscription 本质是基于 WebSocket 的长连接,不是 HTTP 请求重试或 SignalR 封装。如果你在 Startup 中只配了 AddGraphQLServer() 却没启用 WebSocket 支持,subscription 字段会直接返回 Null 或抛出 NotSupportedException,前端连连接都建不起来。
必须显式启用 WebSocket 中间件,且顺序不能错:
-
app.UseWebSockets()要放在UseRouting()之后、UseEndpoints()之前 -
endpoints.MapGraphQL()本身不自动开启 WebSocket —— 它只响应POST /graphql,而 subscription 走的是GET /graphql升级为 WebSocket - 若用了反向代理(如 Nginx),需额外配置
proxy_http_version 1.1和Upgrade $http_upgrade,否则握手失败,浏览器控制台报WebSocket connection to 'ws://...' failed
定义订阅类型时别漏掉 EventStream<t></t>
HotChocolate v12+ 强制要求订阅 resolver 返回 IAsyncEnumerable<t></t> 或 EventStream<t></t>。直接 return 一个普通对象、Task<t></t> 或 IObservable<t></t> 都会静默失败——字段不报错,但客户端收不到任何数据。
典型写法是:
public async IAsyncEnumerable<messagetype> OnMessageReceived(
[Service] IMessagePublisher publisher,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var msg in publisher.Listen(ct))
{
yield return msg;
}
}</messagetype>
注意点:
-
[EnumeratorCancellation]是必须的,否则客户端断连时 CancellationToken 不触发,后台任务持续占用资源 - 不要在 resolver 里 new
ChannelReader或手动管理Task.Run,HotChocolate 已内置背压和生命周期绑定 - 如果用
EventStream<t></t>,需注册AddEventStreaming(),否则运行时报Unable to resolve service for type 'IEventStream'...
客户端发 subscription 请求前得先确认协议支持
HotChocolate 默认只接受 graphql-ws 协议(Apollo Server 旧版),不兼容 graphql-transport-ws(Apollo Client 3.8+ 默认)。你看到前端报 connection_init timeout 或后端日志出现 Unknown protocol: graphql-transport-ws,基本就是这个原因。
解决方式只有两个:
- 客户端降级:Apollo Client 配
graphqlWs协议(需装@graphql-ws/client) - 服务端升级:HotChocolate v13+ 支持双协议,但需显式调用
AddWebSocketTransports().AddApolloFederation()并启用EnableWebSocketTransport() - 别信文档里“自动适配”的说法——v12.16.1 及以前版本对
graphql-transport-ws的支持是实验性的,连complete消息都可能丢
调试订阅卡住时优先查 ExecutionResult 的 Errors 字段
订阅 resolver 抛异常不会像 query/mutation 那样返回标准 GraphQL 错误体。它常表现为客户端无响应、连接秒断、或收到空的 next 帧。真正的问题藏在服务器端的 ExecutionResult.Errors 里,但默认不打印到日志。
建议加一层诊断 wrapper:
services.AddGraphQLServer()
.AddQueryType<query>()
.AddMutationType<mutation>()
.AddSubscriptionType<subscription>()
.AddErrorFilter(_ => null) // 先清空默认过滤器
.AddDiagnosticEventListener(sp => new SubscriptionErrorLogger(sp.GetRequiredService<ilogger>>()));</ilogger></subscription></mutation></query>
其中 SubscriptionErrorLogger 要监听 OnSubscriptionExecutionError 事件,否则你永远不知道 publisher.Listen() 里 throw 了个 NullReferenceException。
另外,EventStream 内部异常不会传播到 resolver 外层,所以 try/catch 包不住——必须靠 HotChocolate 的诊断事件捞。











