{"record":{"id":"6beaff899e28a56f","repo":"JeffreySu/WeiXinMPSDK","slug":"smartrobotwebsocketclient-6beaff","errorCode":null,"errorMessage":"SmartRobotWebSocketClient","messagePattern":"SmartRobotWebSocketClient","errorType":"exception","errorClass":"ObjectDisposedException","httpStatus":null,"severity":"error","filePath":"src/Senparc.Weixin.Work/Senparc.Weixin.Work/AdvancedAPIs/SmartRobot/SmartRobotWebSocketClient.cs","lineNumber":267,"sourceCode":"\n        /// <summary>\n        /// 创建智能机器人 WebSocket 请求 ID。\n        /// </summary>\n        /// <returns>32 位无连字符 GUID 请求 ID。</returns>\n        public static string CreateRequestId() => Guid.NewGuid().ToString(\"N\");\n\n        private static ClientWebSocket CreateSocket()\n        {\n            var socket = new ClientWebSocket();\n            socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);\n            return socket;\n        }\n\n        private void ThrowIfDisposed()\n        {\n            if (_disposed)\n            {\n                throw new ObjectDisposedException(nameof(SmartRobotWebSocketClient));\n            }\n        }\n\n        /// <summary>\n        /// 释放 WebSocket 连接和发送锁。\n        /// </summary>\n        public void Dispose()\n        {\n            if (_disposed) return;\n            _disposed = true;\n            _socket.Dispose();\n            _sendLock.Dispose();\n        }\n\n        /// <summary>\n        /// 异步释放 WebSocket 连接和发送锁。\n        /// </summary>\n        /// <returns>表示释放操作的任务。</returns>","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/JeffreySu/WeiXinMPSDK/blob/be573f6f94bdbf718dd5f6cdecb137fbc7ff651e/src/Senparc.Weixin.Work/Senparc.Weixin.Work/AdvancedAPIs/SmartRobot/SmartRobotWebSocketClient.cs#L249-L285","documentation":"ThrowIfDisposed throws an ObjectDisposedException naming SmartRobotWebSocketClient when the client has already been disposed (Dispose/DisposeAsync called, _disposed=true). ConnectAndSubscribeAsync and ReceiveLoopAsync call it, so any attempt to connect or keep receiving after disposal fails. It guards the WebSocket and the send semaphore, which are destroyed in Dispose.","triggerScenarios":"Calling ConnectAndSubscribeAsync after Dispose()/DisposeAsync(); the ReceiveLoopAsync running in background while the client is disposed elsewhere (e.g. host shutdown) and it touches the disposed instance.","commonSituations":"ASP.NET Core DI container disposing a singleton/client at shutdown while its receive loop is still running; using statements that dispose the client too early while a background loop holds a reference; restart logic that disposes then accidentally reuses the old instance instead of creating a new one.","solutions":["Never reuse a disposed client: after Dispose, create a new SmartRobotWebSocketClient instance.","Coordinate shutdown: await/cancel the receive loop task before disposing the client.","Guard call sites with a lifetime check (e.g. try/catch ObjectDisposedException or a _disposed-aware wrapper).","In DI scenarios, register with the correct lifetime and implement IHostedService so disposal happens after the loop stops."],"exampleFix":"// before\nclient.Dispose();\nawait client.ConnectAndSubscribeAsync(); // ObjectDisposedException\n\n// after\nawait receiveLoopTask; // stop the loop first\nclient.Dispose();\nclient = new SmartRobotWebSocketClient(url, token);\nawait client.ConnectAndSubscribeAsync();","handlingStrategy":"type-guard","validationCode":"// track disposal yourself\nprivate bool _clientClosed;\n// before reuse\nif (_clientClosed) { _client = new SmartRobotWebSocketClient(url, token); }","typeGuard":"bool IsUsable(SmartRobotWebSocketClient c) => c != null && !c.IsDisposed; // expose if possible; otherwise track via wrapper","tryCatchPattern":"try { await client.ConnectAndSubscribeAsync(); }\ncatch (ObjectDisposedException)\n{\n    client = new SmartRobotWebSocketClient(url, token);\n    await client.ConnectAndSubscribeAsync();\n}","preventionTips":["Cancel the receive loop task before calling Dispose","Never use a client after Dispose; treat it as one-shot per connection","In DI, use IHostedService to order shutdown: stop loop, then dispose","Wrap the client in an owner class that serializes connect/dispose calls"],"tags":["object-disposed","websocket","lifetime","smart-robot"],"backgroundTag":"invalid-state-transition","analyzedSha":"be573f6f94bdbf718dd5f6cdecb137fbc7ff651e","analyzedAt":"2026-09-12T10:01:50.733Z","contentChangedAt":"2026-09-12T10:01:50.733Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}