Table of Contents

Class Connection

Namespace
WebDriverBiDi.Protocol
Assembly
WebDriverBiDi.dll

Represents a connection to a WebDriver BiDi remote end.

public abstract class Connection : IAsyncDisposable
Inheritance
Connection
Implements
Derived
Inherited Members

Remarks

The Connection class is an abstract base class that defines the contract for transport-layer communication with a browser. It is wrapped by the Transport class, which handles protocol-level concerns like JSON serialization and command/response correlation.

Most users will never need to interact with Connection objects directly. The BiDiDriver class manages connections automatically. Custom connection implementations are only needed for specialized transport mechanisms.

Available implementations:

  • WebSocketConnectionStandard WebSocket transport (recommended for all scenarios)
  • PipeConnectionAnonymous pipes transport (specialized for high-performance local Chromium automation)

StartAsync(string, CancellationToken), StopAsync(CancellationToken) and DisposeAsync() are implemented by this class and are not overridable: the sequence each performs is the same for every transport, and several of its steps are only correct in a particular order. A derived class supplies the transport-specific parts of them:

IsActive is implemented by this class as well. A connection is active only while it is open and its receive loop has not reported that it ended. A receive loop reports its end by calling NotifyRemoteDisconnectedObserversAsync() or NotifyConnectionErrorObserversAsync(string, Exception).

A derived class raises this class's events only through the methods that keep each event consistent with the state of the connection: NotifyDataReceivedObserverAsync(MessageBuffer), or its overload taking pooled memory directly, for OnDataReceived; LogAsync(string, WebDriverBiDiLogLevel) for OnLogMessage; and the two notification methods above for OnRemoteDisconnected and OnConnectionError. The events are not exposed for raising in any other way, so no implementation can deliver a message without transferring its memory, log a message that LogLevel excludes, or report the end of its receive loop and leave the connection active.

Thread safety: Connection implementations use internal synchronization to ensure thread-safe operation. Multiple threads can safely call SendDataAsync(ReadOnlyMemory<byte>, CancellationToken) concurrently. The Transport class that wraps connections provides additional synchronization for StartAsync(string, CancellationToken) and StopAsync(CancellationToken) operations.

Constructors

Connection()

Initializes a new instance of the Connection class.

protected Connection()

Remarks

The cached ConnectionCancellationToken is taken here rather than only when a session starts, so that it always belongs to the connection's current CancellationTokenSource. Left unassigned until the first StartAsync(string, CancellationToken), it would be the default token, which can never be canceled, and the cancellation StopAsync(CancellationToken) performs on a connection that was never started would be requested on a source nothing is watching.

The connection starts with a reporter for failures of observers of its events that records each failure as the EventHandlerError(string, string, string, string) event; see WebDriverBiDi.Protocol.Connection.SetObserverErrorReporter(System.Func{WebDriverBiDi.EventObserverErrorInfo,System.Threading.Tasks.Task}). A failing observer therefore never fails a connection operation, even for a connection used without a Transport.

Fields

LogReceiveMessagePrefix

The prefix for logging message content during a receive operation.

protected const string LogReceiveMessagePrefix = "RECV <<< "

Field Value

string

LogSendMessagePrefix

The prefix for logging message content during a send operation.

protected const string LogSendMessagePrefix = "SEND >>> "

Field Value

string

LoggerComponentName

Gets the component name for this class to use in log messages.

public const string LoggerComponentName = "Connection"

Field Value

string

Properties

BufferSize

Gets the buffer size for communication used by this connection.

public int BufferSize { get; }

Property Value

int

ConnectionCancellationToken

Gets the CancellationToken used to cancel the operations of this connection.

protected CancellationToken ConnectionCancellationToken { get; }

Property Value

CancellationToken

Remarks

