Description
Epic #10869
The goal of this issue is to specify abstractions which the .NET community can use to build robust, correct, high-performance client & server applications. This is a follow-up to #4772 (Project Bedrock) and is largely a summary of a discussion with @davidfowl, @anurse, @halter73, @shirhatti, & @jkotalik.
Initiating & accepting connections
Clients initiate connections to an endpoint using IConnectionFactory
. Servers bind to an endpoint using IConnectionListenerFactory
and then accept incoming connections using the IConnectionListener
returned at binding time.
public abstract class ConnectionContext : IAsyncDisposable
{
// unchanged except for IAsyncDisposable base
}
public interface IConnectionListenerFactory
{
ValueTask<IConnectionListener> BindAsync(System.Net.EndPoint endpoint, CancellationToken cancellationToken = default);
}
public interface IConnectionListener : IAsyncDisposable
{
EndPoint EndPoint { get; }
ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default);
ValueTask UnbindAsync(CancellationToken cancellationToken = default);
}
public interface IConnectionFactory
{
ValueTask<ConnectionContext> ConnectAsync(System.Net.EndPoint endpoint, CancellationToken cancellationToken = default);
}
A framework might want to support multiple different transports, such as file + socket. In that case, it can use multiple IConnectionListenerFactory
implementations.
The shared abstractions should not dictate how these factories are are specified or configured: frameworks should continue to follow a framework-specific builder pattern. Eg, KestrelServerOptions
has List<ListenOptions>
where each entry is a different endpoint to listen on + middleware to handle connections to that endpoint.
Both IConnectionListenerFactory.BindAsync
& IConnectionFactory.ConnectAsync
accept a System.Net.EndPoint
to bind/connect to. The BCL has EndPoint
implementations for IPEndPoint
, DnsEndPoint
, and UnixDomainSocketEndPoint
. Users can add new transports which accept custom sub-classes of EndPoint
, eg MyOverlayNetworkEndPoint
.
Servers bind to endpoints using IConnectionListenerFactory
, resulting in a IConnectionListener
. The listener's AcceptAsync()
is then called in a loop, each time resulting in a ConnectionContext
instance which represent a newly accepted connection. When the server wants to stop accepting new connections, the listener is disposed by calling listener.DisposeAsync()
. Connections which were previously accepted continue to operate until each is individually terminated.
For implementations of IConnectionListener
which are backed by a callback system (eg, libuv), decoupling the lifetime of IConnectionListener
and the (many) resulting ConnectionContext
s poses a challenge. We need to consider whether or not this minimal interface allows graceful shutdown (including draining connections) to still be cleanly implemented in those cases.
Handling connections
The client/server decides how it handles the ConnectionContext
. Typically it will be decorated (eg, via ConnectionDelegate
) and pumped until the connection is terminated. Connections can be terminated by calling DisposeAsync()
(from IAsyncDisposable
) but can also be terminated out-of-band (eg, because the remote endpoint terminated it). In either case, clients/servers can subscribe to the ConnectionContext
's IConnectionLifetimeFeature
feature to learn about termination and take necessary action (eg, cleanup).
Connection metadata
Currently, IHttpConnectionFeature
is used to expose local/remote endpoint information. We will add a new feature, IConnectionEndPointFeature
as a uniform way to access this information.
public interface IConnectionEndPointFeature
{
EndPoint RemoteEndPoint { get; set; }
EndPoint LocalEndPoint { get; set; }
}