/// <inheritdoc />
    public override async Task OnConnectedAsync(ConnectionContext connection)
    {
        // We check to see if HubOptions<THub> are set because those take precedence over global hub options.
        // Then set the keepAlive and handshakeTimeout values to the defaults in HubOptionsSetup when they were explicitly set to null.

        var supportedProtocols = _hubOptions.SupportedProtocols ?? _globalHubOptions.SupportedProtocols;

        if (supportedProtocols == null || supportedProtocols.Count == 0)
        {
            throw new InvalidOperationException("There are no supported protocols");
        }

        var handshakeTimeout = _hubOptions.HandshakeTimeout ?? _globalHubOptions.HandshakeTimeout ?? HubOptionsSetup.DefaultHandshakeTimeout;

        var contextOptions = new HubConnectionContextOptions()
        {
            KeepAliveInterval         = _hubOptions.KeepAliveInterval ?? _globalHubOptions.KeepAliveInterval ?? HubOptionsSetup.DefaultKeepAliveInterval,
            ClientTimeoutInterval     = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval,
            StreamBufferCapacity      = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity,
            MaximumReceiveMessageSize = _maximumMessageSize,
            SystemClock = SystemClock,
            MaximumParallelInvocations = _maxParallelInvokes,
        };

        Log.ConnectedStarting(_logger);

        var connectionContext = new HubConnectionContext(connection, contextOptions, _loggerFactory);

        var resolvedSupportedProtocols = (supportedProtocols as IReadOnlyList <string>) ?? supportedProtocols.ToList();

        if (!await connectionContext.HandshakeAsync(handshakeTimeout, resolvedSupportedProtocols, _protocolResolver, _userIdProvider, _enableDetailedErrors))
        {
            return;
        }

        // -- the connectionContext has been set up --

        try
        {
            await _lifetimeManager.OnConnectedAsync(connectionContext);
            await RunHubAsync(connectionContext);
        }
        finally
        {
            connectionContext.Cleanup();

            Log.ConnectedEnding(_logger);
            await _lifetimeManager.OnDisconnectedAsync(connectionContext);
        }
    }
    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);
    }
    private async Task SendCloseAsync(HubConnectionContext connection, Exception?exception, bool allowReconnect)
    {
        var closeMessage = CloseMessage.Empty;

        if (exception != null)
        {
            var errorMessage = ErrorMessageHelper.BuildErrorMessage("Connection closed with an error.", exception, _enableDetailedErrors);
            closeMessage = new CloseMessage(errorMessage, allowReconnect);
        }
        else if (allowReconnect)
        {
            closeMessage = new CloseMessage(error: null, allowReconnect);
        }

        try
        {
            await connection.WriteAsync(closeMessage, ignoreAbort : true);
        }
        catch (Exception ex)
        {
            Log.ErrorSendingClose(_logger, ex);
        }
    }