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:
- ResolveConnectionString(string)Interprets the connection string, rejecting a value this transport could never connect to. Optional; the default accepts every value
- StartConnectionAsync(CancellationToken)Establishes the connection
- StopConnectionAsync(CancellationToken)Whatever this transport must exchange with the remote end to close by agreement
- SendConnectionDataAsync(ReadOnlyMemory<byte>, CancellationToken)Writes one message to the transport
- ReceiveDataAsync()The receive loop, started once the connection is established
- DisposeAsyncCore()Releases the resources the derived class owns
- IsConnectionOpenReports whether the transport-specific connection is open
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
LogSendMessagePrefix
The prefix for logging message content during a send operation.
protected const string LogSendMessagePrefix = "SEND >>> "
Field Value
LoggerComponentName
Gets the component name for this class to use in log messages.
public const string LoggerComponentName = "Connection"
Field Value
Properties
BufferSize
Gets the buffer size for communication used by this connection.
public int BufferSize { get; }
Property Value
ConnectionCancellationToken
Gets the CancellationToken used to cancel the operations of this connection.
protected CancellationToken ConnectionCancellationToken { get; }
Property Value
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
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
DataReceiveTask
Gets the Task object representing the method that receives data from the connection.
protected Task? DataReceiveTask { get; }
Property Value
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
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
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
IsActive
Gets a value indicating whether this connection is active: open, and still read by its receive loop.
public bool IsActive { get; }
Property Value
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
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
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
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
OnDataReceived
Gets an observable event that notifies when data is received from this connection.
public ObservableEvent<ConnectionDataReceivedEventArgs> OnDataReceived { get; }
Property Value
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
OnRemoteDisconnected
Gets an observable event that notifies when the remote end gracefully closes this connection.
public ObservableEvent<ConnectionDisconnectedEventArgs> OnRemoteDisconnected { get; }
Property Value
ShutdownTimeout
Gets or sets the value of the timeout that bounds shutting the connection down.
public TimeSpan ShutdownTimeout { get; set; }
Property Value
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
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
levelWebDriverBiDiLogLevelThe WebDriverBiDiLogLevel of the message the caller would raise.
Returns
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
messagestringThe 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
messagestringThe log message to raise in the event.
levelWebDriverBiDiLogLevelThe 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
logMessagestringThe message to log at the Error level.
exceptionExceptionThe 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
messageOwnerIMemoryOwner<byte>The owner of the memory holding the message, at its start. Ownership passes to this method with the call.
messageLengthintThe 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
messageOwneris null.- ArgumentOutOfRangeException
Thrown when
messageLengthis negative or exceeds the length of the memory ofmessageOwner. 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
messageBufferMessageBufferThe 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
messageBufferis 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
connectionStringstringThe 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
connectionStringis 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
messageBufferReadOnlyMemory<byte>The buffer containing the data to be sent to the remote end of this connection.
cancellationTokenCancellationTokenA 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
dataReadOnlyMemory<byte>The data to be sent to the remote end of this connection.
cancellationTokenCancellationTokenA 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
cancellationTokenis 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
connectionStringstringThe connection string used to connect to the remote end.
cancellationTokenCancellationTokenA 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
cancellationTokenis 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
cancellationTokenCancellationTokenA 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
cancellationTokenCancellationTokenA 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
cancellationTokenCancellationTokenA 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.