This is a cached copy of the token of the connection's CancellationTokenSource, taken while that source is known to be alive. A CancellationToken obtained beforehand remains safe to use after its source is disposed, whereas reading the source's Token property after disposal throws ObjectDisposedException. Both the background receive loop and an in-flight send can still be running when the connection is disposed, so they must use this property rather than the source directly.

ConnectionKind

Gets a value indicating the kind of data transport used by this connection.

public abstract ConnectionKind ConnectionKind { get; }

Property Value

ConnectionKind

ConnectionString

Gets or sets the string naming the remote end this connection is connected to. For a WebSocketConnection this is the WebSocket URL. A PipeConnection does not interpret the value at all -- the anonymous pipes it uses are its own -- so the string only labels the session there.

public string ConnectionString { get; protected set; }

Property Value

string

DataReceiveTask

Gets the Task object representing the method that receives data from the connection.

protected Task? DataReceiveTask { get; }

Property Value

Task

DataSendSemaphore

Gets a SemaphoreSlim to serialize sending data across the connection, ensuring sending data to be an atomic action.

protected SemaphoreSlim DataSendSemaphore { get; }

Property Value

SemaphoreSlim

DataTimeout

Gets or sets the value of the timeout to wait for exclusive access when sending data over the connection. It bounds only that wait, not the send itself and not any receive.

public TimeSpan DataTimeout { get; set; }

Property Value

TimeSpan

Exceptions

ArgumentOutOfRangeException

Thrown when the value is negative (other than InfiniteTimeSpan) or exceeds the maximum timer duration supported by the runtime.

Id

Gets the ID of this Connection.

public string Id { get; }

Property Value

string

IsActive

Gets a value indicating whether this connection is active: open, and still read by its receive loop.

public bool IsActive { get; }

Property Value

bool

Remarks

A connection is active when IsConnectionOpen reports that the transport-specific connection is open and its receive loop has not reported that it ended, by calling NotifyRemoteDisconnectedObserversAsync() or NotifyConnectionErrorObserversAsync(string, Exception). The receive loop is the only reader a connection has, so a connection whose loop has ended cannot receive the response to anything sent on it, even while the connection itself is still open. Such a connection reports itself inactive from the moment its loop reports the end, before any observer is notified, until the next StartAsync(string, CancellationToken) starts a new loop.

This matters most to ConnectAsync(string, CancellationToken), which adopts an active connection rather than starting it again. A connection that went on reporting itself active after its loop ended would be adopted by a reconnect, and no command sent on the new session would ever be answered.

IsConnectionOpen

Gets a value indicating whether the transport-specific connection is open.

protected abstract bool IsConnectionOpen { get; }

Property Value

bool

Remarks

This is the transport-specific part of IsActive, which also requires that the receive loop has not reported that it ended. An implementation reports only whether the underlying channel is open, and need not account for the receive loop at all.

DisposeAsync() stops a connection for which this reports true, even when the connection is inactive because its receive loop has ended, so that an open channel is always shut down before its resources are released.

IsDisposed

Gets a value indicating whether this connection has been disposed.

protected bool IsDisposed { get; }

Property Value

bool

LogLevel

Gets or sets the minimum WebDriverBiDiLogLevel at which this connection raises OnLogMessage. Messages below this level are never built or raised. Defaults to Info.

public WebDriverBiDiLogLevel LogLevel { get; set; }

Property Value

WebDriverBiDiLogLevel

Remarks

This is the single setting for the whole log pipeline: LogLevel and LogLevel, reached from TransportConfiguration, read and write this property, so setting it on any of the three sets it for all of them.

The default excludes the two most voluminous levels. Every message this connection sends and receives is logged at Trace, and each such message is decoded from UTF-8 into a string only when that level is enabled, so raising the level to Trace to inspect protocol traffic also opts in to that cost. Off suppresses every message, including Fatal; it is only meaningful here, and is never the level of a message that is raised.

OnConnectionError

Gets an observable event that notifies when a communication error occurs on this connection.

