File: KestrelServerOptions.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core;
 
/// <summary>
/// Provides programmatic configuration of Kestrel-specific features.
/// </summary>
public class KestrelServerOptions
{
    internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators";
    private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
    internal const string CertificateFileWatchingSwitch = "Microsoft.AspNetCore.Server.Kestrel.DisableCertificateFileWatching";
    private static readonly bool _finOnError;
    private static readonly bool _disableCertificateFileWatching;
 
    static KestrelServerOptions()
    {
        AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
        AppContext.TryGetSwitch(CertificateFileWatchingSwitch, out _disableCertificateFileWatching);
    }
 
    // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
    internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null;
 
    // Opt-out flag for back compat. Remove in 9.0 (or make public).
    internal bool FinOnError { get; set; } = _finOnError;
 
    private Func<string, Encoding?> _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector;
 
    private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector;
 
    /// <summary>
    /// In HTTP/1.x, when a request target is in absolute-form (see RFC 9112 Section 3.2.2),
    /// for example
    /// <code>
    /// GET http://www.example.com/path/to/index.html HTTP/1.1
    /// </code>
    /// the Host header is redundant.  In fact, the RFC says
    ///
    ///   When an origin server receives a request with an absolute-form of request-target,
    ///   the origin server MUST ignore the received Host header field (if any) and instead
    ///   use the host information of the request-target.
    ///
    /// However, it is still sensible to check whether the request target and Host header match
    /// because a mismatch might indicate, for example, a spoofing attempt.  Setting this property
    /// to true bypasses that check and unconditionally overwrites the Host header with the value
    /// from the request target.
    /// </summary>
    /// <remarks>
    /// This option does not apply to HTTP/2 or HTTP/3.
    /// </remarks>
    /// <seealso href="https://datatracker.ietf.org/doc/html/rfc9112#section-3.2.2-8"/>
    public bool AllowHostHeaderOverride { get; set; }
 
    // The following two lists configure the endpoints that Kestrel should listen to. If both lists are empty, the "urls" config setting (e.g. UseUrls) is used.
    internal List<ListenOptions> CodeBackedListenOptions { get; } = new List<ListenOptions>();
    internal List<ListenOptions> ConfigurationBackedListenOptions { get; } = new List<ListenOptions>();
 
    internal ListenOptions[] GetListenOptions()
    {
        int resultCount = CodeBackedListenOptions.Count + ConfigurationBackedListenOptions.Count;
        if (resultCount == 0)
        {
            return Array.Empty<ListenOptions>();
        }
 
        var result = new ListenOptions[resultCount];
        CodeBackedListenOptions.CopyTo(result);
        ConfigurationBackedListenOptions.CopyTo(result, CodeBackedListenOptions.Count);
        return result;
    }
 
    // For testing and debugging.
    internal List<ListenOptions> OptionsInUse { get; } = new List<ListenOptions>();
 
    /// <summary>
    /// Gets or sets whether the <c>Server</c> header should be included in each response.
    /// </summary>
    /// <remarks>
    /// Defaults to true.
    /// </remarks>
    public bool AddServerHeader { get; set; } = true;
 
    /// <summary>
    /// Gets or sets a value that controls whether dynamic compression of response headers is allowed.
    /// For more information about the security considerations of HPack dynamic header compression, visit
    /// <see href="https://tools.ietf.org/html/rfc7541#section-7"/>.
    /// </summary>
    /// <remarks>
    /// Defaults to true.
    /// </remarks>
    public bool AllowResponseHeaderCompression { get; set; } = true;
 
    /// <summary>
    /// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/>
    /// </summary>
    /// <remarks>
    /// Defaults to false.
    /// </remarks>
    public bool AllowSynchronousIO { get; set; }
 
    /// <summary>
    /// Gets or sets a value that controls how the `:scheme` field for HTTP/2 and HTTP/3 requests is validated.
    /// <para>
    /// If <c>false</c> then the `:scheme` field for HTTP/2 and HTTP/3 requests must exactly match the transport (e.g. https for TLS
    /// connections, http for non-TLS). If <c>true</c> then the `:scheme` field for HTTP/2 and HTTP/3 requests can be set to alternate values
    /// and this will be reflected by `HttpRequest.Scheme`. The Scheme must still be valid according to
    /// <see href="https://datatracker.ietf.org/doc/html/rfc3986/#section-3.1"/>. Only enable this when working with a trusted proxy. This can be used in
    /// scenarios such as proxies converting from alternate protocols. See <see href="https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3"/>.
    /// Applications that enable this should validate an expected scheme is provided before using it.
    /// </para>
    /// </summary>
    /// <remarks>
    /// Defaults to <c>false</c>.
    /// </remarks>
    public bool AllowAlternateSchemes { get; set; }
 
