From 91408ed371494792082931ef4644cdc7178f2b89 Mon Sep 17 00:00:00 2001 From: Evgeny Valavin <35497422+evgenyvalavin@users.noreply.github.com> Date: Tue, 15 Nov 2022 10:38:14 +0500 Subject: [PATCH] Add support for Timeout.InifiniteTimeSpan (#26) --- .../Arbus.Network.UnitTests.csproj | 49 ++++++++++++------- src/Arbus.Network.UnitTests/TestFixture.cs | 17 ++++--- .../{ => Tests}/HttpClientContextTests.cs | 11 ++--- .../NativeHttpClientTests.cs | 40 ++++++++++++++- src/Arbus.Network.UnitTests/Usings.cs | 1 - .../Abstractions/INativeHttpClient.cs | 2 +- src/Arbus.Network/Arbus.Network.csproj | 11 +++-- .../Extensions/HttpRequestExtensions.cs | 26 +++++++--- .../Extensions/HttpResponseExtensions.cs | 1 + src/Arbus.Network/IsExternalInit.cs | 2 +- src/Arbus.Network/NativeHttpClient.cs | 48 ++++++++++-------- 11 files changed, 141 insertions(+), 67 deletions(-) rename src/Arbus.Network.UnitTests/{ => Tests}/HttpClientContextTests.cs (75%) rename src/Arbus.Network.UnitTests/{Application => Tests}/NativeHttpClientTests.cs (58%) delete mode 100644 src/Arbus.Network.UnitTests/Usings.cs diff --git a/src/Arbus.Network.UnitTests/Arbus.Network.UnitTests.csproj b/src/Arbus.Network.UnitTests/Arbus.Network.UnitTests.csproj index f8290a7..358346d 100644 --- a/src/Arbus.Network.UnitTests/Arbus.Network.UnitTests.csproj +++ b/src/Arbus.Network.UnitTests/Arbus.Network.UnitTests.csproj @@ -1,24 +1,35 @@ - + - - net6.0 - enable - enable + + net6.0 + enable + enable - false - + false + - - - - - - - - + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + - + + + + + + \ No newline at end of file diff --git a/src/Arbus.Network.UnitTests/TestFixture.cs b/src/Arbus.Network.UnitTests/TestFixture.cs index fc9d81d..d4f978b 100644 --- a/src/Arbus.Network.UnitTests/TestFixture.cs +++ b/src/Arbus.Network.UnitTests/TestFixture.cs @@ -1,11 +1,16 @@ -using Moq; - -namespace Arbus.Network.UnitTests; +namespace Arbus.Network.UnitTests; +[TestFixture] public abstract class TestFixture { private readonly MockRepository _mockRepository = new(default); - public Mock CreateMock(MockBehavior mockBehavior = MockBehavior.Strict) where T : class - => _mockRepository.Create(mockBehavior); -} + public Mock CreateMock(MockBehavior mockBehavior = MockBehavior.Strict, params object[] args) where T : class + => _mockRepository.Create(mockBehavior, args); + + [TearDown] + public void AfterEachTest() + { + _mockRepository.VerifyAll(); + } +} \ No newline at end of file diff --git a/src/Arbus.Network.UnitTests/HttpClientContextTests.cs b/src/Arbus.Network.UnitTests/Tests/HttpClientContextTests.cs similarity index 75% rename from src/Arbus.Network.UnitTests/HttpClientContextTests.cs rename to src/Arbus.Network.UnitTests/Tests/HttpClientContextTests.cs index 00a1410..fd46881 100644 --- a/src/Arbus.Network.UnitTests/HttpClientContextTests.cs +++ b/src/Arbus.Network.UnitTests/Tests/HttpClientContextTests.cs @@ -1,19 +1,18 @@ using Arbus.Network.Abstractions; -using Moq; -namespace Arbus.Network.UnitTests; +namespace Arbus.Network.UnitTests.Tests; public class HttpClientContextTests : TestFixture { [Test] public async Task RunEndpointInternal_InvokesSendRequestWithCancellationTokenFromEndpoint() { - using CancellationTokenSource cancellationTokenSource = new(); - CancellationToken cancellationToken = cancellationTokenSource.Token; + using CancellationTokenSource cts = new(); + CancellationToken cancellationToken = cts.Token; var mockApiEndpoint = Mock.Of(x => x.CancellationToken == cancellationToken && x.Path == "http://localhost" && x.Method == HttpMethod.Get); var mockDefaultHttpClient = CreateMock(); - mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny(), cancellationToken)).ReturnsAsync(new HttpResponseMessage()); + mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny(), cancellationToken, It.IsAny())).ReturnsAsync(new HttpResponseMessage()); HttpClientContext httpClientContext = new(mockDefaultHttpClient.Object); @@ -25,7 +24,7 @@ public async Task RunEndpointInternal_InvokesSendRequestWithDefaultCancellationT { var mockApiEndpoint = Mock.Of(x => x.Path == "http://localhost" && x.Method == HttpMethod.Get); var mockDefaultHttpClient = CreateMock(); - mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny(), default)).ReturnsAsync(new HttpResponseMessage()); + mockDefaultHttpClient.Setup(x => x.SendRequest(It.IsAny(), default, It.IsAny())).ReturnsAsync(new HttpResponseMessage()); HttpClientContext httpClientContext = new(mockDefaultHttpClient.Object); diff --git a/src/Arbus.Network.UnitTests/Application/NativeHttpClientTests.cs b/src/Arbus.Network.UnitTests/Tests/NativeHttpClientTests.cs similarity index 58% rename from src/Arbus.Network.UnitTests/Application/NativeHttpClientTests.cs rename to src/Arbus.Network.UnitTests/Tests/NativeHttpClientTests.cs index 94f710c..7f49d4b 100644 --- a/src/Arbus.Network.UnitTests/Application/NativeHttpClientTests.cs +++ b/src/Arbus.Network.UnitTests/Tests/NativeHttpClientTests.cs @@ -1,7 +1,8 @@ using Arbus.Network.Abstractions; using Arbus.Network.Exceptions; +using Arbus.Network.Extensions; -namespace Arbus.Network.UnitTests.Application; +namespace Arbus.Network.UnitTests.Tests; public class NativeHttpClientTests : TestFixture { @@ -43,4 +44,39 @@ public void EnsureNetworkAvailable_NetworkAvailable_NoException() Assert.DoesNotThrow(() => nativeHttpClient.EnsureNetworkAvailable()); } -} + + [Test] + public void GetTimeoutCts_InfiniteTimeSpan_AssertNullCts() + { + using HttpRequestMessage timeout = new(); + timeout.SetTimeout(Timeout.InfiniteTimeSpan); + + var cts = NativeHttpClient.GetTimeoutCts(timeout, default); + + Assert.IsNull(cts); + } + + [Test] + public void GetTimeoutCts_NotInfiniteTimeSpan_AssertNotNUllCts() + { + using HttpRequestMessage timeout = new(); + timeout.SetTimeout(TimeSpan.FromSeconds(1)); + + using var cts = NativeHttpClient.GetTimeoutCts(timeout, default); + + Assert.NotNull(cts); + } + + [Test] + public void GetTimeoutCts_CancellFirstToken_AssertSecondIsCancelled() + { + using HttpRequestMessage timeout = new(); + timeout.SetTimeout(TimeSpan.FromSeconds(1)); + using var cts1 = new CancellationTokenSource(); + cts1.Cancel(); + + using var cts2 = NativeHttpClient.GetTimeoutCts(timeout, cts1.Token); + + Assert.IsTrue(cts2?.Token.IsCancellationRequested); + } +} \ No newline at end of file diff --git a/src/Arbus.Network.UnitTests/Usings.cs b/src/Arbus.Network.UnitTests/Usings.cs deleted file mode 100644 index cefced4..0000000 --- a/src/Arbus.Network.UnitTests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using NUnit.Framework; \ No newline at end of file diff --git a/src/Arbus.Network/Abstractions/INativeHttpClient.cs b/src/Arbus.Network/Abstractions/INativeHttpClient.cs index dad0fc0..146208d 100644 --- a/src/Arbus.Network/Abstractions/INativeHttpClient.cs +++ b/src/Arbus.Network/Abstractions/INativeHttpClient.cs @@ -2,7 +2,7 @@ { public interface INativeHttpClient { - Task SendRequest(HttpRequestMessage request, CancellationToken cancellationToken); + Task SendRequest(HttpRequestMessage request, CancellationToken cancellationToken, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead); Task GetString(string url, TimeSpan? timeout = default); Task GetString(Uri uri, TimeSpan? timeout = default); } diff --git a/src/Arbus.Network/Arbus.Network.csproj b/src/Arbus.Network/Arbus.Network.csproj index a4fdefd..4e511be 100644 --- a/src/Arbus.Network/Arbus.Network.csproj +++ b/src/Arbus.Network/Arbus.Network.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 - 10 + netstandard2.0;net6.0 + Latest enable enable @@ -13,6 +13,9 @@ https://github.com/ArbusBiz/Arbus.Network - + + + + diff --git a/src/Arbus.Network/Extensions/HttpRequestExtensions.cs b/src/Arbus.Network/Extensions/HttpRequestExtensions.cs index 342cef8..35961a6 100644 --- a/src/Arbus.Network/Extensions/HttpRequestExtensions.cs +++ b/src/Arbus.Network/Extensions/HttpRequestExtensions.cs @@ -2,16 +2,28 @@ public static class HttpRequestExtensions { - private const string _timeoutProperyKey = "Timeout"; - private const int _defaultTimeoutInSeconds = 20; + private const string _key = "Timeout"; + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(100); +#if NET6_0_OR_GREATER + private static HttpRequestOptionsKey _optionsKey = new(_key); +#endif public static void SetTimeout(this HttpRequestMessage request, TimeSpan timeout) - => request.Properties[_timeoutProperyKey] = timeout; +#if NETSTANDARD + => request.Properties[_key] = timeout; +#else + => request.Options.Set(_optionsKey, timeout); +#endif public static TimeSpan GetTimeout(this HttpRequestMessage request) { - return request.Properties.TryGetValue(_timeoutProperyKey, out object value) - ? (TimeSpan)value - : TimeSpan.FromSeconds(_defaultTimeoutInSeconds); +#if NETSTANDARD + if (request.Properties.TryGetValue(_key, out object value)) + return (TimeSpan)value; + return _defaultTimeout; +#else + return request.Options.TryGetValue(_optionsKey, out var value) + ? value : _defaultTimeout; +#endif } -} +} \ No newline at end of file diff --git a/src/Arbus.Network/Extensions/HttpResponseExtensions.cs b/src/Arbus.Network/Extensions/HttpResponseExtensions.cs index fcbe32e..c1b10c8 100644 --- a/src/Arbus.Network/Extensions/HttpResponseExtensions.cs +++ b/src/Arbus.Network/Extensions/HttpResponseExtensions.cs @@ -2,6 +2,7 @@ public static class HttpResponseExtensions { + [Obsolete("Do not use. Will be removed.")] public static string ToReadableString(this HttpResponseMessage response) => $"\n{response.RequestMessage.Method} {response.RequestMessage.RequestUri} {response.StatusCode} {(int)response.StatusCode}"; } diff --git a/src/Arbus.Network/IsExternalInit.cs b/src/Arbus.Network/IsExternalInit.cs index 3e1d5db..451a15e 100644 --- a/src/Arbus.Network/IsExternalInit.cs +++ b/src/Arbus.Network/IsExternalInit.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace System.Runtime.CompilerServices +namespace Arbus.Network { [EditorBrowsable(EditorBrowsableState.Never)] internal static class IsExternalInit { } diff --git a/src/Arbus.Network/NativeHttpClient.cs b/src/Arbus.Network/NativeHttpClient.cs index 0fe6f19..0c31e26 100644 --- a/src/Arbus.Network/NativeHttpClient.cs +++ b/src/Arbus.Network/NativeHttpClient.cs @@ -8,19 +8,20 @@ namespace Arbus.Network; public class NativeHttpClient : INativeHttpClient { - private static readonly HttpClient _httpClient = new(); + private static readonly HttpClient _httpClient = new() + { + Timeout = Timeout.InfiniteTimeSpan + }; private readonly INetworkManager _networkManager; - public NativeHttpClient(INetworkManager networkManager) + public NativeHttpClient(INetworkManager networkManager, ProductInfoHeaderValue userAgent) : this(networkManager) { - _networkManager = networkManager; + _httpClient.DefaultRequestHeaders.UserAgent.Add(userAgent); } - - public NativeHttpClient(INetworkManager networkManager, ProductInfoHeaderValue userAgent) + + public NativeHttpClient(INetworkManager networkManager) { _networkManager = networkManager; - _httpClient.DefaultRequestHeaders.UserAgent.Add(userAgent); - } public Task GetString(string uri, TimeSpan? timeout = null) => GetString(new Uri(uri), timeout); @@ -34,24 +35,18 @@ public async Task GetString(Uri uri, TimeSpan? timeout = null) return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } - public virtual Task Send(HttpRequestMessage httpRequest, CancellationToken timeout, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead) + public virtual async Task SendRequest(HttpRequestMessage request, CancellationToken cancellationToken, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead) { - return _httpClient.SendAsync(httpRequest, httpCompletionOption, timeout); - } - - public async Task SendRequest(HttpRequestMessage request, CancellationToken cancellationToken) - { - using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + using var timeoutCts = GetTimeoutCts(request, cancellationToken); try { - linkedTokenSource.CancelAfter(request.GetTimeout()); - var response = await Send(request, linkedTokenSource.Token).ConfigureAwait(false); + var response = await _httpClient.SendAsync(request, httpCompletionOption, timeoutCts?.Token ?? cancellationToken).ConfigureAwait(false); return await EnsureSuccessResponse(response).ConfigureAwait(false); } catch (Exception) when (cancellationToken.IsCancellationRequested is false) { EnsureNetworkAvailable(); - EnsureNoTimeout(linkedTokenSource); + EnsureNoTimeout(timeoutCts); throw; } finally @@ -60,15 +55,28 @@ public async Task SendRequest(HttpRequestMessage request, C } } + public static CancellationTokenSource? GetTimeoutCts(HttpRequestMessage request, CancellationToken cancellationToken) + { + var timeout = request.GetTimeout(); + var hasTimeout = timeout != Timeout.InfiniteTimeSpan; + if (hasTimeout) + { + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); + return cts; + } + return default; + } + public void EnsureNetworkAvailable() { if (_networkManager.IsNetworkAvailable is false) throw new NoNetoworkConnectionException(); } - public static void EnsureNoTimeout(CancellationTokenSource linkedTokenSource) + public static void EnsureNoTimeout(CancellationTokenSource? cts) { - if (linkedTokenSource.IsCancellationRequested) + if (cts != null && cts.IsCancellationRequested) throw new HttpTimeoutException(); } @@ -84,7 +92,7 @@ public virtual Task HandleNotSuccessStatusCode(HttpResponse return HandleAnyResponse(response); } - public async Task HandleProblemDetailsResponse(HttpResponseMessage response) + public static async Task HandleProblemDetailsResponse(HttpResponseMessage response) { var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var problemDetails = await DefaultJsonSerializer.DeserializeAsync(responseStream).ConfigureAwait(false)