From 607b9d766606d4b30b097a25213831cb231a2c31 Mon Sep 17 00:00:00 2001 From: tr00d Date: Fri, 5 Apr 2024 14:44:59 +0200 Subject: [PATCH] feat: add SocketsHttpHandler in Configuration to override http connection pool lifetime and idle timeout. --- Vonage.Test/ConfigurationTest.cs | 448 +++++++++++++--------- Vonage.Test/TestHelpers/TestingContext.cs | 23 +- Vonage/Configuration.cs | 22 +- Vonage/Vonage.csproj | 1 + 4 files changed, 307 insertions(+), 187 deletions(-) diff --git a/Vonage.Test/ConfigurationTest.cs b/Vonage.Test/ConfigurationTest.cs index bf4cb9e62..d47c4c18e 100644 --- a/Vonage.Test/ConfigurationTest.cs +++ b/Vonage.Test/ConfigurationTest.cs @@ -1,204 +1,300 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Net; +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Configuration; using Vonage.Cryptography; using Vonage.Test.Common.Extensions; +using Vonage.Test.TestHelpers; +using WireMock.ResponseBuilders; using Xunit; +using Xunit.Abstractions; -namespace Vonage.Test +namespace Vonage.Test; + +[Trait("Category", "Unit")] +public class ConfigurationTest { - [Trait("Category", "Unit")] - public class ConfigurationTest + [Fact] + public void BuildCredentials_ShouldCreateEmptyCredentials_GivenConfigurationContainsNoElement() { - [Fact] - public void BuildCredentials_ShouldCreateEmptyCredentials_GivenConfigurationContainsNoElement() - { - var credentials = Configuration.FromConfiguration(new ConfigurationBuilder().Build()).BuildCredentials(); - credentials.ApiKey.Should().BeEmpty(); - credentials.ApiSecret.Should().BeEmpty(); - credentials.ApplicationId.Should().BeEmpty(); - credentials.ApplicationKey.Should().BeEmpty(); - credentials.SecuritySecret.Should().BeEmpty(); - credentials.Method.Should().Be(SmsSignatureGenerator.Method.md5hash); - } + var credentials = Configuration.FromConfiguration(new ConfigurationBuilder().Build()).BuildCredentials(); + credentials.ApiKey.Should().BeEmpty(); + credentials.ApiSecret.Should().BeEmpty(); + credentials.ApplicationId.Should().BeEmpty(); + credentials.ApplicationKey.Should().BeEmpty(); + credentials.SecuritySecret.Should().BeEmpty(); + credentials.Method.Should().Be(SmsSignatureGenerator.Method.md5hash); + } - [Fact] - public void FromConfiguration_ShouldCreateEmptyConfiguration_GivenConfigurationContainsNoElement() - { - var configuration = Configuration.FromConfiguration(new ConfigurationBuilder().Build()); - configuration.ApiKey.Should().BeEmpty(); - configuration.ApiSecret.Should().BeEmpty(); - configuration.ApplicationId.Should().BeEmpty(); - configuration.ApplicationKey.Should().BeEmpty(); - configuration.SecuritySecret.Should().BeEmpty(); - configuration.SigningMethod.Should().BeEmpty(); - configuration.UserAgent.Should().BeEmpty(); - configuration.VonageUrls.Nexmo.Should().Be(new Uri("https://api.nexmo.com")); - configuration.VonageUrls.Rest.Should().Be(new Uri("https://rest.nexmo.com")); - configuration.VonageUrls.Video.Should().Be(new Uri("https://video.api.vonage.com")); - configuration.VonageUrls.Get(VonageUrls.Region.EU).Should().Be(new Uri("https://api-eu.vonage.com")); - configuration.VonageUrls.Get(VonageUrls.Region.APAC).Should().Be(new Uri("https://api-ap.vonage.com")); - configuration.VonageUrls.Get(VonageUrls.Region.US).Should().Be(new Uri("https://api-us.vonage.com")); - configuration.RequestTimeout.Should().BeNone(); - } + [Fact] + public void FromConfiguration_ShouldCreateEmptyConfiguration_GivenConfigurationContainsNoElement() + { + var configuration = Configuration.FromConfiguration(new ConfigurationBuilder().Build()); + configuration.ApiKey.Should().BeEmpty(); + configuration.ApiSecret.Should().BeEmpty(); + configuration.ApplicationId.Should().BeEmpty(); + configuration.ApplicationKey.Should().BeEmpty(); + configuration.SecuritySecret.Should().BeEmpty(); + configuration.SigningMethod.Should().BeEmpty(); + configuration.UserAgent.Should().BeEmpty(); + configuration.VonageUrls.Nexmo.Should().Be(new Uri("https://api.nexmo.com")); + configuration.VonageUrls.Rest.Should().Be(new Uri("https://rest.nexmo.com")); + configuration.VonageUrls.Video.Should().Be(new Uri("https://video.api.vonage.com")); + configuration.VonageUrls.Get(VonageUrls.Region.EU).Should().Be(new Uri("https://api-eu.vonage.com")); + configuration.VonageUrls.Get(VonageUrls.Region.APAC).Should().Be(new Uri("https://api-ap.vonage.com")); + configuration.VonageUrls.Get(VonageUrls.Region.US).Should().Be(new Uri("https://api-us.vonage.com")); + configuration.RequestTimeout.Should().BeNone(); + } - [Fact] - public void FromConfiguration_ShouldSetApiKey_GivenConfigurationContainsApiKey() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Api.Key", "RandomValue"}, - }) - .Build()).ApiKey.Should().Be("RandomValue"); + [Fact] + public void FromConfiguration_ShouldSetApiKey_GivenConfigurationContainsApiKey() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Api.Key", "RandomValue"}, + }) + .Build()).ApiKey.Should().Be("RandomValue"); - [Fact] - public void FromConfiguration_ShouldSetApiKSecret_GivenConfigurationContainsApiSecret() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Api.Secret", "RandomValue"}, - }) - .Build()).ApiSecret.Should().Be("RandomValue"); + [Fact] + public void FromConfiguration_ShouldSetApiKSecret_GivenConfigurationContainsApiSecret() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Api.Secret", "RandomValue"}, + }) + .Build()).ApiSecret.Should().Be("RandomValue"); - [Fact] - public void FromConfiguration_ShouldSetApplicationId_GivenConfigurationContainsApplicationId() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Application.Id", "RandomValue"}, - }) - .Build()).ApplicationId.Should().Be("RandomValue"); + [Fact] + public void FromConfiguration_ShouldSetApplicationId_GivenConfigurationContainsApplicationId() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Application.Id", "RandomValue"}, + }) + .Build()).ApplicationId.Should().Be("RandomValue"); - [Fact] - public void FromConfiguration_ShouldSetApplicationKey_GivenConfigurationContainsApplicationKey() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Application.Key", "RandomValue"}, - }) - .Build()).ApplicationKey.Should().Be("RandomValue"); + [Fact] + public void FromConfiguration_ShouldSetApplicationKey_GivenConfigurationContainsApplicationKey() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Application.Key", "RandomValue"}, + }) + .Build()).ApplicationKey.Should().Be("RandomValue"); - [Fact] - public void FromConfiguration_ShouldSetNexmoApiUrl_GivenConfigurationContainsNexmoApiUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Url.Api", "https://api.vonage.com"}, - }) - .Build()).VonageUrls.Nexmo.Should().Be(new Uri("https://api.vonage.com")); + [Fact] + public void FromConfiguration_ShouldSetNexmoApiUrl_GivenConfigurationContainsNexmoApiUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Url.Api", "https://api.vonage.com"}, + }) + .Build()).VonageUrls.Nexmo.Should().Be(new Uri("https://api.vonage.com")); - [Fact] - public void FromConfiguration_ShouldSetRequestTimeout_GivenConfigurationContainsRequestTimeout() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:RequestTimeout", "100"}, - }) - .Build()).RequestTimeout.Should().BeSome(TimeSpan.FromSeconds(100)); + [Fact] + public void FromConfiguration_ShouldSetRequestTimeout_GivenConfigurationContainsRequestTimeout() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:RequestTimeout", "100"}, + }) + .Build()).RequestTimeout.Should().BeSome(TimeSpan.FromSeconds(100)); - [Fact] - public void FromConfiguration_ShouldSetRestApiUrl_GivenConfigurationContainsRestApiUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Url.Rest", "https://api.vonage.com"}, - }) - .Build()).VonageUrls.Rest.Should().Be(new Uri("https://api.vonage.com")); + [Fact] + public void FromConfiguration_ShouldSetRestApiUrl_GivenConfigurationContainsRestApiUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Url.Rest", "https://api.vonage.com"}, + }) + .Build()).VonageUrls.Rest.Should().Be(new Uri("https://api.vonage.com")); - [Fact] - public void FromConfiguration_ShouldSetSecuritySecret_GivenConfigurationContainsSecuritySecret() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - {"vonage:Security_secret", "RandomValue"}, - }) - .Build()).SecuritySecret.Should().Be("RandomValue"); + [Fact] + public void FromConfiguration_ShouldSetSecuritySecret_GivenConfigurationContainsSecuritySecret() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Security_secret", "RandomValue"}, + }) + .Build()).SecuritySecret.Should().Be("RandomValue"); - [Fact] - public void FromConfiguration_ShouldSetSigningMethod_GivenConfigurationContainsSigningMethod() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary + [Fact] + public void FromConfiguration_ShouldSetSigningMethod_GivenConfigurationContainsSigningMethod() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Signing_method", "sha512"}, + }) + .Build()).SigningMethod.Should().Be("sha512"); + + [Fact] + public void FromConfiguration_ShouldSetUserAgent_GivenConfigurationContainsUserAgent() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:UserAgent", "RandomValue"}, + }) + .Build()).UserAgent.Should().Be("RandomValue"); + + [Fact] + public void FromConfiguration_ShouldSetVideoApiUrl_GivenConfigurationContainsVideoApiUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"vonage:Url.Api.Video", "https://api.vonage.com"}, + }) + .Build()).VonageUrls.Video.Should().Be(new Uri("https://api.vonage.com")); + + [Theory] + [InlineData(VonageUrls.Region.US, "vonage:Url.Api.AMER")] + [InlineData(VonageUrls.Region.EU, "vonage:Url.Api.EMEA")] + [InlineData(VonageUrls.Region.APAC, "vonage:Url.Api.APAC")] + public void VonageUrl_ShouldReturnCustomApiUsUrl_GivenConfigurationContainsApiUsUrl(VonageUrls.Region region, + string key) => + Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary { - {"vonage:Signing_method", "sha512"}, - }) - .Build()).SigningMethod.Should().Be("sha512"); + {key, "https://api.com"}, + }).Build()) + .VonageUrls.Get(region).Should().Be(new Uri("https://api.com")); - [Fact] - public void FromConfiguration_ShouldSetUserAgent_GivenConfigurationContainsUserAgent() => - Configuration.FromConfiguration(new ConfigurationBuilder() + [Fact] + public void VonageUrl_ShouldReturnCustomNexmoUrl_GivenConfigurationContainsDefaultUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary + {{"vonage:Url.Api", "https://api.com"}}).Build()) + .VonageUrls.Nexmo.Should().Be(new Uri("https://api.com")); + + [Fact] + public void VonageUrl_ShouldReturnCustomRestUrl_GivenConfigurationContainsRestUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary { - {"vonage:UserAgent", "RandomValue"}, - }) - .Build()).UserAgent.Should().Be("RandomValue"); + {"vonage:Url.Rest", "https://api.com"}, + }).Build()) + .VonageUrls.Rest.Should().Be(new Uri("https://api.com")); - [Fact] - public void FromConfiguration_ShouldSetVideoApiUrl_GivenConfigurationContainsVideoApiUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary + [Fact] + public void VonageUrl_ShouldReturnCustomVideoUrl_GivenConfigurationContainsVideoUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( + new Dictionary { - {"vonage:Url.Api.Video", "https://api.vonage.com"}, - }) - .Build()).VonageUrls.Video.Should().Be(new Uri("https://api.vonage.com")); - - [Theory] - [InlineData(VonageUrls.Region.US, "vonage:Url.Api.AMER")] - [InlineData(VonageUrls.Region.EU, "vonage:Url.Api.EMEA")] - [InlineData(VonageUrls.Region.APAC, "vonage:Url.Api.APAC")] - public void VonageUrl_ShouldReturnCustomApiUsUrl_GivenConfigurationContainsApiUsUrl(VonageUrls.Region region, - string key) => - Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( - new Dictionary - { - {key, "https://api.com"}, - }).Build()) - .VonageUrls.Get(region).Should().Be(new Uri("https://api.com")); - - [Fact] - public void VonageUrl_ShouldReturnCustomNexmoUrl_GivenConfigurationContainsDefaultUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - {{"vonage:Url.Api", "https://api.com"}}).Build()) - .VonageUrls.Nexmo.Should().Be(new Uri("https://api.com")); - - [Fact] - public void VonageUrl_ShouldReturnCustomRestUrl_GivenConfigurationContainsRestUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( - new Dictionary - { - {"vonage:Url.Rest", "https://api.com"}, - }).Build()) - .VonageUrls.Rest.Should().Be(new Uri("https://api.com")); - - [Fact] - public void VonageUrl_ShouldReturnCustomVideoUrl_GivenConfigurationContainsVideoUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection( - new Dictionary - { - {"vonage:Url.Api.Video", "https://api.com"}, - }).Build()) - .VonageUrls.Video.Should().Be(new Uri("https://api.com")); - - [Theory] - [InlineData(VonageUrls.Region.US, "https://api-us.vonage.com")] - public void VonageUrl_ShouldReturnDefaultApiUsUrl(VonageUrls.Region region, string defaultValue) => - Configuration.FromConfiguration(new ConfigurationBuilder().Build()) - .VonageUrls.Get(region).Should().Be(new Uri(defaultValue)); - - [Fact] - public void VonageUrl_ShouldReturnDefaultRestUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder().Build()) - .VonageUrls.Rest.Should().Be(new Uri("https://rest.nexmo.com")); - - [Fact] - public void VonageUrl_ShouldReturnDefaultVideoUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder().Build()) - .VonageUrls.Video.Should().Be(new Uri("https://video.api.vonage.com")); - - [Fact] - public void VonageUrl_ShouldReturnNexmoUrl() => - Configuration.FromConfiguration(new ConfigurationBuilder().Build()) - .VonageUrls.Nexmo.Should().Be(new Uri("https://api.nexmo.com")); + {"vonage:Url.Api.Video", "https://api.com"}, + }).Build()) + .VonageUrls.Video.Should().Be(new Uri("https://api.com")); + + [Theory] + [InlineData(VonageUrls.Region.US, "https://api-us.vonage.com")] + public void VonageUrl_ShouldReturnDefaultApiUsUrl(VonageUrls.Region region, string defaultValue) => + Configuration.FromConfiguration(new ConfigurationBuilder().Build()) + .VonageUrls.Get(region).Should().Be(new Uri(defaultValue)); + + [Fact] + public void VonageUrl_ShouldReturnDefaultRestUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder().Build()) + .VonageUrls.Rest.Should().Be(new Uri("https://rest.nexmo.com")); + + [Fact] + public void VonageUrl_ShouldReturnDefaultVideoUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder().Build()) + .VonageUrls.Video.Should().Be(new Uri("https://video.api.vonage.com")); + + [Fact] + public void VonageUrl_ShouldReturnNexmoUrl() => + Configuration.FromConfiguration(new ConfigurationBuilder().Build()) + .VonageUrls.Nexmo.Should().Be(new Uri("https://api.nexmo.com")); +} + +[Trait("Category", "HttpConnectionPool")] +public class ConnectionLifetimeTest +{ + private readonly ITestOutputHelper output; + + public ConnectionLifetimeTest(ITestOutputHelper output) => this.output = output; + + [Theory] + [InlineData(5, 2, 1)] + [InlineData(5, 5, 2)] + [InlineData(5, 7, 3)] + [InlineData(5, 12, 4)] + [InlineData(10, 20, 4)] + public async Task ShouldRenewConnection_GivenLifetimeDurationHasExpired(int connectionLifetime, int loop, + int expectedResolutionCount) + { + var settings = new Dictionary + { + {"vonage:PooledConnectionLifetime", connectionLifetime.ToString()}, + }; + var refreshedConnections = await this.RefreshConnectionPool(settings, 2, loop); + refreshedConnections.Should().Be(expectedResolutionCount); + } + + [Fact] + public async Task ShouldRenewConnection_GivenIdleTimeoutHasExpired() + { + var settings = new Dictionary + { + {"vonage:PooledConnectionIdleTimeout", "3"}, + {"vonage:PooledConnectionLifetime", "60"}, + }; + var refreshedConnections = await this.RefreshConnectionPool(settings, 5, 5); + refreshedConnections.Should().Be(5); + } + + private async Task RefreshConnectionPool(Dictionary settings, int timerBuffer, int loops) + { + var spy = new EventSpy(this.output); + var helper = TestingContext.WithBasicCredentials("Url.Rest", settings); + helper.Server.Given(WireMock.RequestBuilders.Request.Create().UsingGet()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK)); + var timer = new PeriodicTimer(TimeSpan.FromSeconds(timerBuffer)); + for (var iterations = 0; iterations < loops; iterations++) + { + await timer.WaitForNextTickAsync(); + await helper.VonageClient.AccountClient.GetAccountBalanceAsync(); + } + + spy.ReceivedRequests.Should().Be(loops); + return spy.RefreshedConnections; + } +} + +internal class EventSpy : EventListener +{ + private readonly ITestOutputHelper output; + + public EventSpy(ITestOutputHelper output) => this.output = output; + + public int RefreshedConnections { get; private set; } + + public int ReceivedRequests { get; private set; } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name.StartsWith("System.Net")) + { + this.EnableEvents(eventSource, EventLevel.Informational); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + switch (eventData.EventName) + { + case "ResolutionStart": + this.RefreshedConnections++; + this.output.WriteLine($"[{this.RefreshedConnections}] - " + eventData.EventName + " - " + + DateTime.Now.TimeOfDay); + break; + case "RequestStart": + this.ReceivedRequests++; + this.output.WriteLine($"[{this.ReceivedRequests}] - " + eventData.EventName + " - " + + DateTime.Now.TimeOfDay); + break; + } } } \ No newline at end of file diff --git a/Vonage.Test/TestHelpers/TestingContext.cs b/Vonage.Test/TestHelpers/TestingContext.cs index 72620b978..5e712885f 100644 --- a/Vonage.Test/TestHelpers/TestingContext.cs +++ b/Vonage.Test/TestHelpers/TestingContext.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; using Vonage.Request; using Vonage.Test.Common.TestHelpers; using WireMock.Server; @@ -8,17 +10,14 @@ namespace Vonage.Test.TestHelpers; internal class TestingContext : IDisposable { - private TestingContext(string appSettingsKey, Credentials credentials, string authorizationHeaderValue) + private TestingContext(string appSettingsKey, Credentials credentials, string authorizationHeaderValue, + Dictionary settings) { this.ExpectedAuthorizationHeaderValue = authorizationHeaderValue; this.Server = WireMockServer.Start(); - var configuration = new Configuration - { - Settings = - { - [$"vonage:{appSettingsKey}"] = this.Server.Url, - }, - }; + settings.Add($"vonage:{appSettingsKey}", this.Server.Url); + var configuration = + Configuration.FromConfiguration(new ConfigurationBuilder().AddInMemoryCollection(settings).Build()); this.VonageClient = new VonageClient(credentials, configuration, new TimeProvider()); } @@ -33,10 +32,14 @@ public void Dispose() } public static TestingContext WithBasicCredentials(string appSettingsKey) => - new TestingContext(appSettingsKey, CreateBasicCredentials(), "Basic NzkwZmM1ZTU6QWEzNDU2Nzg5"); + new TestingContext(appSettingsKey, CreateBasicCredentials(), "Basic NzkwZmM1ZTU6QWEzNDU2Nzg5", + new Dictionary()); public static TestingContext WithBearerCredentials(string appSettingsKey) => - new TestingContext(appSettingsKey, CreateBearerCredentials(), "Bearer *"); + new TestingContext(appSettingsKey, CreateBearerCredentials(), "Bearer *", new Dictionary()); + + public static TestingContext WithBasicCredentials(string appSettingsKey, Dictionary settings) => + new TestingContext(appSettingsKey, CreateBasicCredentials(), "Basic NzkwZmM1ZTU6QWEzNDU2Nzg5", settings); private static Credentials CreateBasicCredentials() => Credentials.FromApiKeyAndSecret("790fc5e5", "Aa3456789"); diff --git a/Vonage/Configuration.cs b/Vonage/Configuration.cs index d84799eba..3d81ad80e 100644 --- a/Vonage/Configuration.cs +++ b/Vonage/Configuration.cs @@ -17,7 +17,9 @@ namespace Vonage; /// public sealed class Configuration { + private const int DefaultPooledConnectionIdleTimeout = 60; private const string LoggerCategory = "Vonage.Configuration"; + private const int DefaultPooledConnectionLifetime = 600; static Configuration() { @@ -27,6 +29,7 @@ private Configuration(IConfiguration configuration) { this.Settings = configuration; this.LogAuthenticationCapabilities(LogProvider.GetLogger(LoggerCategory)); + this.ClientHandler = this.BuildDefaultHandler(); } internal Configuration() @@ -36,6 +39,7 @@ internal Configuration() .AddJsonFile("appsettings.json", true, true); this.Settings = builder.Build(); this.LogAuthenticationCapabilities(LogProvider.GetLogger(LoggerCategory)); + this.ClientHandler = this.BuildDefaultHandler(); } private static Maybe RequestsPerSecond => @@ -63,6 +67,16 @@ internal Configuration() /// public string ApplicationKey => this.Settings["vonage:Application.Key"] ?? string.Empty; + private TimeSpan PooledConnectionIdleTimeout => TimeSpan.FromSeconds( + int.TryParse(this.Settings["vonage:PooledConnectionIdleTimeout"], out var idleTimeout) + ? idleTimeout + : DefaultPooledConnectionIdleTimeout); + + private TimeSpan PooledConnectionLifetime => TimeSpan.FromSeconds( + int.TryParse(this.Settings["vonage:PooledConnectionLifetime"], out var idleTimeout) + ? idleTimeout + : DefaultPooledConnectionLifetime); + /// /// Retrieves a configured HttpClient. /// @@ -83,7 +97,7 @@ public HttpClient Client /// /// Exposes an HttpMessageHandler. /// - public HttpMessageHandler ClientHandler { get; set; } = new HttpClientHandler(); + public HttpMessageHandler ClientHandler { get; set; } /// /// Retrieves the unique instance (Singleton). @@ -123,6 +137,12 @@ public HttpClient Client /// public VonageUrls VonageUrls => VonageUrls.FromConfiguration(this.Settings); + private StandardSocketsHttpHandler BuildDefaultHandler() => new StandardSocketsHttpHandler + { + PooledConnectionLifetime = this.PooledConnectionLifetime, + PooledConnectionIdleTimeout = this.PooledConnectionIdleTimeout, + }; + /// /// Builds a Credentials from the current Configuration. /// diff --git a/Vonage/Vonage.csproj b/Vonage/Vonage.csproj index fd4e9f90b..d07fbc5ed 100644 --- a/Vonage/Vonage.csproj +++ b/Vonage/Vonage.csproj @@ -68,6 +68,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive +