@@ -28,17 +28,12 @@ public abstract class WebsocketBaseController<T> : OpenShockControllerBase, IAsy
2828 /// </summary>
2929 protected readonly ILogger < WebsocketBaseController < T > > Logger ;
3030
31- /// <summary>
32- /// Close cancellation token to be called manually when termination of the current websocket is requested. Called on Dispose as well.
33- /// </summary>
34- protected readonly CancellationTokenSource Close = new ( ) ;
35-
3631 /// <summary>
3732 /// When passing a cancellation token, pass this Linked token, it is a Link from ApplicationStopping and Close.
3833 /// </summary>
39- protected readonly CancellationTokenSource LinkedSource ;
34+ private CancellationTokenSource ? _linkedSource ;
4035
41- protected readonly CancellationToken LinkedToken ;
36+ protected CancellationToken LinkedToken ;
4237
4338 /// <summary>
4439 /// Channel for multithreading thread safety of the websocket, MessageLoop is the only reader for this channel
@@ -53,12 +48,9 @@ public abstract class WebsocketBaseController<T> : OpenShockControllerBase, IAsy
5348 /// DI
5449 /// </summary>
5550 /// <param name="logger"></param>
56- /// <param name="lifetime"></param>
57- public WebsocketBaseController ( ILogger < WebsocketBaseController < T > > logger , IHostApplicationLifetime lifetime )
51+ protected WebsocketBaseController ( ILogger < WebsocketBaseController < T > > logger )
5852 {
5953 Logger = logger ;
60- LinkedSource = CancellationTokenSource . CreateLinkedTokenSource ( Close . Token , lifetime . ApplicationStopping ) ;
61- LinkedToken = LinkedSource . Token ;
6254 }
6355
6456
@@ -89,10 +81,9 @@ public virtual async ValueTask DisposeAsync()
8981 await UnregisterConnection ( ) ;
9082
9183 Channel . Writer . TryComplete ( ) ;
92- await Close . CancelAsync ( ) ;
9384
9485 WebSocket ? . Dispose ( ) ;
95- LinkedSource . Dispose ( ) ;
86+ _linkedSource ? . Dispose ( ) ;
9687
9788 GC . SuppressFinalize ( this ) ;
9889 Logger . LogTrace ( "Disposed websocket controller" ) ;
@@ -110,18 +101,25 @@ public virtual async ValueTask DisposeAsync()
110101 /// </summary>
111102 [ ApiExplorerSettings ( IgnoreApi = true ) ]
112103 [ HttpGet ]
113- public async Task Get ( )
104+ public async Task Get ( [ FromServices ] IHostApplicationLifetime lifetime , CancellationToken cancellationToken )
114105 {
106+ #pragma warning disable IDISP003
107+ _linkedSource = CancellationTokenSource . CreateLinkedTokenSource ( lifetime . ApplicationStopping , cancellationToken ) ;
108+ #pragma warning restore IDISP003
109+ LinkedToken = _linkedSource . Token ;
110+
115111 if ( ! HttpContext . WebSockets . IsWebSocketRequest )
116112 {
117113 var jsonOptions = HttpContext . RequestServices . GetRequiredService < IOptions < JsonOptions > > ( ) ;
118114 HttpContext . Response . StatusCode = StatusCodes . Status400BadRequest ;
119115 var response = WebsocketError . NonWebsocketRequest ;
120116 response . AddContext ( HttpContext ) ;
121117 // ReSharper disable once MethodSupportsCancellation
122- await HttpContext . Response . WriteAsJsonAsync ( response , jsonOptions . Value . SerializerOptions ,
123- contentType : MediaTypeNames . Application . ProblemJson ) ;
124- await Close . CancelAsync ( ) ;
118+ await HttpContext . Response . WriteAsJsonAsync (
119+ response ,
120+ jsonOptions . Value . SerializerOptions ,
121+ contentType : MediaTypeNames . Application . ProblemJson ,
122+ cancellationToken : cancellationToken ) ;
125123 return ;
126124 }
127125
@@ -133,16 +131,19 @@ await HttpContext.Response.WriteAsJsonAsync(response, jsonOptions.Value.Serializ
133131 HttpContext . Response . StatusCode = response . Status ?? StatusCodes . Status400BadRequest ;
134132 response . AddContext ( HttpContext ) ;
135133 // ReSharper disable once MethodSupportsCancellation
136- await HttpContext . Response . WriteAsJsonAsync ( response , jsonOptions . Value . SerializerOptions ,
137- contentType : MediaTypeNames . Application . ProblemJson ) ;
138-
139- await Close . CancelAsync ( ) ;
134+ await HttpContext . Response . WriteAsJsonAsync (
135+ response ,
136+ jsonOptions . Value . SerializerOptions ,
137+ contentType : MediaTypeNames . Application . ProblemJson ,
138+ cancellationToken : cancellationToken ) ;
140139 return ;
141140 }
142141
143142 Logger . LogInformation ( "Opening websocket connection" ) ;
144- WebSocket ? . Dispose ( ) ; // This should never happen, suppresses warning
143+
144+ #pragma warning disable IDISP003
145145 WebSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) ;
146+ #pragma warning restore IDISP003
146147
147148#pragma warning disable CS4014
148149 OsTask . Run ( MessageLoop ) ;
@@ -154,8 +155,14 @@ await HttpContext.Response.WriteAsJsonAsync(response, jsonOptions.Value.Serializ
154155 // Logic ended
155156
156157 await UnregisterConnection ( ) ;
157-
158- await Close . CancelAsync ( ) ;
158+
159+ // Only send close if the socket is still open, this allows us to close the websocket from inside the logic
160+ // We send close if the client sent a close message though
161+ if ( WebSocket is { State : WebSocketState . Open or WebSocketState . CloseReceived } )
162+ {
163+ await WebSocket . CloseOutputAsync ( WebSocketCloseStatus . NormalClosure , "Normal closure" ,
164+ LinkedToken ) ;
165+ }
159166 }
160167
161168 #region Send Loop
@@ -198,7 +205,54 @@ protected virtual Task SendWebSocketMessage(T message, WebSocket websocket, Canc
198205 /// </summary>
199206 /// <returns></returns>
200207 [ NonAction ]
201- protected abstract Task Logic ( ) ;
208+ private async Task Logic ( )
209+ {
210+ while ( ! LinkedToken . IsCancellationRequested )
211+ {
212+ try
213+ {
214+ if ( WebSocket == null )
215+ {
216+ Logger . LogWarning ( "WebSocket is null, aborting" ) ;
217+ return ;
218+ }
219+
220+ if ( WebSocket . State is WebSocketState . CloseReceived or WebSocketState . CloseSent or WebSocketState . Closed )
221+ {
222+ // Client or we sent close message or both, we will close the connection after this
223+ return ;
224+ }
225+
226+ if ( WebSocket ! . State != WebSocketState . Open )
227+ {
228+ Logger . LogWarning ( "WebSocket is not open [{State}], aborting" , WebSocket . State ) ;
229+ WebSocket ? . Abort ( ) ;
230+ return ;
231+ }
232+
233+ await HandleReceive ( ) ;
234+
235+ }
236+ catch ( OperationCanceledException )
237+ {
238+ Logger . LogWarning ( "WebSocket connection terminated due to close or shutdown" ) ;
239+ return ;
240+ }
241+ catch ( Exception ex )
242+ {
243+ Logger . LogError ( ex , "Exception while processing websocket request" ) ;
244+ WebSocket ? . Abort ( ) ;
245+ return ;
246+ }
247+ }
248+ }
249+
250+ /// <summary>
251+ ///
252+ /// </summary>
253+ /// <returns>True if you want to continue the receiver loop, false if you want to terminate</returns>
254+ [ NonAction ]
255+ protected abstract Task < bool > HandleReceive ( ) ;
202256
203257 /// <summary>
204258 /// Send initial data to the client
0 commit comments