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)