Skip to content

[API Proposal]: Add property bag to QuicConnection #72511

@JamesNK

Description

@JamesNK

Background and motivation

QuicListenerOptions.ConnectionOptionsCallback is a callback that allows the server to modify connection options when it accepts a connection. One of its arguments is QuicConnection.

/// <summary>
/// Selection callback to choose inbound connection options dynamically.
/// </summary>
public Func<QuicConnection, SslClientHelloInfo, CancellationToken, ValueTask<QuicServerConnectionOptions>> ConnectionOptionsCallback { get; set; } = null!;

In ASP.NET Core we allow people to hook into this callback, although we wrap it in our own signature because we have an abstraction called ConnectionContext for a server connection that's not specific to QUIC or HTTP/3.

_quicListenerOptions = new QuicListenerOptions
{
    ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
    ListenEndPoint = listenEndPoint,
    ListenBacklog = options.Backlog,
    ConnectionOptionsCallback = async (quicConnection, helloInfo, cancellationToken) =>
    {
        var serverAuthenticationOptions = await _tlsConnectionOptions.OnConnection(new TlsConnectionCallbackContext
        {
            CancellationToken = cancellationToken,
            ClientHelloInfo = helloInfo,
            State = _tlsConnectionOptions.OnConnectionState,
            Connection = new QuicConnectionContext(quicConnection, _context), // wrap QuicConnection with our abstraction here
        });
        var connectionOptions = new QuicServerConnectionOptions
        {
            ServerAuthenticationOptions = serverAuthenticationOptions,
            IdleTimeout = options.IdleTimeout,
            MaxInboundBidirectionalStreams = options.MaxBidirectionalStreamCount,
            MaxInboundUnidirectionalStreams = options.MaxUnidirectionalStreamCount,
            DefaultCloseErrorCode = 0,
            DefaultStreamErrorCode = 0,
        };
        return connectionOptions;
    }
}

Once the connection is accepted and returned from

var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = new QuicConnectionContext(quicConnection, _context);

The code above is creating our abstraction - QuicConnectionContext - twice: once in the callback and then again after AcceptConnectionAsync returns. We don't want to do this because of allocations, but also because someone can update state on our connection abstraction, and that would disappear if it was recreated.

It would be better if we were able to create QuicConnectionContext once inside the callback, use it, then add it to a property bag on QuicConnection, and then use that value when AcceptConnectionAsync returns.

var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = (QuicConnectionContext)quicConnection.Properties[QuicConnectionContextKey];

Other options include adding QuicConnectionContext to a ConditionalWeakTable<QuicConnection, QuicConnectionContext> inside the callback, then removing it outside the callback. But a place to stash state on the QuicConnection would be much simpler.

This isn't critical for .NET 7. Workarounds include using a weak table, allocating twice (which fixes one problem but creates another...), or leaving ConnectionContext null in the callback in .NET 7 and waiting for this API in .NET 8.

API Proposal

namespace System.Net.Quic;

public class QuicConnection
{
    public IDictionary<string, object?> Properties { get; }
}

API Usage

_quicListenerOptions = new QuicListenerOptions
{
    ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
    ListenEndPoint = listenEndPoint,
    ListenBacklog = options.Backlog,
    ConnectionOptionsCallback = async (quicConnection, helloInfo, cancellationToken) =>
    {
        quicConnection.Properties[QuicConnectionContextKey] = new QuicConnectionContext(quicConnection, _context);
        
        // ...figure out conneciton options...

        return connectionOptions;
    }
}
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken);
var connectionContext = (QuicConnectionContext)quicConnection.Properties[QuicConnectionContextKey];

Alternative Designs

Add a state argument to ConnectionOptionsCallback callback and AcceptConnectionAsync.

_quicListenerOptions = new QuicListenerOptions
{
    ApplicationProtocols = _tlsConnectionOptions.ApplicationProtocols,
    ListenEndPoint = listenEndPoint,
    ListenBacklog = options.Backlog,
    ConnectionOptionsCallback = async (quicConnection, helloInfo, state, cancellationToken) =>
    {
        var listenerContext = (QuicListenerContext)state!
        listenerContext._acceptingQuicConnectionContext = new QuicConnectionContext(quicConnection, _context);
        
        // ...figure out conneciton options...

        return connectionOptions;
    }
}
var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken, state: this);
var connectionContext = _acceptingQuicConnectionContext; // was set in callback

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions