From cfccf8679a08bfadf95eff247a01308e6b2b20d0 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 11 Jul 2023 12:36:31 +0100 Subject: [PATCH 1/4] Implementation --- .../ref/Microsoft.Extensions.Http.cs | 22 ++ .../ref/Microsoft.Extensions.Http.csproj | 1 + .../DefaultSocketsHttpHandlerBuilder.cs | 20 ++ .../HttpClientBuilderExtensions.cs | 35 +++ .../ISocketsHttpHandlerBuilder.cs | 14 ++ .../SocketsHttpHandlerBuilderExtensions.cs | 208 ++++++++++++++++++ .../src/Microsoft.Extensions.Http.csproj | 1 + .../src/Resources/Strings.resx | 63 +++--- 8 files changed, 336 insertions(+), 28 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultSocketsHttpHandlerBuilder.cs create mode 100644 src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs create mode 100644 src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs diff --git a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs index ac217fa34612e2..dd4442345659ca 100644 --- a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs +++ b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs @@ -21,6 +21,12 @@ public static partial class HttpClientBuilderExtensions public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Collections.Generic.IEnumerable redactedLoggedHeaderNames) { throw null; } public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func shouldRedactHeaderValue) { throw null; } public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder SetHandlerLifetime(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.TimeSpan handlerLifetime) { throw null; } +#if NET5_0_OR_GREATER + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder UseSocketsHttpHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action? configureHandler = null) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder UseSocketsHttpHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action configureBuilder) { throw null; } +#endif } public static partial class HttpClientFactoryServiceCollectionExtensions { @@ -50,6 +56,22 @@ public partial interface IHttpClientBuilder string Name { get; } Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } } +#if NET5_0_OR_GREATER + public partial interface ISocketsHttpHandlerBuilder + { + string Name { get; } + Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; } + } +#endif +#if NET5_0_OR_GREATER + public static partial class SocketsHttpHandlerBuilderExtensions + { + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public static Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder Configure(this Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder builder, System.Action configure) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public static Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder Configure(this Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder builder, Microsoft.Extensions.Configuration.IConfigurationSection configurationSection) { throw null; } + } +#endif } namespace Microsoft.Extensions.Http { diff --git a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.csproj b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.csproj index 5cefe086c2f97f..152e6594be71ef 100644 --- a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.csproj +++ b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.csproj @@ -13,6 +13,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultSocketsHttpHandlerBuilder.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultSocketsHttpHandlerBuilder.cs new file mode 100644 index 00000000000000..57b905c0f3e6e0 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultSocketsHttpHandlerBuilder.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +namespace Microsoft.Extensions.DependencyInjection +{ + internal sealed class DefaultSocketsHttpHandlerBuilder : ISocketsHttpHandlerBuilder + { + public DefaultSocketsHttpHandlerBuilder(IServiceCollection services, string name) + { + Services = services; + Name = name; + } + + public string Name { get; } + + public IServiceCollection Services { get; } + } +} +#endif diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs index e97b7f4aa2b10f..44695dd9a24717 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; +using System.Runtime.Versioning; using System.Threading; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; @@ -237,6 +238,40 @@ public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpCl return builder; } +#if NET5_0_OR_GREATER + [UnsupportedOSPlatform("browser")] + public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder builder, Action? configureHandler = null) + { + ThrowHelper.ThrowIfNull(builder); + + builder.Services.Configure(builder.Name, options => + { + options.HttpMessageHandlerBuilderActions.Add(b => + { + if (b.PrimaryHandler is not SocketsHttpHandler handler) + { + handler = new SocketsHttpHandler(); + } + configureHandler?.Invoke(handler, b.Services); + b.PrimaryHandler = handler; + }); + }); + + return builder; + } + + [UnsupportedOSPlatform("browser")] + public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder builder, Action configureBuilder) + { + ThrowHelper.ThrowIfNull(builder); + + UseSocketsHttpHandler(builder); + configureBuilder(new DefaultSocketsHttpHandlerBuilder(builder.Services, builder.Name)); + + return builder; + } +#endif + /// /// Configures a binding between the type and the named /// associated with the . diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs new file mode 100644 index 00000000000000..405abe6ebc0e6a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +namespace Microsoft.Extensions.DependencyInjection +{ + public interface ISocketsHttpHandlerBuilder + { + string Name { get; } + + IServiceCollection Services { get; } + } +} +#endif diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs new file mode 100644 index 00000000000000..364b34c9dc89c3 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Net; +using System.Net.Http; +using System.Runtime.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Http; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class SocketsHttpHandlerBuilderExtensions + { + [UnsupportedOSPlatform("browser")] + public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, Action configure) + { + builder.Services.Configure(builder.Name, options => + { + options.HttpMessageHandlerBuilderActions.Add(b => + { + if (b.PrimaryHandler is not SocketsHttpHandler socketsHttpHandler) + { + string message = SR.Format(SR.SocketsHttpHandlerBuilder_PrimaryHandlerIsInvalid, nameof(b.PrimaryHandler), typeof(SocketsHttpHandler).FullName, Environment.NewLine, b.PrimaryHandler?.ToString() ?? "(null)"); + throw new InvalidOperationException(message); + } + + configure(socketsHttpHandler, b.Services); + }); + }); + return builder; + } + + [UnsupportedOSPlatform("browser")] + public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, IConfigurationSection configurationSection) + { + SocketsHttpHandlerConfiguration parsedConfig = SocketsHttpHandlerConfiguration.ParseFromConfig(configurationSection); + return Configure(builder, (handler, _) => FillFromConfig(handler, parsedConfig)); + } + + [UnsupportedOSPlatform("browser")] + private static void FillFromConfig(SocketsHttpHandler handler, SocketsHttpHandlerConfiguration parsedConfig) + { + if(parsedConfig.PooledConnectionIdleTimeout is not null) + { + handler.PooledConnectionIdleTimeout = parsedConfig.PooledConnectionIdleTimeout.Value; + } + + if(parsedConfig.PooledConnectionLifetime is not null) + { + handler.PooledConnectionLifetime = parsedConfig.PooledConnectionLifetime.Value; + } + + if(parsedConfig.PreAuthenticate is not null) + { + handler.PreAuthenticate = parsedConfig.PreAuthenticate.Value; + } + + if(parsedConfig.ResponseDrainTimeout is not null) + { + handler.ResponseDrainTimeout = parsedConfig.ResponseDrainTimeout.Value; + } + + if(parsedConfig.UseCookies is not null) + { + handler.UseCookies = parsedConfig.UseCookies.Value; + } + + if(parsedConfig.UseProxy is not null) + { + handler.UseProxy = parsedConfig.UseProxy.Value; + } + + if(parsedConfig.EnableMultipleHttp2Connections is not null) + { + handler.EnableMultipleHttp2Connections = parsedConfig.EnableMultipleHttp2Connections.Value; + } + + if(parsedConfig.MaxResponseHeadersLength is not null) + { + handler.MaxResponseHeadersLength = parsedConfig.MaxResponseHeadersLength.Value; + } + + if(parsedConfig.MaxResponseDrainSize is not null) + { + handler.MaxResponseDrainSize = parsedConfig.MaxResponseDrainSize.Value; + } + + if(parsedConfig.MaxConnectionsPerServer is not null) + { + handler.MaxConnectionsPerServer = parsedConfig.MaxConnectionsPerServer.Value; + } + + if(parsedConfig.MaxAutomaticRedirections is not null) + { + handler.MaxAutomaticRedirections = parsedConfig.MaxAutomaticRedirections.Value; + } + + if(parsedConfig.InitialHttp2StreamWindowSize is not null) + { + handler.InitialHttp2StreamWindowSize = parsedConfig.InitialHttp2StreamWindowSize.Value; + } + + if(parsedConfig.AllowAutoRedirect is not null) + { + handler.AllowAutoRedirect = parsedConfig.AllowAutoRedirect.Value; + } + + if(parsedConfig.AutomaticDecompression is not null) + { + handler.AutomaticDecompression = parsedConfig.AutomaticDecompression.Value; + } + + if(parsedConfig.ConnectTimeout is not null) + { + handler.ConnectTimeout = parsedConfig.ConnectTimeout.Value; + } + + if(parsedConfig.Expect100ContinueTimeout is not null) + { + handler.Expect100ContinueTimeout = parsedConfig.Expect100ContinueTimeout.Value; + } + + if(parsedConfig.KeepAlivePingDelay is not null) + { + handler.KeepAlivePingDelay = parsedConfig.KeepAlivePingDelay.Value; + } + + if(parsedConfig.KeepAlivePingTimeout is not null) + { + handler.KeepAlivePingTimeout = parsedConfig.KeepAlivePingTimeout.Value; + } + + if(parsedConfig.KeepAlivePingPolicy is not null) + { + handler.KeepAlivePingPolicy = parsedConfig.KeepAlivePingPolicy.Value; + } + } + + private record struct SocketsHttpHandlerConfiguration + { + public TimeSpan? PooledConnectionIdleTimeout { get; set; } + public TimeSpan? PooledConnectionLifetime { get; set; } + public bool? PreAuthenticate { get; set; } + public TimeSpan? ResponseDrainTimeout { get; set; } + public bool? UseCookies { get; set; } + public bool? UseProxy { get; set; } + public bool? EnableMultipleHttp2Connections { get; set; } + public int? MaxResponseHeadersLength { get; set; } + public int? MaxResponseDrainSize { get; set; } + public int? MaxConnectionsPerServer { get; set; } + public int? MaxAutomaticRedirections { get; set; } + public int? InitialHttp2StreamWindowSize { get; set; } + public bool? AllowAutoRedirect { get; set; } + public DecompressionMethods? AutomaticDecompression { get; set; } + public TimeSpan? ConnectTimeout { get; set; } + public TimeSpan? Expect100ContinueTimeout { get; set; } + public TimeSpan? KeepAlivePingDelay { get; set; } + public TimeSpan? KeepAlivePingTimeout { get; set; } + public HttpKeepAlivePingPolicy? KeepAlivePingPolicy { get; set; } + + [UnsupportedOSPlatform("browser")] + public static SocketsHttpHandlerConfiguration ParseFromConfig(IConfigurationSection config) + { + return new SocketsHttpHandlerConfiguration() + { + PooledConnectionIdleTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionIdleTimeout)]), + PooledConnectionLifetime = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionLifetime)]), + PreAuthenticate = ParseBool(config[nameof(SocketsHttpHandler.PreAuthenticate)]), + ResponseDrainTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ResponseDrainTimeout)]), + UseCookies = ParseBool(config[nameof(SocketsHttpHandler.UseCookies)]), + UseProxy = ParseBool(config[nameof(SocketsHttpHandler.UseProxy)]), + EnableMultipleHttp2Connections = ParseBool(config[nameof(SocketsHttpHandler.EnableMultipleHttp2Connections)]), + MaxResponseHeadersLength = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseHeadersLength)]), + MaxResponseDrainSize = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseDrainSize)]), + MaxConnectionsPerServer = ParseInt(config[nameof(SocketsHttpHandler.MaxConnectionsPerServer)]), + MaxAutomaticRedirections = ParseInt(config[nameof(SocketsHttpHandler.MaxAutomaticRedirections)]), + InitialHttp2StreamWindowSize = ParseInt(config[nameof(SocketsHttpHandler.InitialHttp2StreamWindowSize)]), + AllowAutoRedirect = ParseBool(config[nameof(SocketsHttpHandler.AllowAutoRedirect)]), + AutomaticDecompression = ParseDecompressionMethods(config[nameof(SocketsHttpHandler.AutomaticDecompression)]), + ConnectTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ConnectTimeout)]), + Expect100ContinueTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.Expect100ContinueTimeout)]), + KeepAlivePingDelay = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingDelay)]), + KeepAlivePingTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingTimeout)]), + KeepAlivePingPolicy = ParseHttpKeepAlivePingPolicy(config[nameof(SocketsHttpHandler.KeepAlivePingPolicy)]) + }; + } + } + + private static DecompressionMethods? ParseDecompressionMethods(string? decompressionMethods) + => Enum.TryParse(decompressionMethods, ignoreCase: true, out var result) + ? result + : null; + + private static HttpKeepAlivePingPolicy? ParseHttpKeepAlivePingPolicy(string? httpKeepAlivePingPolicy) + => Enum.TryParse(httpKeepAlivePingPolicy, ignoreCase: true, out var result) + ? result + : null; + + private static bool? ParseBool(string? boolString) => bool.TryParse(boolString, out var result) ? result : null; + + private static int? ParseInt(string? intString) => int.TryParse(intString, out var result) ? result : null; + + private static TimeSpan? ParseTimeSpan(string? timeSpanString) => TimeSpan.TryParse(timeSpanString, out var result) ? result : null; + } +} +#endif diff --git a/src/libraries/Microsoft.Extensions.Http/src/Microsoft.Extensions.Http.csproj b/src/libraries/Microsoft.Extensions.Http/src/Microsoft.Extensions.Http.csproj index 2b4f8c334fd5a7..5e49b26ce0ff22 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/Microsoft.Extensions.Http.csproj +++ b/src/libraries/Microsoft.Extensions.Http/src/Microsoft.Extensions.Http.csproj @@ -26,6 +26,7 @@ System.Net.Http.IHttpClientFactory + diff --git a/src/libraries/Microsoft.Extensions.Http/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Http/src/Resources/Strings.resx index 04c2a32b8ad2f1..b3639ca4015f76 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Http/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -134,4 +134,11 @@ The handler lifetime must be at least 1 second. - \ No newline at end of file + + The '{0}' must be '{1}'.{2}Handler: '{3}' + 0 = nameof(PrimaryHandler) +1 = typeof(SocketsHttpHandler).FullName +2 = Environment.NewLine +3 = handler.ToString() + + From 05d6ec1086a6146a13dc2201daad7b9e551011ca Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 13 Jul 2023 18:26:42 +0100 Subject: [PATCH 2/4] Add tests --- .../Microsoft.Extensions.Http.Tests.csproj | 2 + .../SocketsHttpHandlerConfigurationTest.cs | 247 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Microsoft.Extensions.Http.Tests.csproj b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Microsoft.Extensions.Http.Tests.csproj index 863b903aca173b..18b020bb33ff57 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Microsoft.Extensions.Http.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/Microsoft.Extensions.Http.Tests.csproj @@ -25,6 +25,8 @@ + + diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs new file mode 100644 index 00000000000000..c1b7f092b5df79 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs @@ -0,0 +1,247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Security; +using System.Threading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Http +{ + public class SocketsHttpHandlerConfigurationTest + { + private const string ParentSectionName = "HttpClientSettings"; + private const string SectionName = "ConfiguredByIConfiguration"; + private static Dictionary s_configContent = new Dictionary + { + { $"{ParentSectionName}:{SectionName}:AllowAutoRedirect", "true" }, + { $"{ParentSectionName}:{SectionName}:UseCookies", "false" }, + { $"{ParentSectionName}:{SectionName}:ConnectTimeout", "00:00:05" }, + { $"{ParentSectionName}:{SectionName}:PooledConnectionLifetime", "00:01:00" } + }; + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_Parameterless_Success() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("DefaultPrimaryHandler"); + + serviceCollection.AddHttpClient("SocketsHttpHandler") + .UseSocketsHttpHandler(); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var defaultPrimaryHandlerChain = messageHandlerFactory.CreateHandler("DefaultPrimaryHandler"); + var socketsHttpHandlerChain = messageHandlerFactory.CreateHandler("SocketsHttpHandler"); + + Assert.IsType(GetPrimaryHandler(defaultPrimaryHandlerChain)); + Assert.IsType(GetPrimaryHandler(socketsHttpHandlerChain)); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_ConfiguredByAction_Success() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddHttpClient("Unconfigured") + .UseSocketsHttpHandler(); + + serviceCollection.AddHttpClient("ConfiguredByAction") + .UseSocketsHttpHandler((handler, _) => handler.PooledConnectionLifetime = TimeSpan.FromMinutes(1)); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var unconfiguredHandlerChain = messageHandlerFactory.CreateHandler("Unconfigured"); + var configuredHandlerChain = messageHandlerFactory.CreateHandler("ConfiguredByAction"); + + var unconfiguredHandler = (SocketsHttpHandler)GetPrimaryHandler(unconfiguredHandlerChain); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.Equal(Timeout.InfiniteTimeSpan, unconfiguredHandler.PooledConnectionLifetime); + Assert.Equal(TimeSpan.FromMinutes(1), configuredHandler.PooledConnectionLifetime); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_ConfiguredByBuilder_Success() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddHttpClient("Unconfigured") + .UseSocketsHttpHandler(); + + serviceCollection.AddHttpClient("ConfiguredByBuilder") + .UseSocketsHttpHandler(builder => + builder.Configure((handler, _) => handler.ConnectTimeout = TimeSpan.FromSeconds(10))); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var unconfiguredHandlerChain = messageHandlerFactory.CreateHandler("Unconfigured"); + var configuredHandlerChain = messageHandlerFactory.CreateHandler("ConfiguredByBuilder"); + + var unconfiguredHandler = (SocketsHttpHandler)GetPrimaryHandler(unconfiguredHandlerChain); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.Equal(Timeout.InfiniteTimeSpan, unconfiguredHandler.ConnectTimeout); + Assert.Equal(TimeSpan.FromSeconds(10), configuredHandler.ConnectTimeout); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_ConfiguredByIConfiguration_Success() + { + IConfiguration config = new ConfigurationBuilder() + .AddInMemoryCollection(s_configContent) + .Build(); + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddHttpClient(SectionName) + .UseSocketsHttpHandler(builder => + builder.Configure(config.GetSection($"{ParentSectionName}:{SectionName}"))); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var configuredHandlerChain = messageHandlerFactory.CreateHandler(SectionName); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.True(configuredHandler.AllowAutoRedirect); + Assert.False(configuredHandler.UseCookies); + Assert.Equal(TimeSpan.FromSeconds(5), configuredHandler.ConnectTimeout); + Assert.Equal(TimeSpan.FromMinutes(1), configuredHandler.PooledConnectionLifetime); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_ChainingActionAfterIConfiguration_Updates() + { + IConfiguration config = new ConfigurationBuilder() + .AddInMemoryCollection(s_configContent) + .Build(); + + var serviceCollection = new ServiceCollection(); + + var allowAllCertsSslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = delegate { return true; } + }; + + serviceCollection.AddHttpClient("ActionAfterIConfiguration") + .UseSocketsHttpHandler(builder => + builder.Configure(config.GetSection($"{ParentSectionName}:{SectionName}")) + .Configure((handler, _) => + { + handler.ConnectTimeout = TimeSpan.FromSeconds(10); // will overwrite value from IConfiguration + handler.SslOptions = allowAllCertsSslOptions; + })); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var configuredHandlerChain = messageHandlerFactory.CreateHandler("ActionAfterIConfiguration"); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.True(configuredHandler.AllowAutoRedirect); // from IConfiguration + Assert.False(configuredHandler.UseCookies); // from IConfiguration + Assert.Equal(TimeSpan.FromSeconds(10), configuredHandler.ConnectTimeout); // overwritten by action + Assert.Equal(TimeSpan.FromMinutes(1), configuredHandler.PooledConnectionLifetime); // from IConfiguration + Assert.Equal(allowAllCertsSslOptions, configuredHandler.SslOptions); // from action + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public void UseSocketsHttpHandler_ChainingIConfigurationAfterAction_Updates() + { + IConfiguration config = new ConfigurationBuilder() + .AddInMemoryCollection(s_configContent) + .Build(); + + var serviceCollection = new ServiceCollection(); + + var allowAllCertsSslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = delegate { return true; } + }; + + serviceCollection.AddHttpClient("IConfigurationAfterAction") + .UseSocketsHttpHandler(builder => + builder.Configure((handler, _) => + { + handler.ConnectTimeout = TimeSpan.FromSeconds(10); // will be overwrittten by IConfiguration + handler.SslOptions = allowAllCertsSslOptions; + }) + .Configure(config.GetSection($"{ParentSectionName}:{SectionName}"))); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var configuredHandlerChain = messageHandlerFactory.CreateHandler("IConfigurationAfterAction"); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.True(configuredHandler.AllowAutoRedirect); // from IConfiguration + Assert.False(configuredHandler.UseCookies); // from IConfiguration + Assert.Equal(TimeSpan.FromSeconds(5), configuredHandler.ConnectTimeout); // overwritten by IConfiguration + Assert.Equal(TimeSpan.FromMinutes(1), configuredHandler.PooledConnectionLifetime); // from IConfiguration + Assert.Equal(allowAllCertsSslOptions, configuredHandler.SslOptions); // from action + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [InlineData(false)] + [InlineData(true)] + public void UseSocketsHttpHandler_PresetSocketsHttpHandler_Updates(bool handlerSetByUseSocketsHttpHandler) + { + var serviceCollection = new ServiceCollection(); + + var builder = serviceCollection.AddHttpClient("SocketsHttpHandler"); + + if (handlerSetByUseSocketsHttpHandler) + { + builder.UseSocketsHttpHandler((handler, _) => handler.PooledConnectionLifetime = TimeSpan.FromMinutes(10)); + } + else + { + builder.ConfigurePrimaryHttpMessageHandler(() => + { + var handler = new SocketsHttpHandler(); + handler.PooledConnectionLifetime = TimeSpan.FromMinutes(10); + return handler; + }); + } + + var allowAllCertsSslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = delegate { return true; } + }; + + builder.UseSocketsHttpHandler((handler, _) => + { + handler.SslOptions = allowAllCertsSslOptions; // this will update existing SocketsHttpHandler + }); + + var services = serviceCollection.BuildServiceProvider(); + var messageHandlerFactory = services.GetRequiredService(); + + var configuredHandlerChain = messageHandlerFactory.CreateHandler("SocketsHttpHandler"); + var configuredHandler = (SocketsHttpHandler)GetPrimaryHandler(configuredHandlerChain); + + Assert.Equal(TimeSpan.FromMinutes(10), configuredHandler.PooledConnectionLifetime); // from initial config + Assert.Equal(allowAllCertsSslOptions, configuredHandler.SslOptions); // from second config + } + + private HttpMessageHandler GetPrimaryHandler(HttpMessageHandler handlerChain) + { + var handler = handlerChain; + while (handler is DelegatingHandler delegatingHandler) + { + handler = delegatingHandler.InnerHandler; + } + return handler; + } + } +} +#endif From 89680f068728c9f2135fea10f654040b9a13e7df Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 13 Jul 2023 21:15:04 +0100 Subject: [PATCH 3/4] API review changes and minor code reshuffling --- .../ref/Microsoft.Extensions.Http.cs | 2 +- .../SocketsHttpHandlerBuilderExtensions.cs | 187 +++++++++--------- .../SocketsHttpHandlerConfigurationTest.cs | 3 +- 3 files changed, 92 insertions(+), 100 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs index dd4442345659ca..3a60d5920adbcb 100644 --- a/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs +++ b/src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs @@ -69,7 +69,7 @@ public static partial class SocketsHttpHandlerBuilderExtensions [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public static Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder Configure(this Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder builder, System.Action configure) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] - public static Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder Configure(this Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder builder, Microsoft.Extensions.Configuration.IConfigurationSection configurationSection) { throw null; } + public static Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder Configure(this Microsoft.Extensions.DependencyInjection.ISocketsHttpHandlerBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; } } #endif } diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs index 364b34c9dc89c3..39b4e4930be8fd 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs @@ -33,175 +33,166 @@ public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuild } [UnsupportedOSPlatform("browser")] - public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, IConfigurationSection configurationSection) + public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, IConfiguration configuration) { - SocketsHttpHandlerConfiguration parsedConfig = SocketsHttpHandlerConfiguration.ParseFromConfig(configurationSection); + SocketsHttpHandlerConfiguration parsedConfig = ParseSocketsHttpHandlerConfiguration(configuration); return Configure(builder, (handler, _) => FillFromConfig(handler, parsedConfig)); } [UnsupportedOSPlatform("browser")] - private static void FillFromConfig(SocketsHttpHandler handler, SocketsHttpHandlerConfiguration parsedConfig) + private static void FillFromConfig(SocketsHttpHandler handler, SocketsHttpHandlerConfiguration config) { - if(parsedConfig.PooledConnectionIdleTimeout is not null) + if (config.PooledConnectionIdleTimeout is not null) { - handler.PooledConnectionIdleTimeout = parsedConfig.PooledConnectionIdleTimeout.Value; + handler.PooledConnectionIdleTimeout = config.PooledConnectionIdleTimeout.Value; } - if(parsedConfig.PooledConnectionLifetime is not null) + if (config.PooledConnectionLifetime is not null) { - handler.PooledConnectionLifetime = parsedConfig.PooledConnectionLifetime.Value; + handler.PooledConnectionLifetime = config.PooledConnectionLifetime.Value; } - if(parsedConfig.PreAuthenticate is not null) + if (config.PreAuthenticate is not null) { - handler.PreAuthenticate = parsedConfig.PreAuthenticate.Value; + handler.PreAuthenticate = config.PreAuthenticate.Value; } - if(parsedConfig.ResponseDrainTimeout is not null) + if (config.ResponseDrainTimeout is not null) { - handler.ResponseDrainTimeout = parsedConfig.ResponseDrainTimeout.Value; + handler.ResponseDrainTimeout = config.ResponseDrainTimeout.Value; } - if(parsedConfig.UseCookies is not null) + if (config.UseCookies is not null) { - handler.UseCookies = parsedConfig.UseCookies.Value; + handler.UseCookies = config.UseCookies.Value; } - if(parsedConfig.UseProxy is not null) + if (config.UseProxy is not null) { - handler.UseProxy = parsedConfig.UseProxy.Value; + handler.UseProxy = config.UseProxy.Value; } - if(parsedConfig.EnableMultipleHttp2Connections is not null) + if (config.EnableMultipleHttp2Connections is not null) { - handler.EnableMultipleHttp2Connections = parsedConfig.EnableMultipleHttp2Connections.Value; + handler.EnableMultipleHttp2Connections = config.EnableMultipleHttp2Connections.Value; } - if(parsedConfig.MaxResponseHeadersLength is not null) + if (config.MaxResponseHeadersLength is not null) { - handler.MaxResponseHeadersLength = parsedConfig.MaxResponseHeadersLength.Value; + handler.MaxResponseHeadersLength = config.MaxResponseHeadersLength.Value; } - if(parsedConfig.MaxResponseDrainSize is not null) + if (config.MaxResponseDrainSize is not null) { - handler.MaxResponseDrainSize = parsedConfig.MaxResponseDrainSize.Value; + handler.MaxResponseDrainSize = config.MaxResponseDrainSize.Value; } - if(parsedConfig.MaxConnectionsPerServer is not null) + if (config.MaxConnectionsPerServer is not null) { - handler.MaxConnectionsPerServer = parsedConfig.MaxConnectionsPerServer.Value; + handler.MaxConnectionsPerServer = config.MaxConnectionsPerServer.Value; } - if(parsedConfig.MaxAutomaticRedirections is not null) + if (config.MaxAutomaticRedirections is not null) { - handler.MaxAutomaticRedirections = parsedConfig.MaxAutomaticRedirections.Value; + handler.MaxAutomaticRedirections = config.MaxAutomaticRedirections.Value; } - if(parsedConfig.InitialHttp2StreamWindowSize is not null) + if (config.InitialHttp2StreamWindowSize is not null) { - handler.InitialHttp2StreamWindowSize = parsedConfig.InitialHttp2StreamWindowSize.Value; + handler.InitialHttp2StreamWindowSize = config.InitialHttp2StreamWindowSize.Value; } - if(parsedConfig.AllowAutoRedirect is not null) + if (config.AllowAutoRedirect is not null) { - handler.AllowAutoRedirect = parsedConfig.AllowAutoRedirect.Value; + handler.AllowAutoRedirect = config.AllowAutoRedirect.Value; } - if(parsedConfig.AutomaticDecompression is not null) + if (config.AutomaticDecompression is not null) { - handler.AutomaticDecompression = parsedConfig.AutomaticDecompression.Value; + handler.AutomaticDecompression = config.AutomaticDecompression.Value; } - if(parsedConfig.ConnectTimeout is not null) + if (config.ConnectTimeout is not null) { - handler.ConnectTimeout = parsedConfig.ConnectTimeout.Value; + handler.ConnectTimeout = config.ConnectTimeout.Value; } - if(parsedConfig.Expect100ContinueTimeout is not null) + if (config.Expect100ContinueTimeout is not null) { - handler.Expect100ContinueTimeout = parsedConfig.Expect100ContinueTimeout.Value; + handler.Expect100ContinueTimeout = config.Expect100ContinueTimeout.Value; } - if(parsedConfig.KeepAlivePingDelay is not null) + if (config.KeepAlivePingDelay is not null) { - handler.KeepAlivePingDelay = parsedConfig.KeepAlivePingDelay.Value; + handler.KeepAlivePingDelay = config.KeepAlivePingDelay.Value; } - if(parsedConfig.KeepAlivePingTimeout is not null) + if (config.KeepAlivePingTimeout is not null) { - handler.KeepAlivePingTimeout = parsedConfig.KeepAlivePingTimeout.Value; + handler.KeepAlivePingTimeout = config.KeepAlivePingTimeout.Value; } - if(parsedConfig.KeepAlivePingPolicy is not null) + if (config.KeepAlivePingPolicy is not null) { - handler.KeepAlivePingPolicy = parsedConfig.KeepAlivePingPolicy.Value; + handler.KeepAlivePingPolicy = config.KeepAlivePingPolicy.Value; } } - private record struct SocketsHttpHandlerConfiguration + private readonly record struct SocketsHttpHandlerConfiguration { - public TimeSpan? PooledConnectionIdleTimeout { get; set; } - public TimeSpan? PooledConnectionLifetime { get; set; } - public bool? PreAuthenticate { get; set; } - public TimeSpan? ResponseDrainTimeout { get; set; } - public bool? UseCookies { get; set; } - public bool? UseProxy { get; set; } - public bool? EnableMultipleHttp2Connections { get; set; } - public int? MaxResponseHeadersLength { get; set; } - public int? MaxResponseDrainSize { get; set; } - public int? MaxConnectionsPerServer { get; set; } - public int? MaxAutomaticRedirections { get; set; } - public int? InitialHttp2StreamWindowSize { get; set; } - public bool? AllowAutoRedirect { get; set; } - public DecompressionMethods? AutomaticDecompression { get; set; } - public TimeSpan? ConnectTimeout { get; set; } - public TimeSpan? Expect100ContinueTimeout { get; set; } - public TimeSpan? KeepAlivePingDelay { get; set; } - public TimeSpan? KeepAlivePingTimeout { get; set; } - public HttpKeepAlivePingPolicy? KeepAlivePingPolicy { get; set; } - - [UnsupportedOSPlatform("browser")] - public static SocketsHttpHandlerConfiguration ParseFromConfig(IConfigurationSection config) - { - return new SocketsHttpHandlerConfiguration() - { - PooledConnectionIdleTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionIdleTimeout)]), - PooledConnectionLifetime = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionLifetime)]), - PreAuthenticate = ParseBool(config[nameof(SocketsHttpHandler.PreAuthenticate)]), - ResponseDrainTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ResponseDrainTimeout)]), - UseCookies = ParseBool(config[nameof(SocketsHttpHandler.UseCookies)]), - UseProxy = ParseBool(config[nameof(SocketsHttpHandler.UseProxy)]), - EnableMultipleHttp2Connections = ParseBool(config[nameof(SocketsHttpHandler.EnableMultipleHttp2Connections)]), - MaxResponseHeadersLength = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseHeadersLength)]), - MaxResponseDrainSize = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseDrainSize)]), - MaxConnectionsPerServer = ParseInt(config[nameof(SocketsHttpHandler.MaxConnectionsPerServer)]), - MaxAutomaticRedirections = ParseInt(config[nameof(SocketsHttpHandler.MaxAutomaticRedirections)]), - InitialHttp2StreamWindowSize = ParseInt(config[nameof(SocketsHttpHandler.InitialHttp2StreamWindowSize)]), - AllowAutoRedirect = ParseBool(config[nameof(SocketsHttpHandler.AllowAutoRedirect)]), - AutomaticDecompression = ParseDecompressionMethods(config[nameof(SocketsHttpHandler.AutomaticDecompression)]), - ConnectTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ConnectTimeout)]), - Expect100ContinueTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.Expect100ContinueTimeout)]), - KeepAlivePingDelay = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingDelay)]), - KeepAlivePingTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingTimeout)]), - KeepAlivePingPolicy = ParseHttpKeepAlivePingPolicy(config[nameof(SocketsHttpHandler.KeepAlivePingPolicy)]) - }; - } + public TimeSpan? PooledConnectionIdleTimeout { get; init; } + public TimeSpan? PooledConnectionLifetime { get; init; } + public bool? PreAuthenticate { get; init; } + public TimeSpan? ResponseDrainTimeout { get; init; } + public bool? UseCookies { get; init; } + public bool? UseProxy { get; init; } + public bool? EnableMultipleHttp2Connections { get; init; } + public int? MaxResponseHeadersLength { get; init; } + public int? MaxResponseDrainSize { get; init; } + public int? MaxConnectionsPerServer { get; init; } + public int? MaxAutomaticRedirections { get; init; } + public int? InitialHttp2StreamWindowSize { get; init; } + public bool? AllowAutoRedirect { get; init; } + public DecompressionMethods? AutomaticDecompression { get; init; } + public TimeSpan? ConnectTimeout { get; init; } + public TimeSpan? Expect100ContinueTimeout { get; init; } + public TimeSpan? KeepAlivePingDelay { get; init; } + public TimeSpan? KeepAlivePingTimeout { get; init; } + public HttpKeepAlivePingPolicy? KeepAlivePingPolicy { get; init; } } - private static DecompressionMethods? ParseDecompressionMethods(string? decompressionMethods) - => Enum.TryParse(decompressionMethods, ignoreCase: true, out var result) - ? result - : null; + [UnsupportedOSPlatform("browser")] + private static SocketsHttpHandlerConfiguration ParseSocketsHttpHandlerConfiguration(IConfiguration config) + { + return new SocketsHttpHandlerConfiguration() + { + PooledConnectionIdleTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionIdleTimeout)]), + PooledConnectionLifetime = ParseTimeSpan(config[nameof(SocketsHttpHandler.PooledConnectionLifetime)]), + PreAuthenticate = ParseBool(config[nameof(SocketsHttpHandler.PreAuthenticate)]), + ResponseDrainTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ResponseDrainTimeout)]), + UseCookies = ParseBool(config[nameof(SocketsHttpHandler.UseCookies)]), + UseProxy = ParseBool(config[nameof(SocketsHttpHandler.UseProxy)]), + EnableMultipleHttp2Connections = ParseBool(config[nameof(SocketsHttpHandler.EnableMultipleHttp2Connections)]), + MaxResponseHeadersLength = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseHeadersLength)]), + MaxResponseDrainSize = ParseInt(config[nameof(SocketsHttpHandler.MaxResponseDrainSize)]), + MaxConnectionsPerServer = ParseInt(config[nameof(SocketsHttpHandler.MaxConnectionsPerServer)]), + MaxAutomaticRedirections = ParseInt(config[nameof(SocketsHttpHandler.MaxAutomaticRedirections)]), + InitialHttp2StreamWindowSize = ParseInt(config[nameof(SocketsHttpHandler.InitialHttp2StreamWindowSize)]), + AllowAutoRedirect = ParseBool(config[nameof(SocketsHttpHandler.AllowAutoRedirect)]), + AutomaticDecompression = ParseEnum(config[nameof(SocketsHttpHandler.AutomaticDecompression)]), + ConnectTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.ConnectTimeout)]), + Expect100ContinueTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.Expect100ContinueTimeout)]), + KeepAlivePingDelay = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingDelay)]), + KeepAlivePingTimeout = ParseTimeSpan(config[nameof(SocketsHttpHandler.KeepAlivePingTimeout)]), + KeepAlivePingPolicy = ParseEnum(config[nameof(SocketsHttpHandler.KeepAlivePingPolicy)]) + }; + } - private static HttpKeepAlivePingPolicy? ParseHttpKeepAlivePingPolicy(string? httpKeepAlivePingPolicy) - => Enum.TryParse(httpKeepAlivePingPolicy, ignoreCase: true, out var result) - ? result - : null; + private static TEnum? ParseEnum(string? enumString) where TEnum : struct + => Enum.TryParse(enumString, ignoreCase: true, out var result) ? result : null; private static bool? ParseBool(string? boolString) => bool.TryParse(boolString, out var result) ? result : null; - private static int? ParseInt(string? intString) => int.TryParse(intString, out var result) ? result : null; - private static TimeSpan? ParseTimeSpan(string? timeSpanString) => TimeSpan.TryParse(timeSpanString, out var result) ? result : null; } } diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs index c1b7f092b5df79..acc181070fcbbd 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs @@ -22,7 +22,8 @@ public class SocketsHttpHandlerConfigurationTest { $"{ParentSectionName}:{SectionName}:AllowAutoRedirect", "true" }, { $"{ParentSectionName}:{SectionName}:UseCookies", "false" }, { $"{ParentSectionName}:{SectionName}:ConnectTimeout", "00:00:05" }, - { $"{ParentSectionName}:{SectionName}:PooledConnectionLifetime", "00:01:00" } + { $"{ParentSectionName}:{SectionName}:PooledConnectionLifetime", "00:01:00" }, + { $"{ParentSectionName}:{SectionName}:SomeUnrelatedProperty", "WillBeIgnored" } }; [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] From ea76239d80543a8f7014dfca13f68ca80e111f4c Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 13 Jul 2023 22:18:40 +0100 Subject: [PATCH 4/4] Add triple slash docs --- .../HttpClientBuilderExtensions.cs | 32 +++++++++++++++++++ .../ISocketsHttpHandlerBuilder.cs | 10 ++++++ .../SocketsHttpHandlerBuilderExtensions.cs | 26 +++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs index 44695dd9a24717..e3329e829a4ba5 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs @@ -239,6 +239,21 @@ public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpCl } #if NET5_0_OR_GREATER + /// + /// Adds or updates as a primary handler for a named . If provided, + /// also adds a delegate that will be used to configure the primary . + /// + /// The . + /// Optional delegate that is used to configure the primary . + /// An that can be used to configure the client. + /// + /// + /// If a primary handler was already set to be by previously calling, for example, + /// or + /// , then the passed + /// delegate will be applied to the existing instance. Otherwise, a new instance of will be created. + /// + /// [UnsupportedOSPlatform("browser")] public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder builder, Action? configureHandler = null) { @@ -260,6 +275,23 @@ public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder b return builder; } + /// + /// Adds or updates as a primary handler for a named + /// and configures it using . + /// + /// The . + /// Delegate that is used to set up the configuration of the the primary + /// on that will later be applied on the primary handler during its creation. + /// An that can be used to configure the client. + /// + /// + /// If a primary handler was already set to be by previously calling, for example, + /// or + /// , then the configuration set on + /// will be applied to the existing instance. Otherwise, a new instance of + /// will be created. + /// + /// [UnsupportedOSPlatform("browser")] public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder builder, Action configureBuilder) { diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs index 405abe6ebc0e6a..d7509e009c9c47 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/ISocketsHttpHandlerBuilder.cs @@ -4,10 +4,20 @@ #if NET5_0_OR_GREATER namespace Microsoft.Extensions.DependencyInjection { + /// + /// A builder for configuring for a named + /// instances returned by . + /// public interface ISocketsHttpHandlerBuilder { + /// + /// Gets the name of the client for a handler configured by this builder. + /// string Name { get; } + /// + /// Gets the application service collection. + /// IServiceCollection Services { get; } } } diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs index 39b4e4930be8fd..d7635d368c5578 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/SocketsHttpHandlerBuilderExtensions.cs @@ -11,8 +11,19 @@ namespace Microsoft.Extensions.DependencyInjection { + /// + /// Extension methods to configure for a named + /// instances returned by . + /// public static class SocketsHttpHandlerBuilderExtensions { + /// + /// Adds a delegate that will be used to configure the primary for a + /// named . + /// + /// The . + /// A delegate that is used to modify a . + /// An that can be used to configure the handler. [UnsupportedOSPlatform("browser")] public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, Action configure) { @@ -32,6 +43,21 @@ public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuild return builder; } + /// + /// Uses to configure the primary for a + /// named . + /// + /// The . + /// Configuration containing properties of . + /// An that can be used to configure the handler. + /// + /// + /// Only simple (of type `bool`, `int`, or ) properties of will be parsed. + /// + /// + /// All unmatched properties in will be ignored. + /// + /// [UnsupportedOSPlatform("browser")] public static ISocketsHttpHandlerBuilder Configure(this ISocketsHttpHandlerBuilder builder, IConfiguration configuration) {