public ObservableEvent<ConnectionErrorEventArgs> OnConnectionError { get; }

Property Value

ObservableEvent<ConnectionErrorEventArgs>

OnDataReceived

Gets an observable event that notifies when data is received from this connection.

public ObservableEvent<ConnectionDataReceivedEventArgs> OnDataReceived { get; }

Property Value

ObservableEvent<ConnectionDataReceivedEventArgs>

Remarks

Due to the shared-memory nature of the data received, one, and only one, EventObserver<T> can be observing this event at a time. Attempting to connect a second observer will throw an exception.

OnLogMessage

Gets an observable event that notifies when a log message is written.

public ObservableEvent<LogMessageEventArgs> OnLogMessage { get; }

Property Value

ObservableEvent<LogMessageEventArgs>

OnRemoteDisconnected

Gets an observable event that notifies when the remote end gracefully closes this connection.

public ObservableEvent<ConnectionDisconnectedEventArgs> OnRemoteDisconnected { get; }

Property Value

ObservableEvent<ConnectionDisconnectedEventArgs>

ShutdownTimeout

Gets or sets the value of the timeout that bounds shutting the connection down.

public TimeSpan ShutdownTimeout { get; set; }

Property Value

TimeSpan

Remarks

It bounds the transport's own close of the connection, such as a WebSocket close handshake, and it separately bounds the wait for the receive loop to finish. That wait happens at both ends of a session: StopAsync(CancellationToken) waits for the loop it is ending, and StartAsync(string, CancellationToken) waits for a loop a previous StopAsync(CancellationToken) had to abandon, because a second loop must not run alongside it.

A wait for the receive loop that is not satisfied does not throw. It raises a Warn message on OnLogMessage and proceeds: StopAsync(CancellationToken) returns with the loop left running in the background, and StartAsync(string, CancellationToken) refuses to begin a new session while it is.

Exceptions

ArgumentOutOfRangeException

Thrown when the value is negative (other than InfiniteTimeSpan) or exceeds the maximum timer duration supported by the runtime.

StartupTimeout

Gets or sets the value of the timeout to wait before throwing an error when starting up the connection.

public TimeSpan StartupTimeout { get; set; }

Property Value

TimeSpan

Remarks

The timeout is a single budget for the whole of startup: it bounds each individual connection attempt as well as the retries between them, so a remote end that accepts slowly is cut off at the deadline rather than allowed to complete late. Name resolution and address fallback (for example localhost resolving to an IPv6 address first) count against the budget, so avoid sub-second values when connecting by host name.

Because the startup budget is computed by subtracting elapsed time, it must be a finite value; InfiniteTimeSpan is not permitted.

Exceptions

ArgumentOutOfRangeException

Thrown when the value is negative, is InfiniteTimeSpan, or exceeds the maximum timer duration supported by the runtime.

TimeProvider

Gets or sets the TimeProvider whose clock measures this connection's timeouts: StartupTimeout, ShutdownTimeout and DataTimeout. Defaults to System. A derived type may substitute another, for example to drive the timeouts with virtual time in a test, in the same way that ObservableEvent<T> exposes its provider to derived types.

protected TimeProvider TimeProvider { get; set; }

Property Value

TimeProvider

Methods

DisposeAsync()

Asynchronously releases the resources used by this Connection.

public ValueTask DisposeAsync()

Returns

ValueTask

A task that represents the asynchronous dispose operation.

Remarks

A connection that is still open is stopped before its resources are released, even when its receive loop has already ended, so that the remote end sees the shutdown the transport defines rather than the connection simply disappearing. A connection that is no longer open has no shutdown to perform, but if its receive loop is still running -- the state a pipe reports once its server process has exited -- the session is canceled and the loop waited for, so that DisposeAsyncCore() does not release the underlying pipes or socket while the loop is still reading from them. A failure to stop is logged and does not prevent disposal, because disposal must release the connection's resources whatever state it is in. A derived class releases its own resources by implementing DisposeAsyncCore(); it neither performs the stop nor records the disposal itself, both of which happen here.

