private async Task HubOnDisconnectedAsync(HubConnectionContext connection, Exception?exception)
    {
        // send close message before aborting the connection
        await SendCloseAsync(connection, exception, connection.AllowReconnect);

        // We wait on abort to complete, this is so that we can guarantee that all callbacks have fired
        // before OnDisconnectedAsync

        // Ensure the connection is aborted before firing disconnect
        await connection.AbortAsync();

        try
        {
            await _dispatcher.OnDisconnectedAsync(connection, exception);
        }
        catch (Exception ex)
        {
            Log.ErrorDispatchingHubEvent(_logger, "OnDisconnectedAsync", ex);
            throw;
        }
    }
    private async Task RunHubAsync(HubConnectionContext connection)
    {
        try
        {
            await _dispatcher.OnConnectedAsync(connection);
        }
        catch (Exception ex)
        {
            Log.ErrorDispatchingHubEvent(_logger, "OnConnectedAsync", ex);

            // The client shouldn't try to reconnect given an error in OnConnected.
            await SendCloseAsync(connection, ex, allowReconnect : false);

            // return instead of throw to let close message send successfully
            return;
        }

        try
        {
            await DispatchMessagesAsync(connection);
        }
        catch (OperationCanceledException)
        {
            // Don't treat OperationCanceledException as an error, it's basically a "control flow"
            // exception to stop things from running
        }
        catch (Exception ex)
        {
            Log.ErrorProcessingRequest(_logger, ex);

            await HubOnDisconnectedAsync(connection, ex);

            // return instead of throw to let close message send successfully
            return;
        }

        await HubOnDisconnectedAsync(connection, connection.CloseException);
    }