diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ae8ad23..38d78dc 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -17,10 +17,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Setup .NET 8.0
+ - name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Install Kampose
run: dotnet tool install --global kampose
@@ -43,3 +43,4 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./.site
+ keep_files: false
diff --git a/src/Kampute.HttpClient.DataContract/HttpRestClientXmlExtensions.cs b/src/Kampute.HttpClient.DataContract/HttpRestClientXmlExtensions.cs
index cbade5f..1f696de 100644
--- a/src/Kampute.HttpClient.DataContract/HttpRestClientXmlExtensions.cs
+++ b/src/Kampute.HttpClient.DataContract/HttpRestClientXmlExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.DataContract package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.DataContract/Kampute.HttpClient.DataContract.csproj b/src/Kampute.HttpClient.DataContract/Kampute.HttpClient.DataContract.csproj
index d2d3320..6619227 100644
--- a/src/Kampute.HttpClient.DataContract/Kampute.HttpClient.DataContract.csproj
+++ b/src/Kampute.HttpClient.DataContract/Kampute.HttpClient.DataContract.csproj
@@ -5,9 +5,9 @@
Kampute.HttpClient.DataContract
This package is an extension package for Kampute.HttpClient, enhancing it to manage application/xml content types, using DataContractSerializer for serialization and deserialization of XML responses and payloads.
Kambiz Khojasteh
- 2.4.0
+ 2.5.0
Kampute
- Copyright (c) 2024 Kampute
+ Copyright (c) 2025 Kampute
latest
enable
true
diff --git a/src/Kampute.HttpClient.DataContract/NamespaceDoc.cs b/src/Kampute.HttpClient.DataContract/NamespaceDoc.cs
index af65811..3c4dfd0 100644
--- a/src/Kampute.HttpClient.DataContract/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient.DataContract/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.DataContract/XmlContent.cs b/src/Kampute.HttpClient.DataContract/XmlContent.cs
index d0be6e5..0c76a93 100644
--- a/src/Kampute.HttpClient.DataContract/XmlContent.cs
+++ b/src/Kampute.HttpClient.DataContract/XmlContent.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.DataContract package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.DataContract/XmlContentDeserializer.cs b/src/Kampute.HttpClient.DataContract/XmlContentDeserializer.cs
index eb51a92..c501122 100644
--- a/src/Kampute.HttpClient.DataContract/XmlContentDeserializer.cs
+++ b/src/Kampute.HttpClient.DataContract/XmlContentDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.DataContract package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Json/HttpRestClientJsonExtensions.cs b/src/Kampute.HttpClient.Json/HttpRestClientJsonExtensions.cs
index bdcfe5b..288f4d3 100644
--- a/src/Kampute.HttpClient.Json/HttpRestClientJsonExtensions.cs
+++ b/src/Kampute.HttpClient.Json/HttpRestClientJsonExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Json package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Json/JsonContent.cs b/src/Kampute.HttpClient.Json/JsonContent.cs
index 8dac131..2f5e08c 100644
--- a/src/Kampute.HttpClient.Json/JsonContent.cs
+++ b/src/Kampute.HttpClient.Json/JsonContent.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Json package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Json/JsonContentDeserializer.cs b/src/Kampute.HttpClient.Json/JsonContentDeserializer.cs
index 6751bc9..56c1c45 100644
--- a/src/Kampute.HttpClient.Json/JsonContentDeserializer.cs
+++ b/src/Kampute.HttpClient.Json/JsonContentDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Json package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj b/src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj
index 6fdaa68..1029cbc 100644
--- a/src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj
+++ b/src/Kampute.HttpClient.Json/Kampute.HttpClient.Json.csproj
@@ -5,9 +5,9 @@
Kampute.HttpClient.Json
This package is an extension package for Kampute.HttpClient, enhancing it to manage application/json content types, using System.Text.Json library for serialization and deserialization of JSON responses and payloads.
Kambiz Khojasteh
- 2.4.0
+ 2.5.0
Kampute
- Copyright (c) 2024 Kampute
+ Copyright (c) 2025 Kampute
latest
enable
true
@@ -36,7 +36,7 @@
-
+
diff --git a/src/Kampute.HttpClient.Json/NamespaceDoc.cs b/src/Kampute.HttpClient.Json/NamespaceDoc.cs
index 084c71f..4c03a31 100644
--- a/src/Kampute.HttpClient.Json/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient.Json/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.NewtonsoftJson/HttpRestClientJsonExtensions.cs b/src/Kampute.HttpClient.NewtonsoftJson/HttpRestClientJsonExtensions.cs
index e0e780f..c6894d5 100644
--- a/src/Kampute.HttpClient.NewtonsoftJson/HttpRestClientJsonExtensions.cs
+++ b/src/Kampute.HttpClient.NewtonsoftJson/HttpRestClientJsonExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.NewtonsoftJson package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.NewtonsoftJson/JsonContent.cs b/src/Kampute.HttpClient.NewtonsoftJson/JsonContent.cs
index 1fbfb6d..c714dc0 100644
--- a/src/Kampute.HttpClient.NewtonsoftJson/JsonContent.cs
+++ b/src/Kampute.HttpClient.NewtonsoftJson/JsonContent.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.NewtonsoftJson package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.NewtonsoftJson/JsonContentDeserializer.cs b/src/Kampute.HttpClient.NewtonsoftJson/JsonContentDeserializer.cs
index 3de04ad..8135531 100644
--- a/src/Kampute.HttpClient.NewtonsoftJson/JsonContentDeserializer.cs
+++ b/src/Kampute.HttpClient.NewtonsoftJson/JsonContentDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.NewtonsoftJson package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.NewtonsoftJson/Kampute.HttpClient.NewtonsoftJson.csproj b/src/Kampute.HttpClient.NewtonsoftJson/Kampute.HttpClient.NewtonsoftJson.csproj
index 5d3f663..a83bf10 100644
--- a/src/Kampute.HttpClient.NewtonsoftJson/Kampute.HttpClient.NewtonsoftJson.csproj
+++ b/src/Kampute.HttpClient.NewtonsoftJson/Kampute.HttpClient.NewtonsoftJson.csproj
@@ -5,9 +5,9 @@
Kampute.HttpClient.NewtonsoftJson
This package is an extension package for Kampute.HttpClient, enhancing it to manage application/json content types, using Newtonsoft.Json library for serialization and deserialization of JSON responses and payloads.
Kambiz Khojasteh
- 2.4.0
+ 2.5.0
Kampute
- Copyright (c) 2024 Kampute
+ Copyright (c) 2025 Kampute
latest
enable
true
@@ -36,7 +36,7 @@
-
+
diff --git a/src/Kampute.HttpClient.NewtonsoftJson/NamespaceDoc.cs b/src/Kampute.HttpClient.NewtonsoftJson/NamespaceDoc.cs
index 6af8af3..afb947e 100644
--- a/src/Kampute.HttpClient.NewtonsoftJson/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient.NewtonsoftJson/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Xml/HttpRestClientXmlExtensions.cs b/src/Kampute.HttpClient.Xml/HttpRestClientXmlExtensions.cs
index a178418..c0f8c9f 100644
--- a/src/Kampute.HttpClient.Xml/HttpRestClientXmlExtensions.cs
+++ b/src/Kampute.HttpClient.Xml/HttpRestClientXmlExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Xml package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj b/src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj
index 3bacaad..b0f9b0b 100644
--- a/src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj
+++ b/src/Kampute.HttpClient.Xml/Kampute.HttpClient.Xml.csproj
@@ -5,9 +5,9 @@
Kampute.HttpClient.Xml
This package is an extension package for Kampute.HttpClient, enhancing it to manage application/xml content types, using XmlSerializer for serialization and deserialization of XML responses and payloads.
Kambiz Khojasteh
- 2.4.0
+ 2.5.0
Kampute
- Copyright (c) 2024 Kampute
+ Copyright (c) 2025 Kampute
latest
enable
true
diff --git a/src/Kampute.HttpClient.Xml/NamespaceDoc.cs b/src/Kampute.HttpClient.Xml/NamespaceDoc.cs
index e04ff5e..c55a2d8 100644
--- a/src/Kampute.HttpClient.Xml/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient.Xml/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Xml/XmlContent.cs b/src/Kampute.HttpClient.Xml/XmlContent.cs
index be8ec7a..41bcf09 100644
--- a/src/Kampute.HttpClient.Xml/XmlContent.cs
+++ b/src/Kampute.HttpClient.Xml/XmlContent.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Xml package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient.Xml/XmlContentDeserializer.cs b/src/Kampute.HttpClient.Xml/XmlContentDeserializer.cs
index adb495a..7ac0339 100644
--- a/src/Kampute.HttpClient.Xml/XmlContentDeserializer.cs
+++ b/src/Kampute.HttpClient.Xml/XmlContentDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient.Xml package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/AuthSchemes.cs b/src/Kampute.HttpClient/AuthSchemes.cs
index 61c3706..f37718a 100644
--- a/src/Kampute.HttpClient/AuthSchemes.cs
+++ b/src/Kampute.HttpClient/AuthSchemes.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/BackoffStrategies.cs b/src/Kampute.HttpClient/BackoffStrategies.cs
index e57fdf4..e305cae 100644
--- a/src/Kampute.HttpClient/BackoffStrategies.cs
+++ b/src/Kampute.HttpClient/BackoffStrategies.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Content/Abstracts/NamespaceDoc.cs b/src/Kampute.HttpClient/Content/Abstracts/NamespaceDoc.cs
index 339a989..3e78d72 100644
--- a/src/Kampute.HttpClient/Content/Abstracts/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Content/Abstracts/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Content/Compression/Abstracts/NamespaceDoc.cs b/src/Kampute.HttpClient/Content/Compression/Abstracts/NamespaceDoc.cs
index 97f16df..8fcf83d 100644
--- a/src/Kampute.HttpClient/Content/Compression/Abstracts/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Content/Compression/Abstracts/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Content/Compression/NamespaceDoc.cs b/src/Kampute.HttpClient/Content/Compression/NamespaceDoc.cs
index 8eff1fd..e915de3 100644
--- a/src/Kampute.HttpClient/Content/Compression/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Content/Compression/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Content/NamespaceDoc.cs b/src/Kampute.HttpClient/Content/NamespaceDoc.cs
index f3c416d..5808a4f 100644
--- a/src/Kampute.HttpClient/Content/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Content/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/ErrorHandlers/Abstracts/NamespaceDoc.cs b/src/Kampute.HttpClient/ErrorHandlers/Abstracts/NamespaceDoc.cs
new file mode 100644
index 0000000..1ef59a7
--- /dev/null
+++ b/src/Kampute.HttpClient/ErrorHandlers/Abstracts/NamespaceDoc.cs
@@ -0,0 +1,13 @@
+// Copyright (C) 2025 Kampute
+//
+// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
+// See the LICENSE file in the project root for the full license text.
+
+namespace Kampute.HttpClient.ErrorHandlers.Abstracts
+{
+ ///
+ /// This namespace contains abstract classes for handling transient HTTP error responses by implementing
+ /// backoff and retry strategies.
+ ///
+ internal static class NamespaceDoc { }
+}
diff --git a/src/Kampute.HttpClient/ErrorHandlers/Abstracts/RetryableHttpErrorHandler.cs b/src/Kampute.HttpClient/ErrorHandlers/Abstracts/RetryableHttpErrorHandler.cs
new file mode 100644
index 0000000..eef599d
--- /dev/null
+++ b/src/Kampute.HttpClient/ErrorHandlers/Abstracts/RetryableHttpErrorHandler.cs
@@ -0,0 +1,125 @@
+// Copyright (C) 2025 Kampute
+//
+// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
+// See the LICENSE file in the project root for the full license text.
+
+namespace Kampute.HttpClient.ErrorHandlers.Abstracts
+{
+ using Kampute.HttpClient.Interfaces;
+ using System;
+ using System.Net;
+ using System.Threading;
+ using System.Threading.Tasks;
+
+ ///
+ /// Provides the base functionality for handling HTTP responses with transient error status codes by attempting to back off and
+ /// retry the request according to a specified or default backoff strategy.
+ ///
+ ///
+ /// This handler class is designed to be extended for specific transient error status codes. It offers a mechanism to respond to
+ /// transient HTTP errors by retrying the request after a delay. The delay duration and retry logic can be customized through the
+ /// delegate.
+ ///
+ ///
+ ///
+ public abstract class RetryableHttpErrorHandler : IHttpErrorHandler
+ {
+ ///
+ /// A delegate that allows customization of the backoff strategy when responses with transient error status codes are received.
+ ///
+ ///
+ /// A function that takes an and an optional representing
+ /// the suggested retry time, and returns an to be used for the retry operation.
+ ///
+ ///
+ ///
+ /// If this delegate is set and returns an , the returned strategy is used for the retry operation.
+ /// If it is not set, or returns , a default behavior is applied.
+ ///
+ ///
+ /// The delegate receives the following parameters:
+ ///
+ /// -
+ /// context
+ ///
+ /// Provides context about the HTTP response that indicates a transient error. It is encapsulated within an
+ /// instance, allowing for an informed decision on the retry strategy.
+ ///
+ ///
+ /// -
+ /// retryTime
+ ///
+ /// Advises on the next retry attempt timing as a value if the response suggests one. If the response
+ /// does not include a suggested retry time, the value will be .
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Func? OnBackoffStrategy { get; set; }
+
+ ///
+ /// Determines whether this handler can process the specified HTTP status code.
+ ///
+ /// The HTTP status code to evaluate.
+ /// if the handler can process the status code; otherwise, .
+ public abstract bool CanHandle(HttpStatusCode statusCode);
+
+ ///
+ /// Extracts the suggested retry time from the HTTP response's header, if present.
+ ///
+ /// The context containing information about the HTTP response.
+ /// The suggested to retry the request, or if the header is not present.
+ /// Thrown if is .
+ protected virtual DateTimeOffset? GetSuggestedRetryTime(HttpResponseErrorContext ctx)
+ {
+ if (ctx is null)
+ throw new ArgumentNullException(nameof(ctx));
+
+ ctx.Response.Headers.TryExtractRetryAfterTime(out var retryTime);
+ return retryTime;
+ }
+
+ ///
+ /// Provides the default backoff strategy when no custom strategy is specified.
+ ///
+ /// The context containing information about the HTTP response.
+ /// The suggested retry time, if any.
+ /// An representing the default backoff strategy.
+ /// Thrown if is .
+ protected virtual IHttpBackoffProvider GetDefaultStrategy(HttpResponseErrorContext ctx, DateTimeOffset? retryTime)
+ {
+ if (ctx is null)
+ throw new ArgumentNullException(nameof(ctx));
+
+ return retryTime.HasValue ? BackoffStrategies.Once(retryTime.Value) : ctx.Client.BackoffStrategy;
+ }
+
+ ///
+ /// Creates a scheduler for retrying the failed request based on the error context.
+ ///
+ /// The context containing information about the HTTP response that indicates a failure.
+ /// An that schedules the retry attempts.
+ /// Thrown if is .
+ ///
+ /// The method uses when available. If the delegate is not provided or returns
+ /// , and the response includes a suggested retry time, a single retry at that time is used.
+ /// Otherwise the client's default backoff strategy is used.
+ ///
+ protected virtual IRetryScheduler? CreateScheduler(HttpResponseErrorContext ctx)
+ {
+ if (ctx is null)
+ throw new ArgumentNullException(nameof(ctx));
+
+ var retryTime = GetSuggestedRetryTime(ctx);
+ var strategy = OnBackoffStrategy?.Invoke(ctx, retryTime) ?? GetDefaultStrategy(ctx, retryTime);
+ return strategy.CreateScheduler(ctx);
+ }
+
+ ///
+ Task IHttpErrorHandler.DecideOnRetryAsync(HttpResponseErrorContext ctx, CancellationToken cancellationToken)
+ {
+ return ctx.ScheduleRetryAsync(CreateScheduler, cancellationToken);
+ }
+ }
+}
diff --git a/src/Kampute.HttpClient/ErrorHandlers/DynamicHttpErrorHandler.cs b/src/Kampute.HttpClient/ErrorHandlers/DynamicHttpErrorHandler.cs
index 7635108..9328361 100644
--- a/src/Kampute.HttpClient/ErrorHandlers/DynamicHttpErrorHandler.cs
+++ b/src/Kampute.HttpClient/ErrorHandlers/DynamicHttpErrorHandler.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/ErrorHandlers/HttpError401Handler.cs b/src/Kampute.HttpClient/ErrorHandlers/HttpError401Handler.cs
index 44638bb..5bd7ce6 100644
--- a/src/Kampute.HttpClient/ErrorHandlers/HttpError401Handler.cs
+++ b/src/Kampute.HttpClient/ErrorHandlers/HttpError401Handler.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/ErrorHandlers/HttpError429Handler.cs b/src/Kampute.HttpClient/ErrorHandlers/HttpError429Handler.cs
index b0ae86c..8f5269c 100644
--- a/src/Kampute.HttpClient/ErrorHandlers/HttpError429Handler.cs
+++ b/src/Kampute.HttpClient/ErrorHandlers/HttpError429Handler.cs
@@ -1,15 +1,14 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
namespace Kampute.HttpClient.ErrorHandlers
{
+ using Kampute.HttpClient.ErrorHandlers.Abstracts;
using Kampute.HttpClient.Interfaces;
using System;
using System.Net;
- using System.Threading;
- using System.Threading.Tasks;
///
/// Handles '429 Too Many Requests' HTTP responses by attempting to back off and retry the request according to a specified or
@@ -17,92 +16,41 @@ namespace Kampute.HttpClient.ErrorHandlers
///
///
/// This handler provides a mechanism to respond to HTTP 429 errors by retrying the request after a delay. The delay duration and
- /// retry logic can be customized through the delegate. If the delegate is not provided, or does not
- /// specify a strategy, the handler will look for a rate limit reset header in the response. If the header is present, its value is
- /// used to determine the backoff duration. If the header is not present, no retries will be attempted.
+ /// retry logic can be customized through the delegate. If the delegate
+ /// is not provided, or does not specify a strategy, the handler will look for a rate limit reset header in the response. If the
+ /// header is present, its value is used to determine the backoff duration. If the header is not present, no retries will be attempted.
///
///
- public class HttpError429Handler : IHttpErrorHandler
+ public class HttpError429Handler : RetryableHttpErrorHandler
{
- ///
- /// A delegate that allows customization of the backoff strategy when a 429 Too Many Requests' response is received.
- ///
- ///
- /// A function that takes an and an optional representing
- /// the rate limit reset time, and returns an to define the backoff strategy.
- ///
- ///
- ///
- /// If this delegate is set and returns an , the returned strategy is used for the retry operation.
- /// If it is not set, or returns , the handler will defer to the Retry-After header in the response.
- ///
- ///
- /// The delegate receives the following parameters:
- ///
- /// -
- /// context
- ///
- /// Provides context about the HTTP response indicating a '429 Too Many Requests' error. It is encapsulated within
- /// an instance, allowing for an informed decision on the retry strategy.
- ///
- ///
- /// -
- /// resetTime
- ///
- /// Indicates the time when the rate limit will be lifted as a value. If the server specifies
- /// a reset time via response headers, this parameter provides that time, allowing the client to know when to resume requests.
- /// If the server does not specify a reset time, the value will be .
- ///
- ///
- ///
- ///
- ///
- public Func? OnBackoffStrategy { get; set; }
-
- ///
- /// Determines whether this handler can process the specified HTTP status code.
- ///
- /// The HTTP status code to evaluate.
- /// if the handler can process the status code; otherwise, .
+ ///
///
/// This implementation specifically handles the HTTP '429 Too Many Requests' status code.
///
- public bool CanHandle(HttpStatusCode statusCode) =>
+ public sealed override bool CanHandle(HttpStatusCode statusCode) =>
#if NETSTANDARD2_1_OR_GREATER
statusCode == HttpStatusCode.TooManyRequests;
#else
statusCode == (HttpStatusCode)429;
#endif
- ///
- /// Creates a scheduler for retrying the failed request based on the error context.
- ///
- /// The context containing information about the HTTP response that indicates a failure.
- /// An that schedules the retry attempts.
- /// Thrown if is .
- ///
- /// This method first attempts to use the delegate to obtain a retry strategy. If the delegate is not
- /// provided or returns , and a rate limit reset header is present, the value of this header is used to create a retry delay.
- /// If neither condition is met, no retries will be attempted.
- ///
- protected virtual IRetryScheduler? CreateScheduler(HttpResponseErrorContext ctx)
+ ///
+ protected override DateTimeOffset? GetSuggestedRetryTime(HttpResponseErrorContext ctx)
{
if (ctx is null)
throw new ArgumentNullException(nameof(ctx));
ctx.Response.Headers.TryExtractRateLimitResetTime(out var resetTime);
-
- var strategy = OnBackoffStrategy?.Invoke(ctx, resetTime);
- if (strategy is null && resetTime is DateTimeOffset retryAfter)
- strategy = BackoffStrategies.Once(retryAfter);
-
- return strategy?.CreateScheduler(ctx);
+ return resetTime;
}
///
- Task IHttpErrorHandler.DecideOnRetryAsync(HttpResponseErrorContext ctx, CancellationToken cancellationToken)
+ protected override IHttpBackoffProvider GetDefaultStrategy(HttpResponseErrorContext ctx, DateTimeOffset? retryTime)
{
- return ctx.ScheduleRetryAsync(CreateScheduler, cancellationToken);
+ if (ctx is null)
+ throw new ArgumentNullException(nameof(ctx));
+
+ return retryTime.HasValue ? BackoffStrategies.Once(retryTime.Value) : BackoffStrategies.None;
}
}
}
diff --git a/src/Kampute.HttpClient/ErrorHandlers/HttpError503Handler.cs b/src/Kampute.HttpClient/ErrorHandlers/HttpError503Handler.cs
index 4c1ae5e..640bf11 100644
--- a/src/Kampute.HttpClient/ErrorHandlers/HttpError503Handler.cs
+++ b/src/Kampute.HttpClient/ErrorHandlers/HttpError503Handler.cs
@@ -1,104 +1,39 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
namespace Kampute.HttpClient.ErrorHandlers
{
- using Kampute.HttpClient.Interfaces;
- using System;
+ using Kampute.HttpClient.ErrorHandlers.Abstracts;
using System.Net;
- using System.Threading;
- using System.Threading.Tasks;
///
/// Handles '503 Service Unavailable' HTTP responses by attempting to back off and retry the request according to a specified or
/// default backoff strategy.
///
///
+ ///
/// This handler provides a mechanism to respond to HTTP 503 errors by retrying the request after a delay. The delay duration and
- /// retry logic can be customized through the delegate. If the delegate is not provided, or does not
- /// specify a strategy, the handler will look for a Retry-After header in the response. If the Retry-After header is
- /// present, its value is used to determine the backoff duration. If the header is not present, the default backoff strategy of the
- /// is used.
+ /// retry logic can be customized through the delegate. If the delegate
+ /// is not provided, or does not specify a strategy, the handler will look for a Retry-After header in the response. If the
+ /// Retry-After header is present, its value is used to determine the backoff duration. If the header is not present, the
+ /// default backoff strategy of the is used.
+ ///
+ ///
+ /// Consider using if you want to handle multiple transient HTTP errors (including 503) with
+ /// a single handler.
+ ///
///
///
///
- public class HttpError503Handler : IHttpErrorHandler
+ ///
+ public class HttpError503Handler : RetryableHttpErrorHandler
{
- ///
- /// A delegate that allows customization of the backoff strategy when a '503 Service Unavailable' response is received.
- ///
- ///
- /// A function that takes an and an optional representing
- /// the suggested retry time from the Retry-After header, and returns an to be used
- /// for the retry operation.
- ///
- ///
- ///
- /// If this delegate is set and returns an , the returned strategy is used for the retry operation.
- /// If it is not set, or returns , the handler will defer to the Retry-After header in the response or the
- /// client's default backoff strategy.
- ///
- ///
- /// The delegate receives the following parameters:
- ///
- /// -
- /// context
- ///
- /// Provides context about the HTTP response indicating a '503 Service Unavailable' error. It is encapsulated within
- /// an instance, allowing for an informed decision on the retry strategy.
- ///
- ///
- /// -
- /// retryAfter
- ///
- /// Advises on the next retry attempt timing as a value. If the response includes a Retry-After
- /// header, this parameter reflects its value, suggesting an optimal time to retry. If the header is missing, the value is ,
- /// indicating no specific suggestion from the server.
- ///
- ///
- ///
- ///
- ///
- public Func? OnBackoffStrategy { get; set; }
-
- ///
- /// Determines whether this handler can process the specified HTTP status code.
- ///
- /// The HTTP status code to evaluate.
- /// if the handler can process the status code; otherwise, .
+ ///
///
/// This implementation specifically handles the HTTP '503 Service Unavailable' status code.
///
- public virtual bool CanHandle(HttpStatusCode statusCode) => statusCode == HttpStatusCode.ServiceUnavailable;
-
- ///
- /// Creates a scheduler for retrying the failed request based on the error context.
- ///
- /// The context containing information about the HTTP response that indicates a failure.
- /// An that schedules the retry attempts.
- /// Thrown if is .
- ///
- /// This method first attempts to use the delegate to obtain a retry strategy. If the delegate is not
- /// provided or returns , and a Retry-After header is present, the value of this header is used to create a retry
- /// delay. If neither condition is met, the client's default backoff strategy is utilized.
- ///
- protected virtual IRetryScheduler? CreateScheduler(HttpResponseErrorContext ctx)
- {
- if (ctx is null)
- throw new ArgumentNullException(nameof(ctx));
-
- ctx.Response.Headers.TryExtractRetryAfterTime(out var retryAfter);
-
- var strategy = OnBackoffStrategy?.Invoke(ctx, retryAfter) ?? (retryAfter.HasValue ? BackoffStrategies.Once(retryAfter.Value) : ctx.Client.BackoffStrategy);
- return strategy.CreateScheduler(ctx);
- }
-
- ///
- Task IHttpErrorHandler.DecideOnRetryAsync(HttpResponseErrorContext ctx, CancellationToken cancellationToken)
- {
- return ctx.ScheduleRetryAsync(CreateScheduler, cancellationToken);
- }
+ public sealed override bool CanHandle(HttpStatusCode statusCode) => statusCode == HttpStatusCode.ServiceUnavailable;
}
}
diff --git a/src/Kampute.HttpClient/ErrorHandlers/NamespaceDoc.cs b/src/Kampute.HttpClient/ErrorHandlers/NamespaceDoc.cs
index 5c8f3f2..d4047c9 100644
--- a/src/Kampute.HttpClient/ErrorHandlers/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/ErrorHandlers/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/ErrorHandlers/TransientHttpErrorHandler.cs b/src/Kampute.HttpClient/ErrorHandlers/TransientHttpErrorHandler.cs
new file mode 100644
index 0000000..76f056f
--- /dev/null
+++ b/src/Kampute.HttpClient/ErrorHandlers/TransientHttpErrorHandler.cs
@@ -0,0 +1,77 @@
+// Copyright (C) 2025 Kampute
+//
+// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
+// See the LICENSE file in the project root for the full license text.
+
+namespace Kampute.HttpClient.ErrorHandlers
+{
+ using Kampute.HttpClient.ErrorHandlers.Abstracts;
+ using System;
+ using System.Collections.Generic;
+ using System.Net;
+
+ ///
+ /// Handles HTTP responses with a transient error status code by attempting to back off and retry the request according to a specified
+ /// or default backoff strategy.
+ ///
+ ///
+ ///
+ public class TransientHttpErrorHandler : RetryableHttpErrorHandler
+ {
+ private readonly HashSet _handledStatusCodes;
+
+ ///
+ /// Initializes a new instance of the class with default transient error status codes.
+ ///
+ ///
+ /// The default transient error status codes handled by this instance are:
+ ///
+ /// - 408Request Timeout
+ /// - 502Bad Gateway
+ /// - 503Service Unavailable
+ /// - 504Gateway Timeout
+ /// - 507Insufficient Storage
+ /// - 509Bandwidth Limit Exceeded
+ ///
+ ///
+ public TransientHttpErrorHandler()
+ {
+ _handledStatusCodes =
+ [
+ HttpStatusCode.RequestTimeout, // 408 - Request Timeout
+ HttpStatusCode.BadGateway, // 502 - Bad Gateway
+ HttpStatusCode.ServiceUnavailable, // 503 - Service Unavailable
+ HttpStatusCode.GatewayTimeout, // 504 - Gateway Timeout
+ (HttpStatusCode) 507, // 507 - Insufficient Storage
+ (HttpStatusCode) 509, // 509 - Bandwidth Limit Exceeded
+ ];
+ }
+
+ ///
+ /// Initializes a new instance of the class with specified transient error status codes.
+ ///
+ /// The collection of HTTP status codes that this handler will process as transient errors.
+ /// Thrown if is .
+ /// Thrown if is empty.
+ public TransientHttpErrorHandler(IEnumerable statusCodes)
+ {
+ if (statusCodes is null)
+ throw new ArgumentNullException(nameof(statusCodes));
+
+ _handledStatusCodes = [.. statusCodes];
+ if (_handledStatusCodes.Count == 0)
+ throw new ArgumentException("At least one status code must be provided.", nameof(statusCodes));
+ }
+
+ ///
+ /// Gets the collection of HTTP status codes that are considered transient errors and handled by this instance.
+ ///
+ ///
+ /// A read-only collection of values that are treated as transient errors.
+ ///
+ public IReadOnlyCollection HandledStatusCodes => _handledStatusCodes;
+
+ ///
+ public sealed override bool CanHandle(HttpStatusCode statusCode) => _handledStatusCodes.Contains(statusCode);
+ }
+}
diff --git a/src/Kampute.HttpClient/HttpContentDeserializerCollection.cs b/src/Kampute.HttpClient/HttpContentDeserializerCollection.cs
index 4101158..d38f0dd 100644
--- a/src/Kampute.HttpClient/HttpContentDeserializerCollection.cs
+++ b/src/Kampute.HttpClient/HttpContentDeserializerCollection.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpContentException.cs b/src/Kampute.HttpClient/HttpContentException.cs
index 1b0e42d..72295d9 100644
--- a/src/Kampute.HttpClient/HttpContentException.cs
+++ b/src/Kampute.HttpClient/HttpContentException.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpContentExtensions.cs b/src/Kampute.HttpClient/HttpContentExtensions.cs
index cf49f83..4c53ebd 100644
--- a/src/Kampute.HttpClient/HttpContentExtensions.cs
+++ b/src/Kampute.HttpClient/HttpContentExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpErrorHandlerCollection.cs b/src/Kampute.HttpClient/HttpErrorHandlerCollection.cs
index c00826a..77f5ebb 100644
--- a/src/Kampute.HttpClient/HttpErrorHandlerCollection.cs
+++ b/src/Kampute.HttpClient/HttpErrorHandlerCollection.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpErrorHandlerResult.cs b/src/Kampute.HttpClient/HttpErrorHandlerResult.cs
index 2b0b3f2..b3acceb 100644
--- a/src/Kampute.HttpClient/HttpErrorHandlerResult.cs
+++ b/src/Kampute.HttpClient/HttpErrorHandlerResult.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRequestErrorContext.cs b/src/Kampute.HttpClient/HttpRequestErrorContext.cs
index d139fd8..538ddb3 100644
--- a/src/Kampute.HttpClient/HttpRequestErrorContext.cs
+++ b/src/Kampute.HttpClient/HttpRequestErrorContext.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRequestMessageCloneManager.cs b/src/Kampute.HttpClient/HttpRequestMessageCloneManager.cs
index 64863a9..7c3d021 100644
--- a/src/Kampute.HttpClient/HttpRequestMessageCloneManager.cs
+++ b/src/Kampute.HttpClient/HttpRequestMessageCloneManager.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
@@ -70,12 +70,13 @@ public readonly void Dispose()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void DisposeNonOriginalRequest()
{
- if (!ReferenceEquals(_currentRequest, _originalRequest))
- {
- if (_currentRequest.IsCloned())
- _currentRequest.Content = null; // Content is reused, not cloned.
- _currentRequest.Dispose();
- }
+ if (ReferenceEquals(_currentRequest, _originalRequest))
+ return;
+
+ if (_currentRequest.IsCloned())
+ _currentRequest.Content = null; // Content is reused, not cloned.
+
+ _currentRequest.Dispose();
}
}
}
diff --git a/src/Kampute.HttpClient/HttpRequestMessageEventArgs.cs b/src/Kampute.HttpClient/HttpRequestMessageEventArgs.cs
index 80e3612..b565bf0 100644
--- a/src/Kampute.HttpClient/HttpRequestMessageEventArgs.cs
+++ b/src/Kampute.HttpClient/HttpRequestMessageEventArgs.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRequestMessageExtensions.cs b/src/Kampute.HttpClient/HttpRequestMessageExtensions.cs
index 4c3fa56..711ab3f 100644
--- a/src/Kampute.HttpClient/HttpRequestMessageExtensions.cs
+++ b/src/Kampute.HttpClient/HttpRequestMessageExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRequestMessagePropertyKeys.cs b/src/Kampute.HttpClient/HttpRequestMessagePropertyKeys.cs
index cf7bedd..a357d26 100644
--- a/src/Kampute.HttpClient/HttpRequestMessagePropertyKeys.cs
+++ b/src/Kampute.HttpClient/HttpRequestMessagePropertyKeys.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpResponseErrorContext.cs b/src/Kampute.HttpClient/HttpResponseErrorContext.cs
index 94baf26..9e6d69b 100644
--- a/src/Kampute.HttpClient/HttpResponseErrorContext.cs
+++ b/src/Kampute.HttpClient/HttpResponseErrorContext.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpResponseException.cs b/src/Kampute.HttpClient/HttpResponseException.cs
index a3b419b..7dd0bab 100644
--- a/src/Kampute.HttpClient/HttpResponseException.cs
+++ b/src/Kampute.HttpClient/HttpResponseException.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpResponseHeadersExtensions.cs b/src/Kampute.HttpClient/HttpResponseHeadersExtensions.cs
index 4d1cd93..4565378 100644
--- a/src/Kampute.HttpClient/HttpResponseHeadersExtensions.cs
+++ b/src/Kampute.HttpClient/HttpResponseHeadersExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpResponseMessageEventArgs.cs b/src/Kampute.HttpClient/HttpResponseMessageEventArgs.cs
index 99285fa..ed8bb07 100644
--- a/src/Kampute.HttpClient/HttpResponseMessageEventArgs.cs
+++ b/src/Kampute.HttpClient/HttpResponseMessageEventArgs.cs
@@ -1,5 +1,5 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRestClient.cs b/src/Kampute.HttpClient/HttpRestClient.cs
index 9c1fa6c..566a532 100644
--- a/src/Kampute.HttpClient/HttpRestClient.cs
+++ b/src/Kampute.HttpClient/HttpRestClient.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
@@ -484,11 +484,11 @@ CancellationToken cancellationToken
if (response is null)
throw new ArgumentNullException(nameof(response));
- var context = new HttpResponseErrorContext(this, request, response, error);
+ var ctx = new HttpResponseErrorContext(this, request, response, error);
foreach (var errorHandler in ErrorHandlers.GetHandlersFor(response.StatusCode))
{
- var decision = await errorHandler.DecideOnRetryAsync(context, cancellationToken).ConfigureAwait(false);
+ var decision = await errorHandler.DecideOnRetryAsync(ctx, cancellationToken).ConfigureAwait(false);
if (decision.RequestToRetry is not null)
{
decision.RequestToRetry.Properties[HttpRequestMessagePropertyKeys.ErrorHandler] = errorHandler;
diff --git a/src/Kampute.HttpClient/HttpRestClientExtensions.cs b/src/Kampute.HttpClient/HttpRestClientExtensions.cs
index 8af375d..967f475 100644
--- a/src/Kampute.HttpClient/HttpRestClientExtensions.cs
+++ b/src/Kampute.HttpClient/HttpRestClientExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpRestClientFormExtensions.cs b/src/Kampute.HttpClient/HttpRestClientFormExtensions.cs
index 1c65218..358626f 100644
--- a/src/Kampute.HttpClient/HttpRestClientFormExtensions.cs
+++ b/src/Kampute.HttpClient/HttpRestClientFormExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/HttpVerb.cs b/src/Kampute.HttpClient/HttpVerb.cs
index 6a6870b..31d0db0 100644
--- a/src/Kampute.HttpClient/HttpVerb.cs
+++ b/src/Kampute.HttpClient/HttpVerb.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/IHttpBackoffProvider.cs b/src/Kampute.HttpClient/Interfaces/IHttpBackoffProvider.cs
index f438ce7..405e2f1 100644
--- a/src/Kampute.HttpClient/Interfaces/IHttpBackoffProvider.cs
+++ b/src/Kampute.HttpClient/Interfaces/IHttpBackoffProvider.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/IHttpContentDeserializer.cs b/src/Kampute.HttpClient/Interfaces/IHttpContentDeserializer.cs
index fb4447f..ebff42b 100644
--- a/src/Kampute.HttpClient/Interfaces/IHttpContentDeserializer.cs
+++ b/src/Kampute.HttpClient/Interfaces/IHttpContentDeserializer.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/IHttpErrorHandler.cs b/src/Kampute.HttpClient/Interfaces/IHttpErrorHandler.cs
index c6532bc..c392a8b 100644
--- a/src/Kampute.HttpClient/Interfaces/IHttpErrorHandler.cs
+++ b/src/Kampute.HttpClient/Interfaces/IHttpErrorHandler.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/IHttpErrorResponse.cs b/src/Kampute.HttpClient/Interfaces/IHttpErrorResponse.cs
index 547442e..5e0f7f7 100644
--- a/src/Kampute.HttpClient/Interfaces/IHttpErrorResponse.cs
+++ b/src/Kampute.HttpClient/Interfaces/IHttpErrorResponse.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/IRetryScheduler.cs b/src/Kampute.HttpClient/Interfaces/IRetryScheduler.cs
index cda146d..26e8101 100644
--- a/src/Kampute.HttpClient/Interfaces/IRetryScheduler.cs
+++ b/src/Kampute.HttpClient/Interfaces/IRetryScheduler.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Interfaces/NamespaceDoc.cs b/src/Kampute.HttpClient/Interfaces/NamespaceDoc.cs
index eb400b5..97258b9 100644
--- a/src/Kampute.HttpClient/Interfaces/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Interfaces/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Kampute.HttpClient.csproj b/src/Kampute.HttpClient/Kampute.HttpClient.csproj
index 6f0139b..38fb281 100644
--- a/src/Kampute.HttpClient/Kampute.HttpClient.csproj
+++ b/src/Kampute.HttpClient/Kampute.HttpClient.csproj
@@ -5,9 +5,9 @@
Kampute.HttpClient
Kampute.HttpClient is a versatile and lightweight .NET library that simplifies RESTful API communication. Its core HttpRestClient class provides a streamlined approach to HTTP interactions, offering advanced features such as flexible serialization/deserialization, robust error handling, configurable backoff strategies, and detailed request-response processing. Striking a balance between simplicity and extensibility, Kampute.HttpClient empowers developers with a powerful yet easy-to-use client for seamless API integration across a wide range of .NET applications.
Kambiz Khojasteh
- 2.4.0
+ 2.5.0
Kampute
- Copyright (c) 2024 Kampute
+ Copyright (c) 2025 Kampute
latest
enable
true
diff --git a/src/Kampute.HttpClient/MediaTypeNames.cs b/src/Kampute.HttpClient/MediaTypeNames.cs
index b915705..333eb60 100644
--- a/src/Kampute.HttpClient/MediaTypeNames.cs
+++ b/src/Kampute.HttpClient/MediaTypeNames.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/NamespaceDoc.cs b/src/Kampute.HttpClient/NamespaceDoc.cs
index 94d260d..7e3b069 100644
--- a/src/Kampute.HttpClient/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/BackoffStrategy.cs b/src/Kampute.HttpClient/RetryManagement/BackoffStrategy.cs
index 864d0be..1ee5322 100644
--- a/src/Kampute.HttpClient/RetryManagement/BackoffStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/BackoffStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/DynamicBackoffStrategy.cs b/src/Kampute.HttpClient/RetryManagement/DynamicBackoffStrategy.cs
index c54806e..ae5bc2a 100644
--- a/src/Kampute.HttpClient/RetryManagement/DynamicBackoffStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/DynamicBackoffStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/NamespaceDoc.cs b/src/Kampute.HttpClient/RetryManagement/NamespaceDoc.cs
index 00a547d..358ce62 100644
--- a/src/Kampute.HttpClient/RetryManagement/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/RetryManagement/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/RetryScheduler.cs b/src/Kampute.HttpClient/RetryManagement/RetryScheduler.cs
index b0a4ae8..907840b 100644
--- a/src/Kampute.HttpClient/RetryManagement/RetryScheduler.cs
+++ b/src/Kampute.HttpClient/RetryManagement/RetryScheduler.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/ExponentialStrategy.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/ExponentialStrategy.cs
index c583730..277a28b 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/ExponentialStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/ExponentialStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/FibonacciStrategy.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/FibonacciStrategy.cs
index 0e4f322..8400c7e 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/FibonacciStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/FibonacciStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/LinearStrategy.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/LinearStrategy.cs
index bdb80cd..bafaa5f 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/LinearStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/LinearStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/Modifiers/NamespaceDoc.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/Modifiers/NamespaceDoc.cs
index fa8289b..c781982 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/Modifiers/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/Modifiers/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/NamespaceDoc.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/NamespaceDoc.cs
index afee57f..d6af15f 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/NoneStrategy.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/NoneStrategy.cs
index b810fbc..ca40f65 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/NoneStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/NoneStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/RetryManagement/Strategies/UniformStrategy.cs b/src/Kampute.HttpClient/RetryManagement/Strategies/UniformStrategy.cs
index 7f4aafc..068a21e 100644
--- a/src/Kampute.HttpClient/RetryManagement/Strategies/UniformStrategy.cs
+++ b/src/Kampute.HttpClient/RetryManagement/Strategies/UniformStrategy.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Utilities/ExceptionExtensions.cs b/src/Kampute.HttpClient/Utilities/ExceptionExtensions.cs
index 0433df2..5bed7fb 100644
--- a/src/Kampute.HttpClient/Utilities/ExceptionExtensions.cs
+++ b/src/Kampute.HttpClient/Utilities/ExceptionExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Utilities/NamespaceDoc.cs b/src/Kampute.HttpClient/Utilities/NamespaceDoc.cs
index bd7de8f..3309cd9 100644
--- a/src/Kampute.HttpClient/Utilities/NamespaceDoc.cs
+++ b/src/Kampute.HttpClient/Utilities/NamespaceDoc.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/src/Kampute.HttpClient/Utilities/SharedDisposable.cs b/src/Kampute.HttpClient/Utilities/SharedDisposable.cs
index 012db17..225e25c 100644
--- a/src/Kampute.HttpClient/Utilities/SharedDisposable.cs
+++ b/src/Kampute.HttpClient/Utilities/SharedDisposable.cs
@@ -1,4 +1,4 @@
-// Copyright (C) 2024 Kampute
+// Copyright (C) 2025 Kampute
//
// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
// See the LICENSE file in the project root for the full license text.
diff --git a/tests/Kampute.HttpClient.DataContract.Test/HttpRestClientXmlExtensionsTests.cs b/tests/Kampute.HttpClient.DataContract.Test/HttpRestClientXmlExtensionsTests.cs
index 4ac158f..e99faae 100644
--- a/tests/Kampute.HttpClient.DataContract.Test/HttpRestClientXmlExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.DataContract.Test/HttpRestClientXmlExtensionsTests.cs
@@ -62,12 +62,12 @@ public async Task PostAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
@@ -89,12 +89,12 @@ public async Task PutAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
@@ -116,12 +116,12 @@ public async Task PatchAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
diff --git a/tests/Kampute.HttpClient.DataContract.Test/Kampute.HttpClient.DataContract.Test.csproj b/tests/Kampute.HttpClient.DataContract.Test/Kampute.HttpClient.DataContract.Test.csproj
index e6eaa33..a686f69 100644
--- a/tests/Kampute.HttpClient.DataContract.Test/Kampute.HttpClient.DataContract.Test.csproj
+++ b/tests/Kampute.HttpClient.DataContract.Test/Kampute.HttpClient.DataContract.Test.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
latest
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Kampute.HttpClient.DataContract.Test/XmlContentTests.cs b/tests/Kampute.HttpClient.DataContract.Test/XmlContentTests.cs
index bfa06e7..5cf4bb6 100644
--- a/tests/Kampute.HttpClient.DataContract.Test/XmlContentTests.cs
+++ b/tests/Kampute.HttpClient.DataContract.Test/XmlContentTests.cs
@@ -17,12 +17,12 @@ public async Task WithDefaultEncoding_SetsContentCorrectly()
var xmlString = await xmlContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(xmlString, Is.EqualTo(expectedString));
Assert.That(xmlContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
Assert.That(xmlContent.Headers.ContentType?.CharSet, Is.EqualTo(Encoding.UTF8.WebName));
- });
+ }
}
[Test]
@@ -36,12 +36,12 @@ public async Task WithCustomEncoding_SetsContentCorrectly()
var xmlString = await xmlContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(xmlString, Is.EqualTo(expectedString));
Assert.That(xmlContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
Assert.That(xmlContent.Headers.ContentType?.CharSet, Is.EqualTo(encoding.WebName));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Json.Test/HttpRestClientJsonExtensionsTests.cs b/tests/Kampute.HttpClient.Json.Test/HttpRestClientJsonExtensionsTests.cs
index 7256a06..71fa5e6 100644
--- a/tests/Kampute.HttpClient.Json.Test/HttpRestClientJsonExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Json.Test/HttpRestClientJsonExtensionsTests.cs
@@ -63,12 +63,12 @@ public async Task PostAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
@@ -90,12 +90,12 @@ public async Task PutAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
@@ -117,12 +117,12 @@ public async Task PatchAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
diff --git a/tests/Kampute.HttpClient.Json.Test/JsonContentTests.cs b/tests/Kampute.HttpClient.Json.Test/JsonContentTests.cs
index 58b10aa..6c508d0 100644
--- a/tests/Kampute.HttpClient.Json.Test/JsonContentTests.cs
+++ b/tests/Kampute.HttpClient.Json.Test/JsonContentTests.cs
@@ -17,12 +17,12 @@ public async Task SetsContentCorrectly()
var jsonString = await jsonContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(jsonString, Is.EqualTo(expectedString));
Assert.That(jsonContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
Assert.That(jsonContent.Headers.ContentType?.CharSet, Is.EqualTo(Encoding.UTF8.WebName));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Json.Test/Kampute.HttpClient.Json.Test.csproj b/tests/Kampute.HttpClient.Json.Test/Kampute.HttpClient.Json.Test.csproj
index eea2a71..f223b5b 100644
--- a/tests/Kampute.HttpClient.Json.Test/Kampute.HttpClient.Json.Test.csproj
+++ b/tests/Kampute.HttpClient.Json.Test/Kampute.HttpClient.Json.Test.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
latest
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Kampute.HttpClient.NewtonsoftJson.Test/HttpRestClientJsonExtensionsTests.cs b/tests/Kampute.HttpClient.NewtonsoftJson.Test/HttpRestClientJsonExtensionsTests.cs
index d364649..2ca5cce 100644
--- a/tests/Kampute.HttpClient.NewtonsoftJson.Test/HttpRestClientJsonExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.NewtonsoftJson.Test/HttpRestClientJsonExtensionsTests.cs
@@ -63,12 +63,12 @@ public async Task PostAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
@@ -90,12 +90,12 @@ public async Task PutAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
@@ -117,12 +117,12 @@ public async Task PatchAsJsonAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
- });
+ }
return new HttpResponseMessage
{
diff --git a/tests/Kampute.HttpClient.NewtonsoftJson.Test/JsonContentTests.cs b/tests/Kampute.HttpClient.NewtonsoftJson.Test/JsonContentTests.cs
index 4b6f95f..ecbdccb 100644
--- a/tests/Kampute.HttpClient.NewtonsoftJson.Test/JsonContentTests.cs
+++ b/tests/Kampute.HttpClient.NewtonsoftJson.Test/JsonContentTests.cs
@@ -17,12 +17,12 @@ public async Task SetsContentCorrectly()
var jsonString = await jsonContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(jsonString, Is.EqualTo(expectedString));
Assert.That(jsonContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Json));
Assert.That(jsonContent.Headers.ContentType?.CharSet, Is.EqualTo(Encoding.UTF8.WebName));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.NewtonsoftJson.Test/Kampute.HttpClient.NewtonsoftJson.Test.csproj b/tests/Kampute.HttpClient.NewtonsoftJson.Test/Kampute.HttpClient.NewtonsoftJson.Test.csproj
index 97932ca..de8de47 100644
--- a/tests/Kampute.HttpClient.NewtonsoftJson.Test/Kampute.HttpClient.NewtonsoftJson.Test.csproj
+++ b/tests/Kampute.HttpClient.NewtonsoftJson.Test/Kampute.HttpClient.NewtonsoftJson.Test.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
latest
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Kampute.HttpClient.Test/Content/Compression/DeflateCompressedContentTests.cs b/tests/Kampute.HttpClient.Test/Content/Compression/DeflateCompressedContentTests.cs
index dd6547e..fb61952 100644
--- a/tests/Kampute.HttpClient.Test/Content/Compression/DeflateCompressedContentTests.cs
+++ b/tests/Kampute.HttpClient.Test/Content/Compression/DeflateCompressedContentTests.cs
@@ -19,11 +19,11 @@ public async Task DeflateCompressedContent_CompressesDataCorrectly()
using var originalContent = new StringContent(text, Encoding.UTF32, MediaTypeNames.Text.Plain);
using var compressedContent = new DeflateCompressedContent(originalContent, CompressionLevel.Optimal);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(compressedContent.Headers.ContentType, Is.EqualTo(originalContent.Headers.ContentType));
Assert.That(compressedContent.Headers.ContentEncoding, Contains.Item("deflate"));
- });
+ }
var compressedStream = await compressedContent.ReadAsStreamAsync();
diff --git a/tests/Kampute.HttpClient.Test/Content/Compression/GzipCompressedContentTests.cs b/tests/Kampute.HttpClient.Test/Content/Compression/GzipCompressedContentTests.cs
index 2ee03a8..0e3a22f 100644
--- a/tests/Kampute.HttpClient.Test/Content/Compression/GzipCompressedContentTests.cs
+++ b/tests/Kampute.HttpClient.Test/Content/Compression/GzipCompressedContentTests.cs
@@ -19,11 +19,11 @@ public async Task GzipCompressedContent_CompressesDataCorrectly()
using var originalContent = new StringContent(text, Encoding.UTF32, MediaTypeNames.Text.Plain);
using var compressedContent = new GzipCompressedContent(originalContent, CompressionLevel.Optimal);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(compressedContent.Headers.ContentType, Is.EqualTo(originalContent.Headers.ContentType));
Assert.That(compressedContent.Headers.ContentEncoding, Contains.Item("gzip"));
- });
+ }
var compressedStream = await compressedContent.ReadAsStreamAsync();
diff --git a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError401HandlerTests.cs b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError401HandlerTests.cs
index aca6ec7..4a05aca 100644
--- a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError401HandlerTests.cs
+++ b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError401HandlerTests.cs
@@ -67,17 +67,17 @@ public async Task On401Response_BySuccessfulAuthentication_AuthorizesRequests()
var tasks = Enumerable.Range(1, numberOfRequests).Select(i => _client.SendAsync(HttpMethod.Get, $"/protected/resource{i}"));
await Task.WhenAll(tasks);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(numberOfInvokes, Is.EqualTo(1));
Assert.That(numberOfResponses, Is.EqualTo(numberOfRequests + 1));
Assert.That(_client.DefaultRequestHeaders.Authorization, Is.Not.Null);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(_client.DefaultRequestHeaders.Authorization?.Scheme, Is.EqualTo(scheme));
Assert.That(_client.DefaultRequestHeaders.Authorization?.Parameter, Is.EqualTo(apiKey));
- });
- });
+ }
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError429HandlerTests.cs b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError429HandlerTests.cs
index 6ea1e89..abbe3b0 100644
--- a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError429HandlerTests.cs
+++ b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError429HandlerTests.cs
@@ -54,11 +54,11 @@ public async Task On429Response_WithRateLimitResetHeader_RetriesRequestAfterSpec
await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/rate-limited/resource"), Throws.TypeOf());
timer.Stop();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(attempts, Is.EqualTo(2));
Assert.That(timer.Elapsed, Is.EqualTo(resetDelay).Within(TimeSpan.FromSeconds(1.0)));
- });
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError503HandlerTests.cs b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError503HandlerTests.cs
index a9dd9fb..7878c30 100644
--- a/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError503HandlerTests.cs
+++ b/tests/Kampute.HttpClient.Test/ErrorHandlers/HttpError503HandlerTests.cs
@@ -55,11 +55,11 @@ public async Task On503Response_WithRetryAfterHeader_AsDate_RetriesRequestAfterS
await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
timer.Stop();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(attempts, Is.EqualTo(2));
Assert.That(timer.Elapsed, Is.EqualTo(retryDelay).Within(0.1 * retryDelay));
- });
+ }
}
[Test]
@@ -84,11 +84,11 @@ public async Task On503Response_WithRetryAfterHeader_AsDelta_RetriesRequestAfter
await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
timer.Stop();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(attempts, Is.EqualTo(2));
Assert.That(timer.Elapsed, Is.EqualTo(retryDelay).Within(0.1 * retryDelay));
- });
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/ErrorHandlers/TransientHttpErrorHandlerTests.cs b/tests/Kampute.HttpClient.Test/ErrorHandlers/TransientHttpErrorHandlerTests.cs
new file mode 100644
index 0000000..276ffdf
--- /dev/null
+++ b/tests/Kampute.HttpClient.Test/ErrorHandlers/TransientHttpErrorHandlerTests.cs
@@ -0,0 +1,187 @@
+// Copyright (C) 2025 Kampute
+//
+// This file is part of the Kampute.HttpClient package and is released under the terms of the MIT license.
+// See the LICENSE file in the project root for the full license text.
+
+namespace Kampute.HttpClient.Test.ErrorHandlers
+{
+ using Kampute.HttpClient.ErrorHandlers;
+ using Kampute.HttpClient.Test.TestHelpers;
+ using Moq;
+ using NUnit.Framework;
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Net;
+ using System.Net.Http;
+ using System.Net.Http.Headers;
+ using System.Threading.Tasks;
+
+ [TestFixture]
+ public class TransientHttpErrorHandlerTests
+ {
+ private readonly Mock _mockMessageHandler = new();
+ private HttpRestClient _client;
+
+ [SetUp]
+ public void Setup()
+ {
+ var httpClient = new HttpClient(_mockMessageHandler.Object, disposeHandler: false);
+ _client = new HttpRestClient(httpClient)
+ {
+ BaseAddress = new Uri("http://api.test.com"),
+ };
+ }
+
+ [TearDown]
+ public void Cleanup()
+ {
+ _client.Dispose();
+ }
+
+ [Test]
+ public void Constructor_WithNullStatusCodes_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new TransientHttpErrorHandler((IEnumerable)null!));
+ }
+
+ [Test]
+ public void Constructor_WithEmptyStatusCodes_ThrowsArgumentException()
+ {
+ Assert.Throws(() => new TransientHttpErrorHandler([]));
+ }
+
+ [Test]
+ public void Constructor_WithCustomStatusCodes_SetsHandledStatusCodes()
+ {
+ var customCodes = new[]
+ {
+ HttpStatusCode.InternalServerError,
+ HttpStatusCode.BadRequest
+ };
+ var handler = new TransientHttpErrorHandler(customCodes);
+
+ Assert.That(handler.HandledStatusCodes, Is.EquivalentTo(customCodes));
+ }
+
+ [Test]
+ [TestCase(HttpStatusCode.RequestTimeout, ExpectedResult = true)]
+ [TestCase(HttpStatusCode.BadGateway, ExpectedResult = true)]
+ [TestCase(HttpStatusCode.ServiceUnavailable, ExpectedResult = true)]
+ [TestCase(HttpStatusCode.GatewayTimeout, ExpectedResult = true)]
+ [TestCase(507 /* Insufficient Storage */, ExpectedResult = true)]
+ [TestCase(509 /* Bandwidth Limit Exceeded */, ExpectedResult = true)]
+ [TestCase(HttpStatusCode.OK, ExpectedResult = false)]
+ [TestCase(HttpStatusCode.NotFound, ExpectedResult = false)]
+ [TestCase(HttpStatusCode.InternalServerError, ExpectedResult = false)]
+ [TestCase(HttpStatusCode.Unauthorized, ExpectedResult = false)]
+ [TestCase(HttpStatusCode.Forbidden, ExpectedResult = false)]
+ [TestCase(HttpStatusCode.BadRequest, ExpectedResult = false)]
+ public bool CanHandle_ForDefaultConfiguration_ReturnsExpectedResults(HttpStatusCode statusCode)
+ {
+ var handler = new TransientHttpErrorHandler();
+
+ return handler.CanHandle(statusCode);
+ }
+
+ [Test]
+ public async Task OnTransientHttpError_WithRetryAfterHeader_AsDate_RetriesRequestAfterSpecifiedTime()
+ {
+ var transientHandler = new TransientHttpErrorHandler();
+ _client.ErrorHandlers.Add(transientHandler);
+
+ var retryDelay = TimeSpan.FromMilliseconds(1000);
+
+ var attempts = 0;
+ _mockMessageHandler.MockHttpResponse(request =>
+ {
+ attempts++;
+
+ var response = new HttpResponseMessage(HttpStatusCode.RequestTimeout);
+ response.Headers.RetryAfter = new RetryConditionHeaderValue(DateTimeOffset.UtcNow.Add(retryDelay));
+ return response;
+ });
+
+ var timer = Stopwatch.StartNew();
+ await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
+ timer.Stop();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(attempts, Is.EqualTo(2));
+ Assert.That(timer.Elapsed, Is.EqualTo(retryDelay).Within(0.1 * retryDelay));
+ }
+ }
+
+ [Test]
+ public async Task OnTransientHttpError_WithRetryAfterHeader_AsDelta_RetriesRequestAfterSpecifiedDelay()
+ {
+ var transientHandler = new TransientHttpErrorHandler();
+ _client.ErrorHandlers.Add(transientHandler);
+
+ var retryDelay = TimeSpan.FromMilliseconds(1000);
+
+ var attempts = 0;
+ _mockMessageHandler.MockHttpResponse(request =>
+ {
+ attempts++;
+
+ var response = new HttpResponseMessage(HttpStatusCode.RequestTimeout);
+ response.Headers.RetryAfter = new RetryConditionHeaderValue(retryDelay);
+ return response;
+ });
+
+ var timer = Stopwatch.StartNew();
+ await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
+ timer.Stop();
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(attempts, Is.EqualTo(2));
+ Assert.That(timer.Elapsed, Is.EqualTo(retryDelay).Within(0.1 * retryDelay));
+ }
+ }
+
+ [Test]
+ public async Task OnTransientHttpError_WithoutRetryAfterHeader_RetriesAccordingToDefaultStrategy()
+ {
+ var transientHandler = new TransientHttpErrorHandler();
+ _client.ErrorHandlers.Add(transientHandler);
+ _client.BackoffStrategy = BackoffStrategies.Uniform(2, TimeSpan.Zero);
+
+ var attempts = 0;
+ _mockMessageHandler.MockHttpResponse(request =>
+ {
+ attempts++;
+
+ return new HttpResponseMessage(HttpStatusCode.RequestTimeout);
+ });
+
+ await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
+
+ Assert.That(attempts, Is.EqualTo(3));
+ }
+
+ [Test]
+ public async Task OnTransientHttpError_WithCustomBackoffStrategy_RetriesAccordingToCustomStrategy()
+ {
+ var transientHandler = new TransientHttpErrorHandler
+ {
+ OnBackoffStrategy = (ctx, retryAfter) => BackoffStrategies.Uniform(2, TimeSpan.Zero)
+ };
+ _client.ErrorHandlers.Add(transientHandler);
+
+ var attempts = 0;
+ _mockMessageHandler.MockHttpResponse(request =>
+ {
+ attempts++;
+
+ return new HttpResponseMessage(HttpStatusCode.RequestTimeout);
+ });
+
+ await Assert.ThatAsync(() => _client.SendAsync(HttpMethod.Get, "/unavailable/resource"), Throws.TypeOf());
+
+ Assert.That(attempts, Is.EqualTo(3));
+ }
+ }
+}
diff --git a/tests/Kampute.HttpClient.Test/HttpContentDeserializerCollectionTests.cs b/tests/Kampute.HttpClient.Test/HttpContentDeserializerCollectionTests.cs
index c2e98cd..b6fcad9 100644
--- a/tests/Kampute.HttpClient.Test/HttpContentDeserializerCollectionTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpContentDeserializerCollectionTests.cs
@@ -99,11 +99,11 @@ public void Remove_WithExistingDeserializer_RemovesDeserializerAndReturnsTrue()
var result = collection.Remove(deserializer);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(collection, Is.Empty);
- });
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/HttpContentExtensionsTests.cs b/tests/Kampute.HttpClient.Test/HttpContentExtensionsTests.cs
index 9966ff4..e4dea46 100644
--- a/tests/Kampute.HttpClient.Test/HttpContentExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpContentExtensionsTests.cs
@@ -1,9 +1,9 @@
namespace Kampute.HttpClient.Test
{
using Kampute.HttpClient.Content.Compression;
+ using Kampute.HttpClient.Test.TestHelpers;
using NUnit.Framework;
using System;
- using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
@@ -11,12 +11,6 @@
[TestFixture]
public class HttpContentExtensionsTests
{
- private class NonSeekableMemoryStream : MemoryStream
- {
- public override bool CanSeek => false;
- public override long Seek(long offset, SeekOrigin loc) => throw new NotSupportedException();
- }
-
[Test]
public void FindCharacterEncoding_WithCharSet_ReturnsEncoding()
{
@@ -49,7 +43,7 @@ public void FindCharacterEncoding_WithUnsupportedCharSet_ThrowsArgumentException
[Test]
public void IsReusable_WhenContentIsReusable_ReturnsTrue()
{
- using var content = new StreamContent(new MemoryStream());
+ using var content = new StreamContent(new TestStream(seekable: true));
var result = content.IsReusable();
@@ -59,7 +53,29 @@ public void IsReusable_WhenContentIsReusable_ReturnsTrue()
[Test]
public void IsReusable_WhenContentIsNotReusable_ReturnsFalse()
{
- using var content = new StreamContent(new NonSeekableMemoryStream());
+ using var content = new StreamContent(new TestStream(seekable: false));
+
+ var result = content.IsReusable();
+
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void IsReusable_WhenCompressedContentIsReusable_ReturnsTrue()
+ {
+ var originalContent = new StreamContent(new TestStream(seekable: true));
+ using var content = new DeflateCompressedContent(originalContent);
+
+ var result = content.IsReusable();
+
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsReusable_WhenCompressedContentIsNotReusable_ReturnsFalse()
+ {
+ var originalContent = new StreamContent(new TestStream(seekable: false));
+ using var content = new DeflateCompressedContent(originalContent);
var result = content.IsReusable();
diff --git a/tests/Kampute.HttpClient.Test/HttpErrorHandlerCollectionTests.cs b/tests/Kampute.HttpClient.Test/HttpErrorHandlerCollectionTests.cs
index ee71137..e2997f9 100644
--- a/tests/Kampute.HttpClient.Test/HttpErrorHandlerCollectionTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpErrorHandlerCollectionTests.cs
@@ -53,11 +53,11 @@ public void Remove_WithExistingErrorHandler_RemovesErrorHandlerAndReturnsTrue()
var result = collection.Remove(errorHandler);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(collection, Is.Empty);
- });
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/HttpRequestMessageExtensionsTests.cs b/tests/Kampute.HttpClient.Test/HttpRequestMessageExtensionsTests.cs
index 64dbc8d..b5121dc 100644
--- a/tests/Kampute.HttpClient.Test/HttpRequestMessageExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpRequestMessageExtensionsTests.cs
@@ -1,19 +1,13 @@
namespace Kampute.HttpClient.Test
{
+ using Kampute.HttpClient.Test.TestHelpers;
using NUnit.Framework;
using System;
- using System.IO;
using System.Net.Http;
[TestFixture]
public class HttpRequestMessageExtensionsTests
{
- private class NonSeekableMemoryStream : MemoryStream
- {
- public override bool CanSeek => false;
- public override long Seek(long offset, SeekOrigin loc) => throw new NotSupportedException();
- }
-
[Test]
public void Clone_WithReusableContent_CreatesCopy()
{
@@ -28,14 +22,14 @@ public void Clone_WithReusableContent_CreatesCopy()
Assert.That(clonedRequest, Is.Not.Null);
Assert.That(clonedRequest, Is.Not.SameAs(originalRequest));
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(clonedRequest.RequestUri, Is.EqualTo(originalRequest.RequestUri));
Assert.That(clonedRequest.Version, Is.EqualTo(originalRequest.Version));
Assert.That(clonedRequest.Headers.Contains("Test-Header"), Is.True);
Assert.That(clonedRequest.Content, Is.SameAs(originalRequest.Content));
Assert.That(clonedRequest.GetCloneGeneration(), Is.EqualTo(1));
- });
+ }
}
[Test]
@@ -43,7 +37,7 @@ public void Clone_WithNonReusableContent_ThrowsInvalidOperationException()
{
using var request = new HttpRequestMessage
{
- Content = new StreamContent(new NonSeekableMemoryStream())
+ Content = new StreamContent(new TestStream(seekable: false))
};
Assert.That(request.Clone, Throws.InstanceOf());
@@ -62,7 +56,7 @@ public void IsClonable_WithReusableContent_ReturnsTrue()
{
using var request = new HttpRequestMessage
{
- Content = new StreamContent(new MemoryStream())
+ Content = new StreamContent(new TestStream(seekable: true))
};
Assert.That(request.CanClone(), Is.True);
@@ -73,7 +67,7 @@ public void IsClonable_WithNonReusableContent_ReturnsFalse()
{
using var request = new HttpRequestMessage
{
- Content = new StreamContent(new NonSeekableMemoryStream())
+ Content = new StreamContent(new TestStream(seekable: false))
};
Assert.That(request.CanClone(), Is.False);
@@ -103,12 +97,12 @@ public void GetCloneGeneration_WhenClonedMultipleTimes_ReturnsCorrectCount()
using var firstGenerationClone = originalRequest.Clone();
using var secondGenerationClone = firstGenerationClone.Clone();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
- Assert.That(originalRequest.GetCloneGeneration(), Is.EqualTo(0));
+ Assert.That(originalRequest.GetCloneGeneration(), Is.Zero);
Assert.That(firstGenerationClone.GetCloneGeneration(), Is.EqualTo(1));
Assert.That(secondGenerationClone.GetCloneGeneration(), Is.EqualTo(2));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/HttpRequestScopeTests.cs b/tests/Kampute.HttpClient.Test/HttpRequestScopeTests.cs
index e4bc189..5ad43a8 100644
--- a/tests/Kampute.HttpClient.Test/HttpRequestScopeTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpRequestScopeTests.cs
@@ -91,12 +91,12 @@ public async Task PerformAsync_AppliesScopedHeadersAndProperties()
_mockMessageHandler.MockHttpResponse(request =>
{
var propExists = request.Options.TryGetValue(new HttpRequestOptionsKey(propertyName), out var propValue);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(propExists, Is.True);
Assert.That(propValue, Is.EqualTo(propertyValue));
- Assert.That(request.Headers.GetValues(headerName), Is.EqualTo(new[] { headerValue }));
- });
+ Assert.That(request.Headers.GetValues(headerName), Is.EqualTo([headerValue]));
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
diff --git a/tests/Kampute.HttpClient.Test/HttpResponseHeadersExtensionsTests.cs b/tests/Kampute.HttpClient.Test/HttpResponseHeadersExtensionsTests.cs
index 805a166..8b2e5ab 100644
--- a/tests/Kampute.HttpClient.Test/HttpResponseHeadersExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpResponseHeadersExtensionsTests.cs
@@ -25,11 +25,11 @@ public void TryExtractRetryAfterTime_WithValidDate_ReturnsTrueAndTime()
var result = _headers.TryExtractRetryAfterTime(out var retryAfterTime);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(retryAfterTime, Is.EqualTo(expectedDate).Within(TimeSpan.FromSeconds(1)));
- });
+ }
}
[Test]
@@ -40,11 +40,11 @@ public void TryExtractRetryAfterTime_WithValidDelta_ReturnsTrueAndTime()
var result = _headers.TryExtractRetryAfterTime(out var retryAfterTime);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(retryAfterTime, Is.EqualTo(DateTimeOffset.UtcNow.Add(delta)).Within(TimeSpan.FromSeconds(1)));
- });
+ }
}
[Test]
@@ -52,11 +52,11 @@ public void TryExtractRetryAfterTime_WithoutRetryAfterHeader_ReturnsFalse()
{
var result = _headers.TryExtractRetryAfterTime(out var retryAfterTime);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(retryAfterTime, Is.Null);
- });
+ }
}
[Test]
@@ -67,11 +67,11 @@ public void TryExtractRateLimitResetTime_WithValidUnixTimestampHeader_ReturnsTru
var result = _headers.TryExtractRateLimitResetTime(out var resetTime);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(resetTime, Is.EqualTo(expectedTime).Within(TimeSpan.FromSeconds(1)));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/HttpRestClientExtensionsTests.cs b/tests/Kampute.HttpClient.Test/HttpRestClientExtensionsTests.cs
index 6407320..41fbf52 100644
--- a/tests/Kampute.HttpClient.Test/HttpRestClientExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpRestClientExtensionsTests.cs
@@ -47,11 +47,11 @@ public async Task HeadAsync_InvokesHttpClientCorrectly()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Head));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
@@ -66,11 +66,11 @@ public async Task OptionsAsync_InvokesHttpClientCorrectly()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Options));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
@@ -87,11 +87,11 @@ public async Task GetAsync_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Get));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -112,11 +112,11 @@ public async Task GetAsByteArrayAsync_ReturnsResponseAsBytes()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Get));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -137,11 +137,11 @@ public async Task GetAsStringAsync_ReturnsResponseAsString()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Get));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -162,11 +162,11 @@ public async Task GetAsStreamAsync_ReturnsResponseAsStream()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Get));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -187,11 +187,11 @@ public async Task GetToStreamAsync_WritesResponseIntoStream()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Get));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -214,12 +214,12 @@ public async Task PostAsync_InvokesHttpClientCorrectly()
var sent = false;
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
sent = true;
return new HttpResponseMessage(HttpStatusCode.OK);
@@ -238,12 +238,12 @@ public async Task PostAsync_Generic_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
return new HttpResponseMessage
{
@@ -265,12 +265,12 @@ public async Task PutAsync_InvokesHttpClientCorrectly()
var sent = false;
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
sent = true;
return new HttpResponseMessage(HttpStatusCode.OK);
@@ -278,7 +278,7 @@ public async Task PutAsync_InvokesHttpClientCorrectly()
await _client.PutAsync("/resource", new TestContent(payload));
- Assert.That(sent, Is.EqualTo(true));
+ Assert.That(sent, Is.True);
}
[Test]
@@ -289,12 +289,12 @@ public async Task PutAsync_Generic_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
return new HttpResponseMessage
{
@@ -316,12 +316,12 @@ public async Task PatchAsync_InvokesHttpClientCorrectly()
var sent = false;
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
sent = true;
return new HttpResponseMessage(HttpStatusCode.OK);
@@ -329,7 +329,7 @@ public async Task PatchAsync_InvokesHttpClientCorrectly()
await _client.PatchAsync("/resource", new TestContent(payload));
- Assert.That(sent, Is.EqualTo(true));
+ Assert.That(sent, Is.True);
}
[Test]
@@ -340,12 +340,12 @@ public async Task PatchAsync_Generic_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo(payload));
- });
+ }
return new HttpResponseMessage
{
@@ -366,11 +366,11 @@ public async Task DeleteAsync_InvokesHttpClientCorrectly()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Delete));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -391,11 +391,11 @@ public async Task DeleteAsync_Generic_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Delete));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -418,11 +418,11 @@ public async Task DownloadAsync_WritesResponseIntoStream()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(method));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
var content = new ByteArrayContent(expectedStream.ToArray());
content.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet);
@@ -437,21 +437,21 @@ public async Task DownloadAsync_WritesResponseIntoStream()
using var resultStream = await _client.DownloadAsync(method, "/resource", new TestContent(payload), contentHeaders =>
{
Assert.That(contentHeaders, Is.Not.Null);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(contentHeaders.ContentType, Is.EqualTo(new MediaTypeHeaderValue(MediaTypeNames.Application.Octet)));
Assert.That(contentHeaders.ContentLength, Is.EqualTo(expectedStream.Length));
- });
+ }
return new MemoryStream((int)contentHeaders.ContentLength.GetValueOrDefault());
});
Assert.That(resultStream, Is.Not.Null);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
resultStream.Seek(0, SeekOrigin.Begin);
Assert.That(resultStream, Is.TypeOf());
Assert.That(resultStream, Is.EqualTo(expectedStream));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/HttpRestClientFormExtensionsTests.cs b/tests/Kampute.HttpClient.Test/HttpRestClientFormExtensionsTests.cs
index 7a5751d..54bccfd 100644
--- a/tests/Kampute.HttpClient.Test/HttpRestClientFormExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpRestClientFormExtensionsTests.cs
@@ -44,13 +44,13 @@ public async Task PostAsFormAsync_InvokesHttpClientCorrectly()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.FormUrlEncoded));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo("name=value"));
- });
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
@@ -63,13 +63,13 @@ public async Task PutAsFormAsync_InvokesHttpClientCorrectly()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.FormUrlEncoded));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo("name=value"));
- });
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
@@ -82,13 +82,13 @@ public async Task PatchAsFormAsync_InvokesHttpClientCorrectly()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.FormUrlEncoded));
Assert.That(request.Content?.ReadAsStringAsync().Result, Is.EqualTo("name=value"));
- });
+ }
return new HttpResponseMessage(HttpStatusCode.OK);
});
diff --git a/tests/Kampute.HttpClient.Test/HttpRestClientTests.cs b/tests/Kampute.HttpClient.Test/HttpRestClientTests.cs
index 3f10640..db763e2 100644
--- a/tests/Kampute.HttpClient.Test/HttpRestClientTests.cs
+++ b/tests/Kampute.HttpClient.Test/HttpRestClientTests.cs
@@ -8,6 +8,9 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
+ using System.IO;
+ using System.IO.Compression;
+ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -47,7 +50,7 @@ public void Cleanup()
}
[Test]
- public void DefaultConstractor_UsesSharedHttpClient()
+ public void DefaultConstructor_UsesSharedHttpClient()
{
var client = new HttpRestClient();
Assert.That(SharedHttpClient.ReferenceCount, Is.EqualTo(1));
@@ -81,11 +84,11 @@ public async Task SendAsync_NonGeneric_ReturnsResponse()
{
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(TestHttpMethod));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
var response = new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("X-Test", "Testing");
@@ -105,11 +108,11 @@ public async Task SendAsync_Generic_ReturnsResponseAsObject()
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(TestHttpMethod));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
- });
+ }
return new HttpResponseMessage
{
@@ -141,13 +144,13 @@ public void OnUnsuccessfulStatusCode_WithoutResponseErrorType_ThrowsStandardRest
var exception = Assert.ThrowsAsync(async () => await _client.SendAsync(TestHttpMethod, "/resource"));
Assert.That(exception, Is.Not.Null);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(exception.ResponseMessage?.RequestMessage?.Method, Is.EqualTo(TestHttpMethod));
Assert.That(exception.ResponseMessage?.RequestMessage?.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(exception.Message, Is.Not.EqualTo(errorDetails.Message));
- });
+ }
}
[Test]
@@ -160,14 +163,14 @@ public void OnUnsuccessfulStatusCode_WithResponseErrorType_ThrowsCustomizedRestE
var exception = Assert.ThrowsAsync(async () => await _client.SendAsync(TestHttpMethod, "/resource"));
Assert.That(exception, Is.Not.Null);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(exception.ResponseMessage?.RequestMessage?.Method, Is.EqualTo(TestHttpMethod));
Assert.That(exception.ResponseMessage?.RequestMessage?.RequestUri, Is.EqualTo(AbsoluteUrl("/resource")));
Assert.That(exception.ResponseObject, Is.EqualTo(errorDetails).UsingPropertiesComparer());
Assert.That(exception.Message, Is.EqualTo(errorDetails.Message));
- });
+ }
}
[Test]
@@ -211,11 +214,11 @@ public async Task OnUnsuccessfulStatusCode_WithErrorHandler_RetriesRequest()
var result = await _client.SendAsync(HttpMethod.Get, "/resource", new StringContent("test"));
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(attempts, Is.EqualTo(2));
Assert.That(result, Is.EqualTo(expectedResult));
- });
+ }
}
[Test]
@@ -252,6 +255,60 @@ public async Task OnConnectionFailure_UsesBackoffStrategy()
Assert.That(attempts, Is.EqualTo(maxRetries + 1));
}
+ [TestCase("gzip")]
+ [TestCase("deflate")]
+ public async Task OnConnectionFailure_WithCompressedContent_UsesBackoffStrategy(string encoding)
+ {
+ var maxRetries = 2;
+
+ var mockBackoffStrategy = new Mock();
+ var mockRetryScheduler = new Mock();
+
+ var retries = 0;
+ mockRetryScheduler.Setup(scheduler => scheduler.WaitAsync(It.IsAny()))
+ .ReturnsAsync(() => retries < maxRetries).Callback(() => ++retries);
+ mockBackoffStrategy.Setup(strategy => strategy.CreateScheduler(It.IsAny()))
+ .Returns(mockRetryScheduler.Object);
+
+ _client.BackoffStrategy = mockBackoffStrategy.Object;
+
+ var attempts = 0;
+ _mockMessageHandler.MockHttpResponse(request =>
+ {
+ if (++attempts <= maxRetries)
+ throw new HttpRequestException("Connection failure", new SocketException((int)SocketError.HostUnreachable));
+
+ Assert.That(request.Content, Is.Not.Null);
+
+ var compressedStream = request.Content.ReadAsStreamAsync().Result;
+ using Stream decompressedStream = request.Content.Headers.ContentEncoding.FirstOrDefault() switch
+ {
+ "gzip" => new GZipStream(compressedStream, CompressionMode.Decompress),
+ "deflate" => new DeflateStream(compressedStream, CompressionMode.Decompress),
+ _ => throw new InvalidOperationException("Unsupported encoding")
+ };
+ using var reader = new StreamReader(decompressedStream, Encoding.UTF8);
+ var actual = reader.ReadToEnd();
+
+ Assert.That(actual, Is.EqualTo("test"));
+
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ });
+
+ using var payload = new StringContent("test", Encoding.UTF8);
+ using HttpContent compressedPayload = encoding switch
+ {
+ "gzip" => payload.AsGzip(),
+ "deflate" => payload.AsDeflate(),
+ _ => throw new InvalidOperationException("Unsupported encoding")
+ };
+
+ await _client.SendAsync(TestHttpMethod, "/test", compressedPayload);
+
+ mockRetryScheduler.Verify(scheduler => scheduler.WaitAsync(It.IsAny()), Times.Exactly(maxRetries));
+ Assert.That(attempts, Is.EqualTo(maxRetries + 1));
+ }
+
[Test]
public async Task BeginPropertyScope_ModifiesRequestPropertiesCorrectly()
{
@@ -261,11 +318,11 @@ public async Task BeginPropertyScope_ModifiesRequestPropertiesCorrectly()
_mockMessageHandler.MockHttpResponse(request =>
{
var propExists = request.Options.TryGetValue(new HttpRequestOptionsKey(scopedProperty.Key), out var propValue);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(propExists, Is.True);
Assert.That(propValue, Is.EqualTo(scopedProperty.Value));
- });
+ }
sent = true;
return new HttpResponseMessage(HttpStatusCode.OK);
@@ -293,12 +350,12 @@ public async Task BeginHeaderScope_ModifiesRequestHeadersCorrectly()
var sent = false;
_mockMessageHandler.MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
- Assert.That(request.Headers.GetValues(scopedHeaderToAdd.Key), Is.EqualTo(new[] { scopedHeaderToAdd.Value }));
- Assert.That(request.Headers.GetValues(scopedHeaderToChange.Key), Is.EqualTo(new[] { scopedHeaderToChange.Value }));
+ Assert.That(request.Headers.GetValues(scopedHeaderToAdd.Key), Is.EqualTo([scopedHeaderToAdd.Value]));
+ Assert.That(request.Headers.GetValues(scopedHeaderToChange.Key), Is.EqualTo([scopedHeaderToChange.Value]));
Assert.That(request.Headers.Contains(scopedHeaderToDelete.Key), Is.False);
- });
+ }
sent = true;
return new HttpResponseMessage(HttpStatusCode.OK);
diff --git a/tests/Kampute.HttpClient.Test/Kampute.HttpClient.Test.csproj b/tests/Kampute.HttpClient.Test/Kampute.HttpClient.Test.csproj
index f0f11e9..8877245 100644
--- a/tests/Kampute.HttpClient.Test/Kampute.HttpClient.Test.csproj
+++ b/tests/Kampute.HttpClient.Test/Kampute.HttpClient.Test.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
latest
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/RetrySchedulerTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/RetrySchedulerTests.cs
index 691208f..fe6ebd0 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/RetrySchedulerTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/RetrySchedulerTests.cs
@@ -56,11 +56,11 @@ public async Task WaitAsync_WhenStrategyIndicatesRetryIsAdvisable_ReturnsTrue()
var result = await scheduler.WaitAsync(CancellationToken.None);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(scheduler.Attempts, Is.EqualTo(1u));
- });
+ }
}
[Test]
@@ -72,11 +72,11 @@ public async Task WaitAsync_WhenStrategyIndicatesRetryIsNotAdvisable_ReturnsFals
var result = await scheduler.WaitAsync(CancellationToken.None);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(scheduler.Attempts, Is.Zero);
- });
+ }
}
[Test]
@@ -102,11 +102,11 @@ public async Task Reset_ResetsInternalState()
await scheduler.WaitAsync(CancellationToken.None);
scheduler.Reset();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(scheduler.Elapsed, Is.LessThanOrEqualTo(TimeSpan.FromMilliseconds(10)));
Assert.That(scheduler.Attempts, Is.Zero);
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/ExponentialStrategyTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/ExponentialStrategyTests.cs
index 16f2f43..f45f231 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/ExponentialStrategyTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/ExponentialStrategyTests.cs
@@ -34,11 +34,11 @@ public void TryGetRetryDelay_ReturnsExpectedDelay(uint attempts, int initialDela
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
[TestCase(0u, 0, 2.0, 0)]
@@ -57,11 +57,11 @@ public void TryGetRetryDelay_IgnoresElapsed(uint attempts, int initialDelayMs, d
var result = strategy.TryGetRetryDelay(TimeSpan.FromHours(1), attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/FibonacciStrategyTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/FibonacciStrategyTests.cs
index a1da2f0..4d98e2d 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/FibonacciStrategyTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/FibonacciStrategyTests.cs
@@ -17,11 +17,11 @@ public void Constructor_WithInitialDelayAndDelayStep_SetsPropertiesCorrectly(int
var strategy = new FibonacciStrategy(initialDelay, delayStep);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(strategy.InitialDelay, Is.EqualTo(initialDelay));
Assert.That(strategy.DelayStep, Is.EqualTo(delayStep));
- });
+ }
}
[TestCase(0)]
@@ -33,11 +33,11 @@ public void Constructor_WithInitialDelayOnly_SetsInitialDelayAndDelayStep(int in
var strategy = new FibonacciStrategy(initialDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(strategy.InitialDelay, Is.EqualTo(initialDelay));
Assert.That(strategy.DelayStep, Is.EqualTo(initialDelay));
- });
+ }
}
[TestCase(0u, 0, 0, 0)]
@@ -57,11 +57,11 @@ public void TryGetRetryDelay_ReturnsExpectedDelay(uint attempts, int initialDela
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
[TestCase(0u, 0, 0, 0)]
@@ -81,11 +81,11 @@ public void TryGetRetryDelay_IgnoresElapsed(uint attempts, int initialDelayMs, i
var result = strategy.TryGetRetryDelay(TimeSpan.FromHours(1), attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/LinearStrategyTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/LinearStrategyTests.cs
index 49c3e4b..ed4f8e7 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/LinearStrategyTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/LinearStrategyTests.cs
@@ -17,11 +17,11 @@ public void Constructor_WithInitialDelayAndDelayStep_SetsPropertiesCorrectly(int
var strategy = new LinearStrategy(initialDelay, delayStep);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(strategy.InitialDelay, Is.EqualTo(initialDelay));
Assert.That(strategy.DelayStep, Is.EqualTo(delayStep));
- });
+ }
}
[TestCase(0)]
@@ -33,11 +33,11 @@ public void Constructor_WithInitialDelayOnly_SetsInitialDelayAndDelayStep(int in
var strategy = new LinearStrategy(initialDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(strategy.InitialDelay, Is.EqualTo(initialDelay));
Assert.That(strategy.DelayStep, Is.EqualTo(initialDelay));
- });
+ }
}
[TestCase(0u, 0, 0, 0)]
@@ -57,11 +57,11 @@ public void TryGetRetryDelay_ReturnsExpectedDelay(uint attempts, int initialDela
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
[TestCase(0u, 0, 0, 0)]
@@ -81,11 +81,11 @@ public void TryGetRetryDelay_IgnoresElapsed(uint attempts, int initialDelayMs, i
var result = strategy.TryGetRetryDelay(TimeSpan.FromHours(1), attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/JitterStrategyModifierTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/JitterStrategyModifierTests.cs
index 5295bb8..381465d 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/JitterStrategyModifierTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/JitterStrategyModifierTests.cs
@@ -32,11 +32,11 @@ public void TryGetRetryDelay_WhenSourceReturnsTrue_ReturnsTrueWithJitteredDelay(
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, 0, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(baseDelay).Within(jitterFactor * baseDelay));
- });
+ }
}
[Test]
@@ -48,11 +48,11 @@ public void TryGetRetryDelay_WhenSourceReturnsFalse_ReturnsFalse()
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, 0, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(actualDelay, Is.Default);
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedAttemptsStrategyModifierTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedAttemptsStrategyModifierTests.cs
index ac2b8f2..10e90aa 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedAttemptsStrategyModifierTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedAttemptsStrategyModifierTests.cs
@@ -27,11 +27,11 @@ public void TryGetRetryDelay_WhenSourceReturnsTrue_ReturnsExpectedResult(uint ma
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.EqualTo(expectedResult));
Assert.That(actualDelay, expectedResult ? Is.Not.Default : Is.Default);
- });
+ }
}
[Test]
@@ -44,11 +44,11 @@ public void TryGetRetryDelay_WhenSourceReturnsFalse_ReturnsFalse()
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, 0, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(actualDelay, Is.Default);
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedDurationStrategyModifierTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedDurationStrategyModifierTests.cs
index 52ef683..47dba62 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedDurationStrategyModifierTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/Modifiers/LimitedDurationStrategyModifierTests.cs
@@ -32,11 +32,11 @@ public void TryGetRetryDelay_WhenSourceReturnsTrue_ReturnsExpectedResult(int tim
var result = strategy.TryGetRetryDelay(elapsed, 0, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.EqualTo(expectedResult));
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
[Test]
@@ -48,11 +48,11 @@ public void TryGetRetryDelay_WhenSourceReturnsFalse_ReturnsFalse()
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, 0, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(actualDelay, Is.Default);
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/NoneStrategyTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/NoneStrategyTests.cs
index 74b5dbc..37e48e8 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/NoneStrategyTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/NoneStrategyTests.cs
@@ -33,11 +33,11 @@ public void TryGetRetryDelay_ReturnsFalseAndDefaultDelay(uint attempts, int elap
var result = NoneStrategy.Instance.TryGetRetryDelay(elapsed, attempts, out var delay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.False);
Assert.That(delay, Is.Default);
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/UniformStrategyTests.cs b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/UniformStrategyTests.cs
index d0d0289..c1c8a84 100644
--- a/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/UniformStrategyTests.cs
+++ b/tests/Kampute.HttpClient.Test/RetryManagement/Strategies/UniformStrategyTests.cs
@@ -27,11 +27,11 @@ public void TryGetRetryDelay_ReturnsExpectedDelay(uint attempts, int delayMillis
var result = strategy.TryGetRetryDelay(TimeSpan.Zero, attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
[TestCase(0u)]
@@ -44,11 +44,11 @@ public void TryGetRetryDelay_IgnoresElapsedAndAttempts(uint attempts)
var result = strategy.TryGetRetryDelay(TimeSpan.FromHours(1), attempts, out var actualDelay);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(result, Is.True);
Assert.That(actualDelay, Is.EqualTo(expectedDelay));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/TestHelpers/TestStream.cs b/tests/Kampute.HttpClient.Test/TestHelpers/TestStream.cs
new file mode 100644
index 0000000..cedf9df
--- /dev/null
+++ b/tests/Kampute.HttpClient.Test/TestHelpers/TestStream.cs
@@ -0,0 +1,18 @@
+namespace Kampute.HttpClient.Test.TestHelpers
+{
+ using System;
+ using System.IO;
+
+ internal class TestStream : MemoryStream
+ {
+ private readonly bool seekable;
+
+ public TestStream(bool seekable) : base() => this.seekable = seekable;
+
+ public override bool CanSeek => seekable;
+
+ public override long Seek(long offset, SeekOrigin loc) => seekable
+ ? base.Seek(offset, loc)
+ : throw new NotSupportedException();
+ }
+}
diff --git a/tests/Kampute.HttpClient.Test/Utilities/AsyncUpdateThrottleTests.cs b/tests/Kampute.HttpClient.Test/Utilities/AsyncUpdateThrottleTests.cs
index a15f3a0..6b3dbe4 100644
--- a/tests/Kampute.HttpClient.Test/Utilities/AsyncUpdateThrottleTests.cs
+++ b/tests/Kampute.HttpClient.Test/Utilities/AsyncUpdateThrottleTests.cs
@@ -22,11 +22,11 @@ public async Task TryUpdateAsync_UpdatesValue()
var updateResult = await synchronizer.TryUpdateAsync(() => Task.FromResult(42));
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(updateResult, Is.True);
Assert.That(synchronizer.Value, Is.EqualTo(42));
- });
+ }
}
[Test]
@@ -48,12 +48,12 @@ public async Task TryUpdateAsync_DoesNotUpdateIfAnotherUpdateHasCompleted()
})
);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(results[0], Is.True);
Assert.That(results[1], Is.False);
Assert.That(synchronizer.Value, Is.EqualTo(2));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/Utilities/FlyweightCacheTests.cs b/tests/Kampute.HttpClient.Test/Utilities/FlyweightCacheTests.cs
index 4417fe2..c406d3f 100644
--- a/tests/Kampute.HttpClient.Test/Utilities/FlyweightCacheTests.cs
+++ b/tests/Kampute.HttpClient.Test/Utilities/FlyweightCacheTests.cs
@@ -26,11 +26,11 @@ public void Get_WhenKeyExists_ReturnsExistingValue()
var result1 = cache.Get(1);
var result2 = cache.Get(1);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(cache.Count, Is.EqualTo(1));
Assert.That(result2, Is.SameAs(result1));
- });
+ }
}
[Test]
diff --git a/tests/Kampute.HttpClient.Test/Utilities/ScopedCollectionTests.cs b/tests/Kampute.HttpClient.Test/Utilities/ScopedCollectionTests.cs
index 5ee23ae..7f35f97 100644
--- a/tests/Kampute.HttpClient.Test/Utilities/ScopedCollectionTests.cs
+++ b/tests/Kampute.HttpClient.Test/Utilities/ScopedCollectionTests.cs
@@ -17,11 +17,11 @@ public void BeginScope_ReturnsScopeWithProperties()
using var scope = context.BeginScope(items);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(scope, Is.Not.Null);
Assert.That(context, Is.EqualTo(items));
- });
+ }
}
[Test]
@@ -113,11 +113,11 @@ public async Task BeginScope_AsyncOperations_IsolatesScopes()
var results = await Task.WhenAll(task1, task2);
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(results[0], Is.EquivalentTo(expectedProperties1));
Assert.That(results[1], Is.EquivalentTo(expectedProperties2));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Test/Utilities/SharedDisposableTests.cs b/tests/Kampute.HttpClient.Test/Utilities/SharedDisposableTests.cs
index 6b50008..d74a60f 100644
--- a/tests/Kampute.HttpClient.Test/Utilities/SharedDisposableTests.cs
+++ b/tests/Kampute.HttpClient.Test/Utilities/SharedDisposableTests.cs
@@ -22,11 +22,11 @@ public void SharedDisposable_DefaultConstructor_CreatesResource_OnFirstReference
using var reference1 = sharedDisposable.AcquireReference();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(sharedDisposable.ReferenceCount, Is.EqualTo(1));
Assert.That(reference1.Instance, Is.Not.Null);
- });
+ }
}
[Test]
@@ -42,11 +42,11 @@ public void SharedDisposable_FactoryConstructor_CreatesResource_OnFirstReference
using var reference1 = sharedDisposable.AcquireReference();
using var reference2 = sharedDisposable.AcquireReference();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(sharedDisposable.ReferenceCount, Is.EqualTo(2));
Assert.That(factoryInvoked, Is.EqualTo(1));
- });
+ }
}
[Test]
@@ -61,18 +61,18 @@ public void SharedDisposable_DisposesResource_OnLastRelease()
Assert.That(sharedDisposable.ReferenceCount, Is.EqualTo(2));
reference1.Dispose();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(sharedDisposable.ReferenceCount, Is.EqualTo(1));
Assert.That(instance.IsDisposed, Is.False);
- });
+ }
reference2.Dispose();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
- Assert.That(sharedDisposable.ReferenceCount, Is.EqualTo(0));
+ Assert.That(sharedDisposable.ReferenceCount, Is.Zero);
Assert.That(instance.IsDisposed, Is.True);
- });
+ }
}
[Test]
@@ -99,11 +99,11 @@ public void SharedDisposable_MaintainsCorrectReferenceCount_OnConcurrentAcquireA
foreach (var thread in threads)
thread.Join();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(sharedDisposable.ReferenceCount, Is.Zero);
Assert.That(createdCount, Is.EqualTo(numberOfThreads));
- });
+ }
}
}
}
diff --git a/tests/Kampute.HttpClient.Xml.Test/HttpRestClientXmlExtensionsTests.cs b/tests/Kampute.HttpClient.Xml.Test/HttpRestClientXmlExtensionsTests.cs
index 01b07af..c892894 100644
--- a/tests/Kampute.HttpClient.Xml.Test/HttpRestClientXmlExtensionsTests.cs
+++ b/tests/Kampute.HttpClient.Xml.Test/HttpRestClientXmlExtensionsTests.cs
@@ -62,12 +62,12 @@ public async Task PostAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Post));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
@@ -89,12 +89,12 @@ public async Task PutAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Put));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
@@ -116,12 +116,12 @@ public async Task PatchAsXmlAsync_InvokesHttpClientCorrectly()
MockHttpResponse(request =>
{
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(request.Method, Is.EqualTo(HttpMethod.Patch));
Assert.That(request.RequestUri, Is.EqualTo(AbsoluteUrl("/echo")));
Assert.That(request.Content?.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
- });
+ }
return new HttpResponseMessage
{
diff --git a/tests/Kampute.HttpClient.Xml.Test/Kampute.HttpClient.Xml.Test.csproj b/tests/Kampute.HttpClient.Xml.Test/Kampute.HttpClient.Xml.Test.csproj
index 233ce5d..b1aa371 100644
--- a/tests/Kampute.HttpClient.Xml.Test/Kampute.HttpClient.Xml.Test.csproj
+++ b/tests/Kampute.HttpClient.Xml.Test/Kampute.HttpClient.Xml.Test.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
false
true
latest
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Kampute.HttpClient.Xml.Test/XmlContentTests.cs b/tests/Kampute.HttpClient.Xml.Test/XmlContentTests.cs
index 286c620..e26c4c9 100644
--- a/tests/Kampute.HttpClient.Xml.Test/XmlContentTests.cs
+++ b/tests/Kampute.HttpClient.Xml.Test/XmlContentTests.cs
@@ -17,12 +17,12 @@ public async Task WithDefaultEncoding_SetsContentCorrectly()
var xmlString = await xmlContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(xmlString, Is.EqualTo(expectedString));
Assert.That(xmlContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
Assert.That(xmlContent.Headers.ContentType?.CharSet, Is.EqualTo(Encoding.UTF8.WebName));
- });
+ }
}
[Test]
@@ -36,12 +36,12 @@ public async Task WithCustomEncoding_SetsContentCorrectly()
var xmlString = await xmlContent.ReadAsStringAsync();
- Assert.Multiple(() =>
+ using (Assert.EnterMultipleScope())
{
Assert.That(xmlString, Is.EqualTo(expectedString));
Assert.That(xmlContent.Headers.ContentType?.MediaType, Is.EqualTo(MediaTypeNames.Application.Xml));
Assert.That(xmlContent.Headers.ContentType?.CharSet, Is.EqualTo(encoding.WebName));
- });
+ }
}
}
}