DisposeAsyncCore()

Asynchronously releases the resources held by this Connection.

protected abstract ValueTask DisposeAsyncCore()

Returns

ValueTask

A task that represents the asynchronous dispose operation.

Remarks

This is called once, from DisposeAsync(), after an active connection has been stopped. An implementation releases only the resources it owns itself; the resources the base class owns are released by DisposeAsync(), which also records that the connection has been disposed.

IsLogLevelEnabled(WebDriverBiDiLogLevel)

Gets a value indicating whether a message at the given level would be raised on OnLogMessage, so that a caller can avoid building a message that would be discarded.

public bool IsLogLevelEnabled(WebDriverBiDiLogLevel level)

Parameters

level WebDriverBiDiLogLevel

The WebDriverBiDiLogLevel of the message the caller would raise.

Returns

bool

true if such a message would be raised; otherwise, false.

Remarks

Test this before composing any log message whose construction is not free. The connection uses it for the SEND and RECV traffic messages, whose construction decodes the whole payload from UTF-8; a custom Connection should use it for the same purpose. Off is never enabled. It selects "no messages at all" when assigned to LogLevel, and is not a level a message can carry; without the explicit test it would compare as enabled against every setting, because it is the highest value.

LogAsync(string)

Asynchronously raises a logging event at the Info log level.

protected Task LogAsync(string message)

Parameters

message string

The log message to raise in the event.

Returns

Task

The task object representing the asynchronous operation.

LogAsync(string, WebDriverBiDiLogLevel)

Asynchronously raises a logging event at the specified log level.

protected Task LogAsync(string message, WebDriverBiDiLogLevel level)

Parameters

message string

The log message to raise in the event.

level WebDriverBiDiLogLevel

The WebDriverBiDiLogLevel at which to raise the event.

Returns

Task

The task object representing the asynchronous operation.

Remarks

A message below LogLevel is discarded here rather than raised. Callers whose message is expensive to compose should also test IsLogLevelEnabled(WebDriverBiDiLogLevel) first, because the message has already been built by the time it reaches this method.

NotifyConnectionErrorObserversAsync(string, Exception)

Reports that the receive loop has ended because of an error, logging the error and notifying the observers of OnConnectionError.

protected Task NotifyConnectionErrorObserversAsync(string logMessage, Exception exception)

Parameters

logMessage string

The message to log at the Error level.

exception Exception

The exception that ended the receive loop.

Returns

Task

The task object representing the asynchronous operation.

Remarks

Call this from the receive loop, as its last act, when a failure ends the loop. It is the only way to raise OnConnectionError. The connection reports itself inactive (see IsActive) before the message is logged and before any observer is notified, so an observer reacting to the error, such as a Transport tearing its session down, already sees a connection that must be started again rather than adopted.

The observers are notified even when logging the message fails because an observer of OnLogMessage throws. That failure still propagates to the caller once the observers have been notified, as the failure of a log observer does anywhere else, unless an observer of OnConnectionError also throws, in which case its failure is the one that propagates.

NotifyDataReceivedObserverAsync(IMemoryOwner<byte>, int)

Notifies the single allowed observer of the OnDataReceived event that a message has been received, transferring ownership of the pooled memory that holds the message to that observer.

protected Task NotifyDataReceivedObserverAsync(IMemoryOwner<byte> messageOwner, int messageLength)

Parameters

messageOwner IMemoryOwner<byte>

The owner of the memory holding the message, at its start. Ownership passes to this method with the call.

messageLength int

The length, in bytes, of the message within the memory of messageOwner.

Returns

Task

The task object representing the asynchronous operation.

Remarks

Use this overload when the receive loop already holds a complete message in pooled memory, so that it can hand the message over without first copying it into a MessageBuffer. The NotifyDataReceivedObserverAsync(MessageBuffer) overload delivers through this one.