    /// <summary>
    /// Gets or sets a value that controls whether the string values materialized
    /// will be reused across requests; if they match, or if the strings will always be reallocated.
    /// </summary>
    /// <remarks>
    /// Defaults to false.
    /// </remarks>
    public bool DisableStringReuse { get; set; }
 
    /// <summary>
    /// Controls whether to return the "Alt-Svc" header from an HTTP/2 or lower response for HTTP/3.
    /// </summary>
    /// <remarks>
    /// Defaults to false.
    /// </remarks>
    [Obsolete($"This property is obsolete and will be removed in a future version. It no longer has any impact on runtime behavior. Use {nameof(Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions)}.{nameof(Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions.DisableAltSvcHeader)} to configure \"Alt-Svc\" behavior.", error: true)]
    public bool EnableAltSvc { get; set; }
 
    /// <summary>
    /// Gets or sets a callback that returns the <see cref="Encoding"/> to decode the value for the specified request header name,
    /// or <see langword="null"/> to use the default <see cref="UTF8Encoding"/>.
    /// </summary>
    public Func<string, Encoding?> RequestHeaderEncodingSelector
    {
        get => _requestHeaderEncodingSelector;
        set => _requestHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value));
    }
 
    /// <summary>
    /// Gets or sets a callback that returns the <see cref="Encoding"/> to encode the value for the specified response header
    /// or trailer name, or <see langword="null"/> to use the default <see cref="ASCIIEncoding"/>.
    /// </summary>
    public Func<string, Encoding?> ResponseHeaderEncodingSelector
    {
        get => _responseHeaderEncodingSelector;
        set => _responseHeaderEncodingSelector = value ?? throw new ArgumentNullException(nameof(value));
    }
 
    /// <summary>
    /// Enables the Listen options callback to resolve and use services registered by the application during startup.
    /// Typically initialized by UseKestrel().
    /// </summary>
    public IServiceProvider ApplicationServices { get; set; } = default!; // This should typically be set
 
    /// <summary>
    /// Provides access to request limit options.
    /// </summary>
    public KestrelServerLimits Limits { get; } = new KestrelServerLimits();
 
    /// <summary>
    /// Provides a configuration source where endpoints will be loaded from on server start.
    /// The default is <see langword="null"/>.
    /// </summary>
    public KestrelConfigurationLoader? ConfigurationLoader { get; set; }
 
    /// <summary>
    /// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
    /// </summary>
    private Action<ListenOptions> EndpointDefaults { get; set; } = _ => { };
 
    /// <summary>
    /// A default configuration action for all https endpoints.
    /// </summary>
    private Action<HttpsConnectionAdapterOptions> HttpsDefaults { get; set; } = _ => { };
 
    /// <summary>
    /// The development server certificate for https endpoints. This is applied lazily after HttpsDefaults and user options.
    /// </summary>
    /// <remarks>
    /// Getter exposed for testing.
    /// </remarks>
    internal X509Certificate2? DevelopmentCertificate { get; private set; }
 
    /// <summary>
    /// Allow tests to explicitly set the default certificate.
    /// </summary>
    internal X509Certificate2? TestOverrideDefaultCertificate { get; set; }
 
    /// <summary>
    /// Has the default dev certificate load been attempted?
    /// </summary>
    internal bool IsDevelopmentCertificateLoaded { get; set; }
 
    /// <summary>
    /// Internal AppContext switch to toggle the WebTransport and HTTP/3 datagrams experiemental features.
    /// </summary>
    private bool? _enableWebTransportAndH3Datagrams;
    internal bool EnableWebTransportAndH3Datagrams
    {
        get
        {
            if (!_enableWebTransportAndH3Datagrams.HasValue)
            {
                _enableWebTransportAndH3Datagrams = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.Experimental.WebTransportAndH3Datagrams", out var enabled) && enabled;
            }
 
            return _enableWebTransportAndH3Datagrams.Value;
        }
        set => _enableWebTransportAndH3Datagrams = value;
    }
 
    /// <summary>
    /// Internal AppContext switch to toggle whether a request line can end with LF only instead of CR/LF.
    /// </summary>
    private bool? _disableHttp1LineFeedTerminators;
    internal bool DisableHttp1LineFeedTerminators
    {
        get
        {
            if (!_disableHttp1LineFeedTerminators.HasValue)
            {
                _disableHttp1LineFeedTerminators = AppContext.TryGetSwitch(DisableHttp1LineFeedTerminatorsSwitchKey, out var disabled) && disabled;
            }
 
            return _disableHttp1LineFeedTerminators.Value;
        }
        set => _disableHttp1LineFeedTerminators = value;
    }
 
    /// <summary>
    /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
    /// the prior action.
    /// </summary>
    public void ConfigureEndpointDefaults(Action<ListenOptions> configureOptions)
    {
        EndpointDefaults = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions));
    }
 
    internal void ApplyEndpointDefaults(ListenOptions listenOptions)
    {
        listenOptions.KestrelServerOptions = this;
        ConfigurationLoader?.ApplyEndpointDefaults(listenOptions);
        EndpointDefaults(listenOptions);
    }
 
    /// <summary>
    /// Specifies a configuration Action to run for each newly created https endpoint. Calling this again will replace
    /// the prior action.
    /// </summary>
    public void ConfigureHttpsDefaults(Action<HttpsConnectionAdapterOptions> configureOptions)
    {
        HttpsDefaults = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions));
    }
 
    internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
    {
        ConfigurationLoader?.ApplyHttpsDefaults(httpsOptions);
        HttpsDefaults(httpsOptions);
    }
 
    internal void ApplyDefaultCertificate(HttpsConnectionAdapterOptions httpsOptions)
    {
        if (httpsOptions.HasServerCertificateOrSelector)
        {
            return;
        }
 
        // It's important (and currently true) that we don't reach here with https configuration uninitialized because
        // we might incorrectly favor the development certificate over one specified by the user.
        Debug.Assert(ApplicationServices.GetRequiredService<IHttpsConfigurationService>().IsInitialized, "HTTPS configuration should have been enabled");
 
        if (TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest)
        {
            httpsOptions.ServerCertificate = certificateFromTest;
            return;
        }
 
        if (ConfigurationLoader?.DefaultCertificate is X509Certificate2 certificateFromLoader)
        {
            httpsOptions.ServerCertificate = certificateFromLoader;
            return;
        }
 
        if (!IsDevelopmentCertificateLoaded)
        {
            IsDevelopmentCertificateLoaded = true;
            Debug.Assert(DevelopmentCertificate is null);
            var logger = ApplicationServices!.GetRequiredService<ILogger<KestrelServer>>();
            DevelopmentCertificate = GetDevelopmentCertificateFromStore(logger);
        }
 
        httpsOptions.ServerCertificate = DevelopmentCertificate;
    }
 
    internal void EnableHttpsConfiguration()
    {
        var httpsConfigurationService = ApplicationServices.GetRequiredService<IHttpsConfigurationService>();
 
        if (!httpsConfigurationService.IsInitialized)
        {
            var hostEnvironment = ApplicationServices.GetRequiredService<IHostEnvironment>();
            var logger = ApplicationServices.GetRequiredService<ILogger<KestrelServer>>();
            var httpsLogger = ApplicationServices.GetRequiredService<ILogger<HttpsConnectionMiddleware>>();
            httpsConfigurationService.Initialize(hostEnvironment, logger, httpsLogger);
        }
    }
 
    internal void Serialize(Utf8JsonWriter writer)
    {
        writer.WritePropertyName(nameof(AllowSynchronousIO));
        writer.WriteBooleanValue(AllowSynchronousIO);
 
        writer.WritePropertyName(nameof(AddServerHeader));
        writer.WriteBooleanValue(AddServerHeader);
 
        writer.WritePropertyName(nameof(AllowAlternateSchemes));
        writer.WriteBooleanValue(AllowAlternateSchemes);
 
        writer.WritePropertyName(nameof(AllowResponseHeaderCompression));
        writer.WriteBooleanValue(AllowResponseHeaderCompression);
 
        writer.WritePropertyName(nameof(IsDevelopmentCertificateLoaded));
        writer.WriteBooleanValue(IsDevelopmentCertificateLoaded);
 
        writer.WriteString(nameof(RequestHeaderEncodingSelector), RequestHeaderEncodingSelector == DefaultHeaderEncodingSelector ? "default" : "configured");
        writer.WriteString(nameof(ResponseHeaderEncodingSelector), ResponseHeaderEncodingSelector == DefaultHeaderEncodingSelector ? "default" : "configured");
 
        // Limits
        writer.WritePropertyName(nameof(Limits));
        writer.WriteStartObject();
        Limits.Serialize(writer);
        writer.WriteEndObject();
 
        // ListenOptions
        writer.WritePropertyName(nameof(ListenOptions));
        writer.WriteStartArray();
        foreach (var listenOptions in OptionsInUse)
        {
            writer.WriteStartObject();
            writer.WriteString("Address", listenOptions.GetDisplayName());
            writer.WritePropertyName(nameof(listenOptions.IsTls));
            writer.WriteBooleanValue(listenOptions.IsTls);
            writer.WriteString(nameof(listenOptions.Protocols), listenOptions.Protocols.ToString());
            writer.WriteEndObject();
        }
        writer.WriteEndArray();
    }
 
    private static X509Certificate2? GetDevelopmentCertificateFromStore(ILogger<KestrelServer> logger)
    {
        try
        {
            var certs = CertificateManager.Instance.ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: false);
 
            var cert = certs.Count > 0 ? certs[0] : null;
            if (cert is null)
            {
                logger.UnableToLocateDevelopmentCertificate();
                return null;
            }
 
            var status = CertificateManager.Instance.CheckCertificateState(cert);
            if (!status.Success)
            {
                // Failure is only possible on MacOS and indicates that, if there is a dev cert, it must be from
                // a dotnet version prior to 7.0 - newer versions store it in such a way that this check succeeds.
                // (Success does not mean that the dev cert has been trusted).
                // In practice, success.FailureMessage will always be MacOSCertificateManager.InvalidCertificateState.
                // Basically, we're just going to encourage the user to generate and trust the dev cert.  We support
                // these older certificates not by accepting them as-is, but by modernizing them when dev-certs is run.
                // If we detect an issue here, we can avoid a UI prompt below.
                Debug.Assert(status.FailureMessage != null, "Status with a failure result must have a message.");
                logger.DeveloperCertificateFirstRun(status.FailureMessage);
 
                // Prevent binding to HTTPS if the certificate is not valid (avoid the prompt)
                return null;
            }
 
            // On MacOS, this may cause a UI prompt, since it requires accessing the keychain.  Kestrel must NEVER
            // cause a UI prompt on a production system. We only attempt this here because MacOS is not supported
            // in production.
            switch (CertificateManager.Instance.GetTrustLevel(cert))
            {
                case CertificateManager.TrustLevel.Partial:
                    logger.DeveloperCertificatePartiallyTrusted();
                    break;
                case CertificateManager.TrustLevel.None:
                    logger.DeveloperCertificateNotTrusted();
                    break;
            }
 
            return cert;
        }
        catch
        {
            logger.UnableToLocateDevelopmentCertificate();
            return null;
        }
    }
 
    /// <summary>
    /// Creates a configuration loader for setting up Kestrel.
    /// </summary>
    /// <returns>A <see cref="KestrelConfigurationLoader"/> for configuring endpoints.</returns>
    public KestrelConfigurationLoader Configure() => Configure(new ConfigurationBuilder().Build());
 
    /// <summary>
    /// Creates a configuration loader for setting up Kestrel that takes an <see cref="IConfiguration"/> as input.
    /// This configuration must be scoped to the configuration section for Kestrel.
    /// Call <see cref="Configure(IConfiguration, bool)"/> to enable dynamic endpoint binding updates.
    /// </summary>
    /// <param name="config">The configuration section for Kestrel.</param>
    /// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>
    public KestrelConfigurationLoader Configure(IConfiguration config) => Configure(config, reloadOnChange: false);
 
    /// <summary>
    /// Creates a configuration loader for setting up Kestrel that takes an <see cref="IConfiguration"/> as input.
    /// This configuration must be scoped to the configuration section for Kestrel.
    /// </summary>
    /// <param name="config">The configuration section for Kestrel.</param>
    /// <param name="reloadOnChange">
    /// If <see langword="true"/>, Kestrel will dynamically update endpoint bindings when configuration changes.
    /// This will only reload endpoints defined in the "Endpoints" section of your <paramref name="config"/>. Endpoints defined in code will not be reloaded.
    /// </param>
    /// <returns>A <see cref="KestrelConfigurationLoader"/> for further endpoint configuration.</returns>
    public KestrelConfigurationLoader Configure(IConfiguration config, bool reloadOnChange)
    {
        if (ApplicationServices is null)
        {
            throw new InvalidOperationException($"{nameof(ApplicationServices)} must not be null. This is normally set automatically via {nameof(IConfigureOptions<KestrelServerOptions>)}.");
        }
 
        var httpsConfigurationService = ApplicationServices.GetRequiredService<IHttpsConfigurationService>();
        var certificatePathWatcher = reloadOnChange && !_disableCertificateFileWatching
            ? new CertificatePathWatcher(
                ApplicationServices.GetRequiredService<IHostEnvironment>(),
                ApplicationServices.GetRequiredService<ILogger<CertificatePathWatcher>>())
            : null;
        var loader = new KestrelConfigurationLoader(this, config, httpsConfigurationService, certificatePathWatcher, reloadOnChange);
        ConfigurationLoader = loader;
        return loader;
    }
 
    /// <summary>
    /// Bind to the given IP address and port.
    /// </summary>
    public void Listen(IPAddress address, int port)
    {
        Listen(address, port, _ => { });
    }
 
    /// <summary>
    /// Bind to the given IP address and port.
    /// The callback configures endpoint-specific settings.
    /// </summary>
    public void Listen(IPAddress address, int port, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(address);
 
        Listen(new IPEndPoint(address, port), configure);
    }
 
    /// <summary>
    /// Bind to the given IP endpoint.
    /// </summary>
    public void Listen(IPEndPoint endPoint)
    {
        Listen((EndPoint)endPoint);
    }
 
    /// <summary>
    /// Bind to the given endpoint.
    /// </summary>
    /// <param name="endPoint"></param>
    public void Listen(EndPoint endPoint)
    {
        Listen(endPoint, _ => { });
    }
 
    /// <summary>
    /// Bind to the given IP address and port.
    /// The callback configures endpoint-specific settings.
    /// </summary>
    public void Listen(IPEndPoint endPoint, Action<ListenOptions> configure)
    {
        Listen((EndPoint)endPoint, configure);
    }
 
    /// <summary>
    /// Bind to the given endpoint.
    /// The callback configures endpoint-specific settings.
    /// </summary>
    public void Listen(EndPoint endPoint, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(endPoint);
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new ListenOptions(endPoint);
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
 
    /// <summary>
    /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported
    /// for this type of endpoint.
    /// </summary>
    public void ListenLocalhost(int port) => ListenLocalhost(port, options => { });
 
    /// <summary>
    /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported
    /// for this type of endpoint.
    /// </summary>
    public void ListenLocalhost(int port, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new LocalhostListenOptions(port);
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
 
    /// <summary>
    /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported.
    /// </summary>
    public void ListenAnyIP(int port) => ListenAnyIP(port, options => { });
 
    /// <summary>
    /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported.
    /// </summary>
    public void ListenAnyIP(int port, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new AnyIPListenOptions(port);
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
 
    /// <summary>
    /// Bind to the given Unix domain socket path.
    /// </summary>
    public void ListenUnixSocket(string socketPath)
    {
        ListenUnixSocket(socketPath, _ => { });
    }
 
    /// <summary>
    /// Bind to the given Unix domain socket path.
    /// Specify callback to configure endpoint-specific settings.
    /// </summary>
    public void ListenUnixSocket(string socketPath, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(socketPath);
 
        if (!Path.IsPathRooted(socketPath))
        {
            throw new ArgumentException(CoreStrings.UnixSocketPathMustBeAbsolute, nameof(socketPath));
        }
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new ListenOptions(socketPath);
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
 
    /// <summary>
    /// Open a socket file descriptor.
    /// </summary>
    public void ListenHandle(ulong handle)
    {
        ListenHandle(handle, _ => { });
    }
 
    /// <summary>
    /// Open a socket file descriptor.
    /// The callback configures endpoint-specific settings.
    /// </summary>
    public void ListenHandle(ulong handle, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new ListenOptions(handle);
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
 
    /// <summary>
    /// Bind to the given named pipe.
    /// </summary>
    public void ListenNamedPipe(string pipeName)
    {
        ListenNamedPipe(pipeName, _ => { });
    }
 
    /// <summary>
    /// Bind to the given named pipe.
    /// Specify callback to configure endpoint-specific settings.
    /// </summary>
    public void ListenNamedPipe(string pipeName, Action<ListenOptions> configure)
    {
        ArgumentNullException.ThrowIfNull(pipeName);
        ArgumentNullException.ThrowIfNull(configure);
 
        var listenOptions = new ListenOptions(new NamedPipeEndPoint(pipeName));
        ApplyEndpointDefaults(listenOptions);
        configure(listenOptions);
        CodeBackedListenOptions.Add(listenOptions);
    }
}