Closed scalablecory closed 2 years ago
Want: Pipelines support
Want: Pipelines support
It's there!
How many 3rd party libraries are in need of this and would use it if available?
MySqlConnector would probably use this. It currently has a homemade version of something similar: the base TCP socket or Unix domain socket connection can optionally have a SslStream-based connection layered on top of it (if the connection is secure), and either of those can have protocol compression layered on top of them. (I say "maybe" because the MySQL protocol has some quirks that make it not seamlessly composable.) There are also currently server load-balancing features that might be more cleanly expressed as another IConnectionFactory
layer.
The connection factory could be exposed to advanced users for many of the same scenarios you've mentioned for HttpClient
: bandwidth monitoring, protocol (SQL) inspection, in-memory (or alternate) transport, etc.
How many 3rd party libraries are in need of this and would use it if available?
Not 3rd party, but Orleans uses the Bedrock abstractions and we will use this when it becomes available. We can help validate the APIs early, if that may be useful to you.
Having both Stream and Pipe exposed is a little confusing to me - how will that work?
I say "maybe" because the MySQL protocol has some quirks that make it not seamlessly composable.
@bgrainger Can you give more detail? The goal is to have a very flexible model for composability, and it would be great to know if there's a flaw we can address.
Having both Stream and Pipe exposed is a little confusing to me - how will that work?
@ReubenBond Stream and Pipelines are both in broad use, and this is a way for us to enable wide adoption of the abstraction.
The intended behavior is that if only one is implemented, the other should wrap that. To avoid usage errors, once one propery is used calling the other will throw an exception.
We can help validate the APIs early, if that may be useful to you.
Awesome! We have some of our own validation planned already; I will ping you with details once it's in that stage.
This is complicated to explain briefly, but I'll try. Each packet in the MySQL protocol has a monotonically increasing "sequence number" in its header. Using compression in the protocol takes an arbitrary number of uncompressed packets, compresses them all together, then writes them as the payload of one or more compressed packets, which have their own sequence numbers. You might expect that this compression would be transparent to the underlying data, but it's not. When the uncompressed packets require more than one compressed packet (because the compressed data exceeds the 2^24 byte size limit), then the sequence numbers of the uncompressed packets get reset, starting from the sequence number of the compressed packet that contains them.
For example, if we have uncompressed packets 1-80, and packets 1-50 become the (compressed) payload of compressed packet 1, and packets 51-80 become the (compressed) payload of compressed packet 2, then packets 51-80 get renumbered 2-31 before compression. So there has to be coordination between the compression stream and the layers around it.
I started testing out the non-multiplexed connection abstractions in Kestrel's Socket transport, and here are a few things I noticed.
ConnectionId
as a top level property on IConnection makes logging difficult. Right now, Kestrel just uses an IConnectionIdFeature interface in ConnectionProperties, but it's not great.I haven't updated Kestrel's HttpsConnectionMiddleware to be a IConnectionListenerFactory decorator, but I could try that if people are interested.
I haven't tried out the multiplexed interfaces, but I think that having IMultiplexedConnection implement IConnectionStream will be problematic since neither the Pipe nor Stream properties will be usable.
Thanks for taking time to prove this out.
IConnectionProperties not having a setter is annoying.
I explicitly left this out to push a compositional model, but am interested in where it breaks down. Can you give some specific examples of how you'd use a setter?
IConnectionProperties.TryGet not having a generic version on the interface removes some opportunities for dead code elimination in custom implementations like Kestrel's TransportConnection.
FWIW we have loosely agreed to get rid of the type-based model all-together and instead have the property key just be object
so there is no accidental shadowing. Available keys would be communicated via static properties similar to WPF dependency properties.
Can you give an example of where a generic version helps? We can revisit that decision if you've found it makes a significant difference.
- IConnection not having an Abort() API means that Kestrel uses IConnectionLifetimeFeature.Abort() and stores the IConnectionLifetimeFeature in ConnectionProperties. How are we supposed to abort connections? Disposing the Stream? Our experience in ASP.NET is that people dispose Streams when they're done using it, not necessarily when they want to close the connection.
Beyond some Abort()
method, there's also a consideration of how to communicate such an abort from the remote end to the local Stream
or Pipe
. How will kestrel react to e.g. a SocketException
with one implementation vs a CustomProtocolResetException
in another?
Right now, it's intentionally left undefined: our experience working with QUIC revealed a very TCP-centric mental model for aborting. Trying to make an abort API generic between just those two doesn't seem possible, let alone considering any other random protocol (does a UDS have compatible concepts of abort/reset/etc.?). Do you feel like a feature will work for now (at least, for this first pass in .NET 5), or do you think it's worth some brainstorming?
No CancellationToken for IConnectionListener.DisposeAsync/UnbindAsync could make it difficult to communicate to the listener when to switch from a graceful to an abortive shutdown, but right now Kestrel doesn't need this.
See above :)
No metadata on IConnectionListener is annoying. This makes it impossible for Kestrel to communicate what port it really bound to when asked to bind to port 0.
Agreed, it makes sense to add a property bag there.
I explicitly left this out to push a compositional model, but am interested in where it breaks down. Can you give some specific examples of how you'd use a setter?
Given that filters already need to wrap the IConnection to replace the Stream or Pipe, this isn't actually so bad by comparison. I think the pain came from adapting existing Kestrel code that expects a more mutable IConnection/ConnectionContext.
Kestrel's HttpsConnectionMiddleware adds ITlsApplicationProtocolFeature to ConnectionProperties. To do this HttpsConnectionMiddleware needs to wrap IConnection, IConnection.Stream/Pipe and IConnection.ConnectionProperties with custom implementations. This is more painful than just setting the Stream/Pipe on a mutable IConnection and adding a property to a mutable IConnectionProperties.
This is definitely not a huge issue though. I think it's mostly a matter of preference. With the wrapping approach, you don't need lines like context.Transport = originalTransport
in a finally
when unwinding. HttpsConnectionMiddleware doesn't even unset ITlsApplicationProtocolFeature, but with the wrapping approach this happens implicitly which is nice.
Can you give an example of where a generic version helps? We can revisit that decision if you've found it makes a significant difference.
It makes a bigger difference the more features/properties there are and the further down the else if (typeof(TFeature) == typeof(IFeatureInQuestion))
chain we check for the feature/property. @benaadams ran some microbenchmarks a while back https://github.com/aspnet/KestrelHttpServer/pull/2290.
How will kestrel react to e.g. a SocketException with one implementation vs a CustomProtocolResetException in another?
Kestrel doesn't special-case exception types thrown from the transport except to log ConnectionResetExceptions (which are custom to Kestrel transports) at a lower level than most other IOExceptions.
Kestrel is probably somewhat unusual where it expects writing to the transport to never fail even if the connection has been closed by the client or aborted by the server app. The only way Kestrel can observe the connection failing is either by observing an exception when reading from the transport or a ConnectionClosed
CancellationToken firing.
Kestrel will probably have to adapt any shared transport to have the unusual write-never-throws behavior, but I do think we should add a ConnectionClosed
or StreamClosed
CancellationToken to IConnectionStream instead of relying on something in ConnectionProperties.
Right now, it's intentionally left undefined: our experience working with QUIC revealed a very TCP-centric mental model for aborting. Trying to make an abort API generic between just those two doesn't seem possible, let alone considering any other random protocol (does a UDS have compatible concepts of abort/reset/etc.?). Do you feel like a feature will work for now (at least, for this first pass in .NET 5), or do you think it's worth some brainstorming?
Yeah. Abort() not taking an error code is problematic for QUIC. In Kestrel we added a new Abort() that takes an error code and default to 0x102 (H3_INTERNAL_ERROR)
for the parameterless version. I agree it's not great. I'm OK with implementing Abort() via ConnectionProperties, but that doesn't feel great either.
It makes a bigger difference the more features/properties there are and the further down the
else if (typeof(TFeature) == typeof(IFeatureInQuestion))
chain we check for the feature/property. @benaadams ran some microbenchmarks a while back aspnet/KestrelHttpServer#2290.
Ahh clever.
I also took a look at rewriting Kestrel’s HttpsConnectionMiddleware to use the proposed abstraction.
This went well aside from one key difference from the proposed SslConnectionListenerFactory which is that HttpsConnectionMiddleware wraps the IConnectionListener instead of IConnectionListenerFactory. The reason for this is that Kestrel needs to be able to configure connection middleware on a per-remote-endpoint basis. We could theoretically have a singleton SslConnectionListenerFactory keep track of mappings between each remote endpoint and its configuration, but Kestrel already easily does this, so I see no reason to make connection middleware do this given each middleware would likely have to come up with its own hand-rolled solution.
When rewriting the HttpsConnectionMiddleware, I wound up liking the wrapping the IConnection more than setting properties on ConnectionContext like we did before. It’s a little more verbose (about 33% more), but it really encourages writing low-allocation code, and it eliminates a class of bug where middleware doesn’t properly reset a property when exiting. If the middleware was simpler, I might find wrapping the IConnection a bit more onerous.
One thing that was really annoying when writing both the transport and middleware was writing the logic to throw when accessing the Stream after the IDuplexPipe and vice versa. This logic needs to be written in the transport, and then rewritten at each layer that wraps the Stream/IDuplexPipe. Maybe we should write some built in type that you can construct with either a Stream or an IDuplexPipe, exposes both types as properties that throws if the other property was accessed first.
One thing I really like is this design allows for the possibility of writing a connection throttling middleware since it allows intercepting the call to IConnectionListener.AcceptAsync(). If we use this, we won’t need to add yet another extensibility point to Kestrel for this functionality as suggested in https://github.com/dotnet/aspnetcore/issues/13295.
IConnectionProperties
is no longer type-based, but rather uses a property system inspired by WPF dependency properties. The new system has improved discoverability, is strongly typed, and helps prevent unintentional shadowing that can happen when using types.SocketsHttpHandler
implementation is significantly changed; it now implements both "easy mode" callbacks and full IConnectionFactory
support. For simplicity it no longer allows the user to override the TLS implementation, though this window is left cracked open to allow for a future API if desired.CreateSocket
to the sockets factories for users wanting to set socket options pre-connect.ListenerProperties
and LocalEndPoint
to IConnectionListener
.IConnectionFactory.Filter
extension methods for easily injecting a stream filter with a callback instead of implementing multiple interfaces.ConnectionFactory
property to SmtpClient
.CC @JamesNK
SmtpClient
provingThe API worked great here, with one caveat:
Only supporting asynchronous APIs presents a significant hurdle for existing APIs to adopt without costly sync-over-async. Not supporting synchronous Dispose
is particularly challenging, as there is a lot of established code doing a non-async using
on e.g. Stream
.
We should consider implementing synchronous APIs to make this story better.
Why do we need sync APIs and which ones exactly do you mean?
Why do we need sync APIs and which ones exactly do you mean?
So here's the driving concern: how do we get these interfaces into existing APIs that have sync support?
For instance, SmtpClient.Send
has a full synchronous path right now. It uses sync Socket.Connect
there. The moment it moves over to this new abstraction, it no longer has that ability. Similarly, we might also have HttpClient
's new sync APIs use a sync connect.
We also see IDisposable
being implemented for a lot of APIs right now, and so only supporting IAsyncDisposable
means all those APIs have no great way to dispose of an IConnection
.
It leads us with a few options:
IConnectionFactory
is just not compatible with sync code, and libraries using it as an extensibility point should throw if you use sync APIs combined with a factory. This kills the usefulness of the API: instead of replacing code, it will just add more.I don't like any of these options, but we need to pick one. I'm leaning toward adding synchronous support being the least worst.
Then we never move forward because someone somewhere is pretending that async networking is actually sync.
If needed, have a different set of interfaces IBlockingConnectionFactory
, IBlockingConnection
, etc don't conflate the different and conflicting modes of operation in one set of interfaces.
Also then support can be determined at compile time; rather than either throwing or hand waving at runtime.
That is a better idea because then someone writing a transport doesn't have to provide the sync methods.
I think adding blocking networking APIs adds a lot of complexity for little benefit. As @Drawaes says, networking inherently async. I understand that the OS exposes blocking APIs, but I don't think that suddenly makes the blocking OK.
We should always be telling developers to do non-blocking I/O if they want to build a highly scalable apps. Blocking I/O still leads to threadpool starvation much faster than non-blocking I/O even without any sync-over-async. For developers who are porting low-traffic line-of-business apps that have always managed doing blocking I/O without threadpool starvation, I think sync-over-async is likely fine.
We could always go back and add IBlocking
variants of the interfaces as @benaadams suggests if there's enough demand.
Lots of expert-focused opinions here. I think we can all agree async is what we'd prefer, but lets make sure we are looking at it from other perspectives as well. I don't view this as an expert-only API.
One of the goals of this is adoption into existing libraries. Lack of blocking APIs will hurt that goal.
We also have data (as outlined in HttpClient sync issue) that many users do not understand effective async, and that they still desire high performance sync APIs. Some are okay with horizontal rather than vertical scalability if it simplifies their code. There is value in enabling this scenario.
One of the goals of this is adoption into existing libraries. Lack of blocking APIs will hurt that goal.
Lets be specific though, lets talk about specific libraries and get some code samples. You might be right but lets get some concrete data about which libraries.
Can you outline the sync APIs you want to add?
cc @jtattermusch
Lets be specific though, lets talk about specific libraries and get some code samples.
SmtpClient
and HttpClient
.
Both need a sync Connect()
mechanism, both keep a socket around that needs cleanup inside of their Dispose()
methods.
We can implement IAsyncDisposable
on both if we want, but this won't erase the large amount of existing user code that is doing a non-async using
.
Can you outline the sync APIs you want to add?
Connect()
and Dispose()
for everything under IConnectionFactory
. I see less use for blocking listener methods so I don't have strong opinions there, but consistency is worth considering.
HttpClient
only just had the sync api added; and it isn't highly discoverable. Also you shouldn't be newing up and disposing HttpClient
per request in any high usage scenario as you will be leaving lots of sockets in a TIME_WAIT
state for 240 seconds. So it would only be low throughput scenerios when threadpool exhaustion due to Connect
sync-over-async would be less important?
Not sure about SmtpClient
does it have an async api?
IConnectionProperties is no longer type-based, but rather uses a property system inspired by WPF dependency properties. The new system has improved discoverability, is strongly typed, and helps prevent unintentional shadowing that can happen when using types.
I don't quite understand the benefits here. Improved discovery ability how? Strongly typed how? Can you show a before and after example of how it improved each of these?
HttpClient
only just had the sync api added; and it isn't highly discoverable.
The new sync APIs are very discoverable first-class APIs on HttpClient
.
And Dispose()
already exists, which now needs to call IConnection.DisposeAsync()
.
Also you shouldn't be newing up and disposing HttpClient per request in any high usage scenario as you will be leaving lots of sockets in a TIME_WAIT state for 240 seconds. So it would only be low throughput scenerios when threadpool exhaustion due to Connect sync-over-async would be less important?
Yes, it's certainly sub-optimal but I think it's not the end of the world for typical HttpClient usage.
Keeping in mind that I'm not just worried purely about efficiency, but about adoptability too. Any library using this will need to know how to do sync-over-async against ValueTask
, and due to lack of understanding around this we currently prefer APIs to only ever expose a ValueTask
if the only thing we expect users to do with it is to directly await
it.
Not sure about SmtpClient does it have an async api?
Both sync and async, with more or less the same usage pattern as HttpClient
.
IConnectionProperties is no longer type-based, but rather uses a property system inspired by WPF dependency properties. The new system has improved discoverability, is strongly typed, and helps prevent unintentional shadowing that can happen when using types.
I don't quite understand the benefits here. Improved discovery ability how? Strongly typed how? Can you show a before and after example of how it improved each of these?
Before:
// zero discoverability -- no way to know if this key exists without reading docs.
if(properties.TryGet(out ISslConnectionInformation? sslInfo))
{
// ...
}
// above is just a helper method that translates to this...
if(properties.TryGet(typeof(ISslConnectionInformation), out object? sslInfoObject) && sslInfoObject is ISslConnectionInformation sslInfo)
{
// ...
}
After:
class SslConnectionFactory : IConnectionFactory
{
// a new pattern factories should follow: property keys are used to lookup properties and can be discovered easily just by reading ref source. inspired by WPF dependency properties.
public static ConnectionPropertyKey<ISslConnectionInformation> ConnectionInformationProperty { get; }
}
// no more object, and you can't accidentally do e.g. "out int".
if(properties.TryGet(SslConnectionFactory.ConnectionInformationProperty, out ISslConnectionInformation? sslInfo))
{
// ...
}
The new sync APIs are very discoverable first-class APIs on HttpClient.
Not they aren't. If they were we'd had more overloads (matching the async ones). We intentionally made it "more advanced" because it's not the ideal or recommended usage.
@scalablecory That example isn't convincing to me. How did you know to look up by a static instance property on a random type?
@scalablecory That example isn't convincing to me. How did you know to look up by a static instance property on a random type?
I think the common use case will be people knowing exactly which factories they're using, and this gives them a path to finding which properties they expose/expect.
For the extensibility scenario, e.g. a custom transport plugin, this pattern could be extended to have HttpClient
expose its own properties to indicate what transports might choose to implement.
SmtpClient is obsolete
SmtpClient is not obsolete. It's a bug in the docs. https://github.com/dotnet/dotnet-api-docs/issues/2986
Aggregating current outstanding discussions from here and in-person meetings. If I'm misrepresenting anyone's opinions here please edit with corrections.
EndPoint
properties, have ISocketRemoteEndPoint
, ISocketLocalEndPoint
, or ISocketEndPoints
.ConnectionPropertiesCollection
. It provides some usefulness in having a default "safe" implementation for users who want less ceremony, but perhaps it doesn't provide enough value.IConnection
type and might have better code sharing between QUIC and TCP implementations.
OpenStream
on IConnection
for TCP, we could further merge the TCP and QUIC APIs and enable more reuse.IDisposable
but no other sync APIs.
IDisposable
as too much existing code depends on synchronous using
.
NotImplementedException
or some other mechanism.cc @wicharypawel
Just curious, shouldn't the namespace be System.IO.Connections as it could interact with different types of transports like serial ports, buses (I2C, SPI, GPIO, etc.), Bluetooth, etc.?
Just curious, shouldn't the namespace be System.IO.Connections as it could interact with different types of transports like serial ports, buses (I2C, SPI, GPIO, etc.), Bluetooth, etc.?
You make a good point. We shouldn't consider namespace final; some things will change here anyway,
API Updated @JamesNK @halter73
Changes:
Type
-based. Reason being: I want to make sure we do not bifurcate patterns used in ASP vs BCL.
Object
keys rather than a custom type. This would result in Kestrel doing TryGet(typeof(IFoo), ...)
and BCL doing TryGet(Factory.FooProperty, ...)
, though, so usage patterns would be different.IDisposable
to all the types taking IAsyncDisposable
.SocketsConnectionFactory
and SocketsConnectionListenerFactory
.SslConnectionFactory
and SslConnectionListenerFactory
.SocketsConnectionFactory
virtuals for creating Socket
, Stream
, and IDuplexPipe
.SocketsHttpHandler.SetFilter
to SetConnectionFilter
, for consistency with SetConnectionFactory
and to avoid confusion between per-connection and per-request filtering.More notes:
HttpsMiddleware
, preferring users to not worry about complexity of inserting TLS implementation. We'd like to revisit this, possibly by enabling this feature directly as part of SslConnectionFactory
. More investigation needed here. Will not block review on it though: will tackle this in a subsequent API proposal.@scalablecory are there any preview packages we can use to start playing around with? Of course, with the understanding this is early and changes are expected. Any help is appreciated.
@scalablecory are there any preview packages we can use to start playing around with? Of course, with the understanding this is early and changes are expected. Any help is appreciated.
We're aiming to get this into a preview soon, once API review has been done.
@scalablecory Had troubles finding the review today. Could you add the link once posted on YouTube? Thx
The API review was rescheduled for Thursday.
In .NET, we try to avoid: a) interfaces, b) factories. Could this work as follows?
// server
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.BindAsync(new IPEndPoint(IPAddress.IPv6Loopback, 0), cancellationToken);
using SocketConnection connection = socket.AcceptConnectionAsync(cancellationToken);
using Stream stream = connection.Stream;
// client
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
using SocketConnection = await socket.OpenConnectionAsync(new DnsEndPoint("contoso.com", 80), cancellationToken);
using Stream stream = connection.Stream;
In .NET, we try to avoid: a) interfaces
Have default interfaces methods changed the guidelines? Interfaces are now almost the same as abstract classes, except that classes can only be derived from once.
API review notes:
One potential alternative for the property lookup (in no way agreed upon, but took elements from the discussion):
public abstract class Connection
{
public bool TryGetProperty<T>(out T value) where T : IConnectionPropertySet
{
object valueObject;
if (!TryGetProperty(typeof(T), out valueObject))
{
return _parent != null && _parent.TryGetProperty(typeof(T), out valueObject);
}
value = (T)valueObject;
return true;
}
public T GetProperty<T>() where T : IConnectionPropertySet
{
if (!TryGetProperty<T>(out T value))
{
throw Something;
}
return value;
}
protected virtual bool TryGetProperty(Type key, out T value);
}
unresolved discussion that most, if not all, of these interfaces should be abstract classes.
If you have both server and client in an abstract class; then you don't have compile time help when only one is implemented. If they are in separate classes then you can't implement both. Also if they are classes then you can't inherit from SafeHandle; so would need a secondary indirection for everything if you did use it.
If there is an interface for server and client; then you get explicit compile time help if you only implement one; you can implement SafeHandle directly etc.
<T>
form of GetProperty is better than an Type, object
as you'd (should) want to cast straight away; so the loose typing isn't great.
If they are in separate classes then you can't implement both.
Depending on what extension methods and overloads and the like existed, that'd actually be a goal. For interface-based approaches, if there were ambiguous-target methods, it would probably be good to write an analyzer that says to not implement both of them... just to avoid the problems later.
@scalablecory, sorry for question out of the blue. Would it be possible to monitor connectivity state of underlying connections? Assuming that client keeps multiple of them, I would like to know their state, possibly using reactive approach based on observer pattern.
@wicharypawel a connection could implement some sort of IConnectionMonitor
property, but the in-box ones will currently not -- the general guidance will be to create Nuget packages that can insert themselves into the layering to expose such features.
@scalablecory thank you for your answer, as long as this would be possible to implement, it's fine to me.
I'm still a little confused on how the TryGetProperty
will work. What if you have multiple properties of the same Type, how would it know when to get a specific property?
This question is similar to @stephentoub concerns in 1st review.
For example, if I have 2 properties of type int
(i.e. int baudRate and int dataBits). How would you get each property individually since they are both int
?
System.Net.Connections
Connections is an abstraction for composable connection establishment. It aims to improve layering separation and provide a standard extensibility model for making network connections.
Connections targets client/server implementations, and their users with advanced needs to plug in custom functionality. The latter is a heavily requested feature for
HttpClient
, and the Kestrel team has a handful of examples of users taking advantage of this pattern.Connections brings .NET into parity with “modern” transport models such as Go’s dialers and Netty's channels. ASP.NET has a similar set of interfaces (“Bedrock Transports”) that this would supersede.
Basic API usage
At its most basic usage, System.Net.Connections is an abstraction over
Socket.Accept
andSocket.Connect
. For thisSocket
usage today:The equivalent with System.Net.Connections is:
Composability
Composability has been modeled after
Stream
. For instance:While Streams compose the raw byte stream, Connections compose the establishment of that stream:
Beyond things like TLS and sockets, library implementation can separate some connection establishment logic into clean layers, as seen in
HttpClient
here:Extensibility
Components can expose a connection factory to the user as an extension model. As an example, a user might implement a bandwidth monitoring extension for
HttpClient
:Requests for
HttpClient
extensibilityAs an example of how users would make use of this, we’ve seen many requests for extensibility in
HttpClient
:Go example
Here we use a SOCKS proxy with Go's HTTP client, via a dialer:
Netty example
And use Netty's channel pipelines to do the same:
Connection Properties
Streams have implementation-specific functionality and properties. For instance,
Socket.Shutdown()
and various properties onSslStream
. This means that, even when composing them, the user must still keep track of multiple layers:This makes it challenging to build extensibility into a library's design:
Stream
, but instead every piece of layer that they need to override.With connections, this is cleaned up by allowing each layer in a composed connection to expose, override, or hide features from the previous layer. Abstractions can be used to avoid exposing specific implementation.
Here, our TLS implementation exposes a property while passing through unknown types to previous layers.
The type keys available in properties are not discoverable :( documentation must be read.
When using an established connection, we then extract it as well as a
Socket
property which was exposed by a previous sockets layer:Connection Establishment properties
Property bags are also used when establishing new connections, to allow each layer to decide how to establish the connection. For instance,
HttpClient
can implement a factory that tests if HTTPS is being used and inject TLS into a connection:Usage Examples
Establish a new connection and send data
Listen for a new connection and receive data
Proposed API
Some thoughts:
EndPoint
parameters/properties into theIConnectionProperties
.Additional APIs
This integrates the above interfaces with HTTP, Sockets, TLS
Thoughts and Questions
HttpClient
and did not observe this complexity. I believe there are not a large number of use cases one would want to introduce layers for, and so layering is unlikely to become complex enough for this to be a problem in most apps.Func<(string host, int port), Stream>
)?HttpClient
has a strong need for a connection establishment abstraction. All but one need could be solved by the simpler one.SmtpClient
,SqlConnection
, and so on to make use of this?Future QUIC amendments
QUIC is not yet out of draft status, so QUIC-specific APIs were not a focus for this proposal. However, current knowledge of QUIC did help shape the APIs to make adapting it easier. Here are some thoughts based on current experience:
QuicStream.Dispose
should do, for instance.IConnectionStream
andIConnection
are split in the proposed API specifically to later support anIMultiplexedConnection
that creates multipleIConnectionStream
.IMultiplexedConnection
, we might choose to merge the two APIs and haveIConnection
force users to explicitly open the one bidirectionalIConnectionStream
for the connection.A QUIC extension to this might look like:
Alternately, the
IConnection
andIMultiplexedConnection
APIs might be merged, reducing surface area significantly. The TCP version would simply throw if opening/accepting more than once. This API might look like:However, beyond API surface reduction there isn't clear practical reason to merge them. Given how QUIC is significantly different from TCP, it isn't clear that libraries would see correct reuse of filtering
IConnectionFactory
implementations.