Ownership of messageOwner passes to this method whatever the outcome, so a caller neither disposes the memory nor reads from it after the call. When an observer is attached, ownership passes on to it by way of BufferOwner, and that observer returns the memory to the pool. When no observer is attached, or when the call fails before the observer is notified, this method returns the memory itself.

The message is logged at the Trace level before the observer is notified. It is not logged when no observer is attached, because the logging describes traffic that was delivered.

Exceptions

ArgumentNullException

Thrown when messageOwner is null.

ArgumentOutOfRangeException

Thrown when messageLength is negative or exceeds the length of the memory of messageOwner. The memory is returned to its pool before the exception is thrown.

NotifyDataReceivedObserverAsync(MessageBuffer)

Notifies the single allowed observer of the OnDataReceived event that a message has been received, transferring ownership of the message's pooled memory from messageBuffer to that observer.

protected Task NotifyDataReceivedObserverAsync(MessageBuffer messageBuffer)

Parameters

messageBuffer MessageBuffer

The MessageBuffer holding the accumulated message.

Returns

Task

The task object representing the asynchronous operation.

Remarks

This method does nothing when messageBuffer holds no data, so a receive loop may call it at every point where a message may have completed without first testing HasData.

When the buffer does hold data, this method takes ownership of its pooled memory block, which leaves messageBuffer empty and ready to accumulate the next message. Ownership then passes to the consumer of the OnDataReceived event by way of BufferOwner, and that consumer is responsible for returning the block to the pool. A caller must therefore neither dispose the memory nor read from it after this method returns.

The content of a delivered message is logged at the Trace level before the observer is notified.

When no observer is attached to OnDataReceived there is no consumer to take that ownership, so the message is discarded and its memory returned to the pool by this method rather than by an observer. Such a message is not logged, because the logging above describes traffic that was delivered. A Transport attaches its observer before the receive loop starts, so this applies only to a Connection driven without one.

Exceptions

ArgumentNullException

Thrown when messageBuffer is null.

NotifyRemoteDisconnectedObserversAsync()

Reports that the receive loop has ended because the remote end closed the connection, notifying the observers of OnRemoteDisconnected.

protected Task NotifyRemoteDisconnectedObserversAsync()

Returns

Task

The task object representing the asynchronous operation.

Remarks

Call this from the receive loop, as its last act, when the remote end closes the connection. It is the only way to raise OnRemoteDisconnected. The connection reports itself inactive before any observer is notified, for the reason given on NotifyConnectionErrorObserversAsync(string, Exception).

ReceiveDataAsync()

Asynchronously receives data from the remote end of this connection.

protected abstract Task ReceiveDataAsync()

Returns

Task

The task object representing the asynchronous operation.

ResolveConnectionString(string)

Interprets the connection string given to StartAsync(string, CancellationToken), rejecting a value this connection could never connect to and keeping whatever StartConnectionAsync(CancellationToken) will need from it.

protected virtual void ResolveConnectionString(string connectionString)

Parameters

connectionString string

The connection string to interpret.

Remarks

This is the only place a connection string is interpreted. The base class carries the value but never reads it, because what counts as a usable connection string is exactly what a transport knows and nothing above it does. A transport that accepts any string, as a pipe connection does, need not override this method; the default implementation accepts every value.

An implementation rejects a value it could never connect to by throwing ArgumentException. That exception type is the distinction the caller acts on: an ArgumentException out of StartAsync(string, CancellationToken) means the connection string must be corrected before starting is worth attempting again, where every other failure means the attempt itself did not succeed.

Validating a connection string and deriving what a connect needs from it are usually the same act -- parsing a URL both proves it is one and produces the value to connect with -- so this method does both, and an implementation that must parse keeps the result for StartConnectionAsync(CancellationToken) rather than parsing a second time there. That is safe to rely on because StartAsync(string, CancellationToken) is not overridable: it calls this method on every path that reaches StartConnectionAsync(CancellationToken), and nothing between the two can invalidate what this method resolved.

