@@ -150,6 +150,80 @@ public void GenericServerEventSubscriptionLifetime()
150150 Assert . Null ( this . server . ServerEventWithCustomArgsAccessor ) ;
151151 }
152152
153+ /// <summary>
154+ /// Verifies that event handlers are unregistered when multiple connections are established and disposed.
155+ /// This tests for a memory leak where event handlers would accumulate if not properly unregistered on disposal.
156+ /// </summary>
157+ [ Fact ]
158+ public void EventHandlersUnregisteredOnMultipleConnectionDisposals ( )
159+ {
160+ // Use a shared server object across multiple connections to simulate the real-world scenario
161+ var sharedServer = new Server ( ) ;
162+
163+ // Verify no handlers initially
164+ Assert . Null ( sharedServer . ServerEventAccessor ) ;
165+ Assert . Null ( sharedServer . ServerEventWithCustomArgsAccessor ) ;
166+
167+ // Create and dispose multiple connections
168+ for ( int i = 0 ; i < 3 ; i ++ )
169+ {
170+ var streams = FullDuplexStream . CreateStreams ( ) ;
171+ var rpc = this . CreateJsonRpcWithTargetObject ( streams . Item1 , sharedServer ) ;
172+ rpc . StartListening ( ) ;
173+
174+ // Verify handler is registered
175+ Assert . NotNull ( sharedServer . ServerEventAccessor ) ;
176+ Assert . NotNull ( sharedServer . ServerEventWithCustomArgsAccessor ) ;
177+
178+ // Count the number of handlers attached
179+ int serverEventHandlerCount = sharedServer . ServerEventAccessor ? . GetInvocationList ( ) . Length ?? 0 ;
180+ int customArgsEventHandlerCount = sharedServer . ServerEventWithCustomArgsAccessor ? . GetInvocationList ( ) . Length ?? 0 ;
181+
182+ // Should only have one handler per event, not accumulating
183+ Assert . Equal ( 1 , serverEventHandlerCount ) ;
184+ Assert . Equal ( 1 , customArgsEventHandlerCount ) ;
185+
186+ // Dispose the connection
187+ rpc . Dispose ( ) ;
188+
189+ // Verify handlers are unregistered after disposal
190+ Assert . Null ( sharedServer . ServerEventAccessor ) ;
191+ Assert . Null ( sharedServer . ServerEventWithCustomArgsAccessor ) ;
192+ }
193+ }
194+
195+ /// <summary>
196+ /// Verifies that event handlers are unregistered when the stream is closed without explicit disposal.
197+ /// This simulates the scenario where a websocket connection drops unexpectedly.
198+ /// </summary>
199+ [ Fact ]
200+ public async Task EventHandlersUnregisteredWhenStreamClosesUnexpectedly ( )
201+ {
202+ var sharedServer = new Server ( ) ;
203+
204+ // Verify no handlers initially
205+ Assert . Null ( sharedServer . ServerEventAccessor ) ;
206+
207+ var streams = FullDuplexStream . CreateStreams ( ) ;
208+ var serverRpc = this . CreateJsonRpcWithTargetObject ( streams . Item1 , sharedServer ) ;
209+ var clientRpc = new JsonRpc ( streams . Item2 ) ;
210+
211+ serverRpc . StartListening ( ) ;
212+ clientRpc . StartListening ( ) ;
213+
214+ // Verify handler is registered
215+ Assert . NotNull ( sharedServer . ServerEventAccessor ) ;
216+
217+ // Simulate connection drop by closing the stream without disposing JsonRpc
218+ streams . Item2 . Dispose ( ) ;
219+
220+ // Wait for the disconnection to be detected
221+ await serverRpc . Completion . WithCancellation ( this . TimeoutToken ) ;
222+
223+ // Verify handlers are unregistered after stream closure
224+ Assert . Null ( sharedServer . ServerEventAccessor ) ;
225+ }
226+
153227 /// <summary>Ensures that JsonRpc only adds one event handler to target objects where events are declared multiple times in an type hierarchy.</summary>
154228 /// <remarks>This is a regression test for <see href="https://github.com/microsoft/vs-streamjsonrpc/issues/481">this bug</see>.</remarks>
155229 [ Fact ]
0 commit comments