This runs before the state of the connection is examined and before any operation that can take time, so that rejecting a malformed connection string costs nothing. Were it left to StartConnectionAsync(CancellationToken), a caller who passed one would wait out the bounded wait for a previous session's receive loop first, which on a reconnect can be as long as ShutdownTimeout.

Exceptions

ArgumentException

Thrown by an implementation when connectionString is a value it could never connect to.

SendConnectionDataAsync(ReadOnlyMemory<byte>, CancellationToken)

Asynchronously sends data to the underlying mechanism of this connection.

protected abstract Task SendConnectionDataAsync(ReadOnlyMemory<byte> messageBuffer, CancellationToken cancellationToken = default)

Parameters

messageBuffer ReadOnlyMemory<byte>

The buffer containing the data to be sent to the remote end of this connection.

cancellationToken CancellationToken

A cancellation token that is canceled only when the connection is stopped. The cancellation token of the caller of SendDataAsync(ReadOnlyMemory<byte>, CancellationToken) is deliberately not passed through; see the remarks on that method.

Returns

Task

The task object representing the asynchronous operation.

Exceptions

WebDriverBiDiConnectionException

Thrown when an exception is encountered sending data to the remote end of the connection.

SendDataAsync(ReadOnlyMemory<byte>, CancellationToken)

Asynchronously sends data to the remote end of this connection.

public virtual Task SendDataAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)

Parameters

data ReadOnlyMemory<byte>

The data to be sent to the remote end of this connection.

cancellationToken CancellationToken

A cancellation token used to propagate notification that the operation should be canceled.

Returns

Task

The task object representing the asynchronous operation.

Remarks

data is written to the connection as it stands, rather than copied, so it must not be modified -- or returned to a pool it was rented from -- until the returned task completes. A buffer changed while its message is in flight sends the remote end something the caller did not mean to say.

cancellationToken is honored while this method waits for exclusive access to the connection, and up to the moment the data begins to be sent, but not after. Once the first byte may have been written, the send runs to completion, and only stopping the connection interrupts it. A message is the unit the remote end reads, so abandoning one part-way cannot leave the connection usable: a WebSocket is aborted when a send is canceled, and a pipe would be left holding an unterminated message that misframes every message after it. Honoring the caller's cancellation during the send would therefore end the session for every other command using the connection, not just the caller's own.

Exceptions

WebDriverBiDiConnectionException

Thrown when the connection is not active.

WebDriverBiDiTimeoutException

Thrown when exclusive access to the connection for sending times out.

OperationCanceledException

Thrown when cancellationToken is canceled before the data begins to be sent.

StartAsync(string, CancellationToken)

Asynchronously starts communication with the remote end of this connection.

public Task StartAsync(string connectionString, CancellationToken cancellationToken = default)

Parameters

connectionString string

The connection string used to connect to the remote end.

cancellationToken CancellationToken

A cancellation token used to propagate notification that the operation should be canceled.

Returns

Task

The task object representing the asynchronous operation.

Remarks

This method is the whole of the startup sequence every connection performs, and it is not overridable. A derived class supplies only the transport-specific parts of it, by implementing StartConnectionAsync(CancellationToken) and, where the connection string admits values it could never connect to, ResolveConnectionString(string).

The steps this method performs around StartConnectionAsync(CancellationToken) are each required for correctness, and several are required in this order: the previous session's receive loop must be accounted for before a new one can be started over the same transport; the cancellation source must be replaced before anything reads ConnectionCancellationToken, because StopAsync(CancellationToken) cancels it unconditionally and a session that reused it would begin already canceled; and ConnectionString must be assigned before the receive loop starts, because the loop reads it. Leaving those steps to each implementation made them a contract that could only be described, and therefore forgotten.

Exceptions

ObjectDisposedException

Thrown when attempting to start a disposed connection.

WebDriverBiDiConnectionException

Thrown when this connection is already active, or when the receive loop of a previous session is still running after a bounded wait.

OperationCanceledException

Thrown when cancellationToken is canceled.

StartConnectionAsync(CancellationToken)

Asynchronously establishes the transport-specific connection to the remote end, to the target that ResolveConnectionString(string) resolved.

protected abstract Task StartConnectionAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

A cancellation token used to propagate notification that the operation should be canceled.

Returns

Task

The task object representing the asynchronous operation.

Remarks

This is the transport-specific part of StartAsync(string, CancellationToken). When it is called, the connection is known not to be disposed, not to be active, and to have no receive loop of its own still running; ResolveConnectionString(string) has been called with the connection string of this attempt and has accepted it; ConnectionString already names the remote end being connected to; and ConnectionCancellationToken belongs to the session now beginning, so an implementation may use it to bound its own work.

Not being active does not mean being closed. A connection whose receive loop reported that it ended is inactive, but stays open until it is stopped, so this method can be called while IsConnectionOpen still reports true. An implementation replaces or reuses whatever it holds, as it would when starting after a stop.

It takes no connection string, because by the time it runs the string has already been interpreted: an implementation that needed a parsed form of it has that form, and one that wants the string itself reads ConnectionString.

An implementation returns only once the connection is established, which is to say once IsConnectionOpen would report true; StartAsync(string, CancellationToken) starts the receive loop immediately afterwards. It reports a failure to connect by throwing, which clears ConnectionString, leaves the connection stopped, and lets the caller of StartAsync(string, CancellationToken) see why.

StopAsync(CancellationToken)

Asynchronously stops communication with the remote end of this connection.

public Task StopAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

A cancellation token used to propagate notification that the operation should be canceled.

Returns

Task

The task object representing the asynchronous operation.

Remarks

This method is the whole of the shutdown sequence every connection performs, and it is not overridable. A derived class supplies only the transport-specific part of it, by implementing StopConnectionAsync(CancellationToken).

The transport-specific shutdown runs first, because a graceful close of a transport that has one must complete before the connection is canceled; cancellation aborts such a close rather than completing it. Cancellation and the wait for the receive loop then happen unconditionally, whatever state the connection was in and whether or not it was ever started, so that stopping is always well defined and always leaves the connection able to be started again.

Waiting for the receive loop to finish is bounded by ShutdownTimeout. If the loop does not finish within it, a warning is logged and this method returns anyway; the receive task continues running in the background until its read unblocks on its own, and StartAsync(string, CancellationToken) refuses to begin a new session while it does.

A disposed connection has already been stopped by DisposeAsync() and can never be started again, so stopping it does nothing: this method returns at once, raises no log message, and does not throw, just as StopAsync(CancellationToken) and DisconnectAsync(CancellationToken) do not throw ObjectDisposedException for a disposed instance. Only StartAsync(string, CancellationToken) rejects a disposed connection.

StopConnectionAsync(CancellationToken)

Asynchronously performs the transport-specific shutdown of the connection to the remote end.

protected abstract Task StopConnectionAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

A cancellation token used to propagate notification that the operation should be canceled.

Returns

Task

The task object representing the asynchronous operation.

Remarks

This is the transport-specific part of StopAsync(CancellationToken), and the only part of shutdown a derived class supplies. It is where a transport that closes by agreement with the remote end performs that exchange, because it runs before the connection is canceled, and cancellation aborts such an exchange rather than completing it.

It is called on every call to StopAsync(CancellationToken), including when the connection is not active and when it was never started, so an implementation that has nothing to do in those cases tests for them itself. Cancelling the connection and waiting for the receive loop are not its concern; StopAsync(CancellationToken) does both after it returns, whether it succeeded or not.