From c084e833f2ce4f628bfebe34c3970c3a1c94121c Mon Sep 17 00:00:00 2001 From: David Perfors Date: Wed, 12 Nov 2025 22:55:00 +0100 Subject: [PATCH 1/2] Clone HttpRequestMessage so the copy is asserted and the original request can be disposed properly --- CHANGELOG.md | 4 +- .../PublicAPI.Unshipped.txt | 2 +- .../TestableHttpMessageHandler.cs | 22 ++++++++++- .../Utils/HttpRequestMessageCloner.cs | 38 +++++++++++++++++++ .../AssertingRequests.cs | 24 ++++++++++++ .../TestableHttpMessageHandlerTests.cs | 24 +++++++++++- 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a1e8e..09908c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - .NET Framework 4.6.2, 4.7.0 and 4.7.2, since these can't be tested using xUnit v3 ### Added - Support for .NET 9.0 -- support for .NET 10.0 +- Support for .NET 10.0 +### Changed +- The TestableHttpMessageHandler now makes a clone of the original request, so that the original request can be disposed. ## [0.11] - 2024-06-15 ### Removed diff --git a/src/TestableHttpClient/PublicAPI.Unshipped.txt b/src/TestableHttpClient/PublicAPI.Unshipped.txt index 5f28270..b7208c0 100644 --- a/src/TestableHttpClient/PublicAPI.Unshipped.txt +++ b/src/TestableHttpClient/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +override TestableHttpClient.TestableHttpMessageHandler.Dispose(bool disposing) -> void \ No newline at end of file diff --git a/src/TestableHttpClient/TestableHttpMessageHandler.cs b/src/TestableHttpClient/TestableHttpMessageHandler.cs index 416920a..5509fd8 100644 --- a/src/TestableHttpClient/TestableHttpMessageHandler.cs +++ b/src/TestableHttpClient/TestableHttpMessageHandler.cs @@ -17,9 +17,20 @@ public class TestableHttpMessageHandler : HttpMessageHandler /// public IEnumerable Requests => httpRequestMessages; + protected override void Dispose(bool disposing) + { + DisposeRequestMessages(); + base.Dispose(disposing); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "It gets disposed in the dispose method")] protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - httpRequestMessages.Enqueue(request); +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNull(request); +#endif + + httpRequestMessages.Enqueue(await HttpRequestMessageCloner.ClonaAsync(request, cancellationToken).ConfigureAwait(false)); HttpResponseMessage responseMessage = new(); HttpResponseContext context = new(request, httpRequestMessages, responseMessage, Options); @@ -55,6 +66,15 @@ public void RespondWith(IResponse response) /// The configuration itself (Options and the configured IResponse) will not be cleared or reset. public void ClearRequests() { + DisposeRequestMessages(); httpRequestMessages.Clear(); } + + private void DisposeRequestMessages() + { + foreach (HttpRequestMessage request in httpRequestMessages) + { + request.Dispose(); + } + } } diff --git a/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs b/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs new file mode 100644 index 0000000..6ce0f44 --- /dev/null +++ b/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs @@ -0,0 +1,38 @@ +using System.Diagnostics.CodeAnalysis; + +namespace TestableHttpClient.Utils; + +internal static class HttpRequestMessageCloner +{ + internal static async Task ClonaAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpRequestMessage clone = new() + { + Method = request.Method, + RequestUri = request.RequestUri, + Version = request.Version, + }; + + foreach (var item in request.Headers) + { + clone.Headers.TryAddWithoutValidation(item.Key, item.Value); + } + + // Copy content (buffered) + if (request.Content is not null) + { + var bytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + var contentClone = new ByteArrayContent(bytes); + + // copy content headers + foreach (var header in request.Content.Headers) + { + contentClone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + clone.Content = contentClone; + } + + return clone; + } +} diff --git a/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs b/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs index 26875f8..6407ef1 100644 --- a/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs +++ b/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs @@ -162,6 +162,30 @@ public async Task AssertingContent() using StringContent content = new("my special content"); _ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken); +#if NETFRAMEWORK + // On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test. + testHandler.ShouldHaveMadeRequests(); +#else + testHandler.ShouldHaveMadeRequests().WithContent("my special content"); + testHandler.ShouldHaveMadeRequests().WithContent("my*content"); + testHandler.ShouldHaveMadeRequests().WithContent("*"); + + Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("")); + Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("my")); +#endif + } + + [Fact] + public async Task AssertingContent_WhenOriginalContentIsDisposed() + { + using TestableHttpMessageHandler testHandler = new(); + using HttpClient client = new(testHandler); + + using (StringContent content = new("my special content")) + { + _ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken); + } + #if NETFRAMEWORK // On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test. testHandler.ShouldHaveMadeRequests(); diff --git a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerTests.cs b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerTests.cs index feb462d..9539673 100644 --- a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerTests.cs +++ b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerTests.cs @@ -16,7 +16,7 @@ public async Task SendAsync_WhenRequestsAreMade_LogsRequests() _ = await client.SendAsync(request, TestContext.Current.CancellationToken); - Assert.Contains(request, sut.Requests); + Assert.Contains(request, sut.Requests, new SimpleHttpRequestMessageComparer()); } [Fact] @@ -34,7 +34,7 @@ public async Task SendAsync_WhenMultipleRequestsAreMade_AllRequestsAreLogged() _ = await client.SendAsync(request3, TestContext.Current.CancellationToken); _ = await client.SendAsync(request4, TestContext.Current.CancellationToken); - Assert.Equal([request1, request2, request3, request4], sut.Requests); + Assert.Equal([request1, request2, request3, request4], sut.Requests, new SimpleHttpRequestMessageComparer()); } [Fact] @@ -210,4 +210,24 @@ public void Dispose() GC.SuppressFinalize(this); } } + + private sealed class SimpleHttpRequestMessageComparer : IEqualityComparer + { + public bool Equals(HttpRequestMessage? x, HttpRequestMessage? y) + { + if (x is null && y is null) + { + return true; + } + + if (x is null || y is null) + { + return false; + } + + return x.Method == y.Method && x.RequestUri == y.RequestUri && x.Version == y.Version; + } + + public int GetHashCode([DisallowNull] HttpRequestMessage obj) => HashCode.Combine(obj.Method, obj.RequestUri, obj.Version); + } } From 9a4ad59c3766d15f48b625c17c6f8f0eb129502d Mon Sep 17 00:00:00 2001 From: David Perfors Date: Thu, 13 Nov 2025 20:49:16 +0100 Subject: [PATCH 2/2] Reduce differences between .netstandard and .net by adding more pollyfills. Reduce netframework specific tests, since they are no longer needed because of the cloning. --- CHANGELOG.md | 3 ++- .../HttpRequestMessagesCheckExtensions.cs | 8 ------- .../TestableHttpMessageHandler.cs | 8 +++---- src/TestableHttpClient/Utils/Guard.cs | 2 +- .../Utils/HttpRequestMessageCloner.cs | 11 +++++----- .../Utils/NetStandardPollyFill.cs | 18 +++++++++++++++ src/TestableHttpClient/Utils/StringMatcher.cs | 5 +---- .../AssertingRequests.cs | 15 ------------- .../CustomizeJsonSerialization.cs | 22 ------------------- .../NetFrameworkPollyFill.cs | 10 +++++++-- 10 files changed, 38 insertions(+), 64 deletions(-) create mode 100644 src/TestableHttpClient/Utils/NetStandardPollyFill.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 09908c6..dcd617c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Support for .NET 9.0 - Support for .NET 10.0 ### Changed -- The TestableHttpMessageHandler now makes a clone of the original request, so that the original request can be disposed. +- The TestableHttpMessageHandler now makes a clone of the original request, so that the original request can be disposed. + This change also makes it possible to assert the content on .NET Framework. ## [0.11] - 2024-06-15 ### Removed diff --git a/src/TestableHttpClient/HttpRequestMessagesCheckExtensions.cs b/src/TestableHttpClient/HttpRequestMessagesCheckExtensions.cs index e6188a1..fd55d7f 100644 --- a/src/TestableHttpClient/HttpRequestMessagesCheckExtensions.cs +++ b/src/TestableHttpClient/HttpRequestMessagesCheckExtensions.cs @@ -258,7 +258,6 @@ private static IHttpRequestMessagesCheck WithHeader(this IHttpRequestMessagesChe /// The implementation that hold all the request messages. /// The expected content, supports wildcards. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCheck check, string pattern) => WithContent(check, pattern, null); /// @@ -268,7 +267,6 @@ private static IHttpRequestMessagesCheck WithHeader(this IHttpRequestMessagesChe /// The expected content, supports wildcards. /// The expected number of requests. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCheck check, string pattern, int expectedNumberOfRequests) => WithContent(check, pattern, (int?)expectedNumberOfRequests); private static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCheck check, string pattern, int? expectedNumberOfRequests) @@ -285,7 +283,6 @@ private static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCh /// The implementation that hold all the request messages. /// The object representation of the expected request content. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessagesCheck check, object? jsonObject) => WithJsonContent(check, jsonObject, null, null); /// @@ -295,7 +292,6 @@ private static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCh /// The object representation of the expected request content. /// The serializer options that should be used for serializing te content. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessagesCheck check, object? jsonObject, JsonSerializerOptions jsonSerializerOptions) => WithJsonContent(check, jsonObject, jsonSerializerOptions, null); /// @@ -305,7 +301,6 @@ private static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCh /// The object representation of the expected request content. /// The expected number of requests. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessagesCheck check, object? jsonObject, int expectedNumberOfRequests) => WithJsonContent(check, jsonObject, null, (int?)expectedNumberOfRequests); /// @@ -316,7 +311,6 @@ private static IHttpRequestMessagesCheck WithContent(this IHttpRequestMessagesCh /// The serializer options that should be used for serializing the content. /// The expected number of requests. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessagesCheck check, object? jsonObject, JsonSerializerOptions jsonSerializerOptions, int expectedNumberOfRequests) => WithJsonContent(check, jsonObject, jsonSerializerOptions, (int?)expectedNumberOfRequests); private static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessagesCheck check, object? jsonObject, JsonSerializerOptions? jsonSerializerOptions, int? expectedNumberOfRequests) @@ -334,7 +328,6 @@ private static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessag /// The implementation that hold all the request messages. /// The collection of key/value pairs that should be url encoded. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithFormUrlEncodedContent(this IHttpRequestMessagesCheck check, IEnumerable> nameValueCollection) => WithFormUrlEncodedContent(check, nameValueCollection, null); /// @@ -344,7 +337,6 @@ private static IHttpRequestMessagesCheck WithJsonContent(this IHttpRequestMessag /// The collection of key/value pairs that should be url encoded. /// The expected number of requests. /// The for further assertions. - /// Note that on .NET Framework, the HttpClient might dispose the content after sending the request. public static IHttpRequestMessagesCheck WithFormUrlEncodedContent(this IHttpRequestMessagesCheck check, IEnumerable> nameValueCollection, int expectedNumberOfRequests) => WithFormUrlEncodedContent(check, nameValueCollection, (int?)expectedNumberOfRequests); private static IHttpRequestMessagesCheck WithFormUrlEncodedContent(this IHttpRequestMessagesCheck check, IEnumerable> nameValueCollection, int? expectedNumberOfRequests) diff --git a/src/TestableHttpClient/TestableHttpMessageHandler.cs b/src/TestableHttpClient/TestableHttpMessageHandler.cs index 5509fd8..beb91aa 100644 --- a/src/TestableHttpClient/TestableHttpMessageHandler.cs +++ b/src/TestableHttpClient/TestableHttpMessageHandler.cs @@ -26,9 +26,7 @@ protected override void Dispose(bool disposing) [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "It gets disposed in the dispose method")] protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { -#if NET8_0_OR_GREATER - ArgumentNullException.ThrowIfNull(request); -#endif + Guard.ThrowIfNull(request); httpRequestMessages.Enqueue(await HttpRequestMessageCloner.ClonaAsync(request, cancellationToken).ConfigureAwait(false)); @@ -38,9 +36,9 @@ protected override async Task SendAsync(HttpRequestMessage responseMessage.RequestMessage ??= request; -#if !NET6_0_OR_GREATER + // In .NET Standard, a response message can be null, but we need it to be at least empty like in the newer versions. + // Newer versions will always have a content, even if it's empty. responseMessage.Content ??= new StringContent(""); -#endif return responseMessage; } diff --git a/src/TestableHttpClient/Utils/Guard.cs b/src/TestableHttpClient/Utils/Guard.cs index e07c677..780832e 100644 --- a/src/TestableHttpClient/Utils/Guard.cs +++ b/src/TestableHttpClient/Utils/Guard.cs @@ -29,7 +29,7 @@ internal static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgum throw new ArgumentException("String should not be empty", paramName); } #else - ArgumentNullException.ThrowIfNullOrEmpty(argument, paramName); + ArgumentException.ThrowIfNullOrEmpty(argument, paramName); #endif } } diff --git a/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs b/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs index 6ce0f44..46c23cb 100644 --- a/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs +++ b/src/TestableHttpClient/Utils/HttpRequestMessageCloner.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace TestableHttpClient.Utils; +namespace TestableHttpClient.Utils; internal static class HttpRequestMessageCloner { @@ -18,12 +16,13 @@ internal static async Task ClonaAsync(HttpRequestMessage req clone.Headers.TryAddWithoutValidation(item.Key, item.Value); } - // Copy content (buffered) if (request.Content is not null) { - var bytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + var bytes = await request.Content + .ReadAsByteArrayAsync(cancellationToken) + .ConfigureAwait(false); var contentClone = new ByteArrayContent(bytes); - + contentClone.Headers.Clear(); // copy content headers foreach (var header in request.Content.Headers) { diff --git a/src/TestableHttpClient/Utils/NetStandardPollyFill.cs b/src/TestableHttpClient/Utils/NetStandardPollyFill.cs new file mode 100644 index 0000000..f3d1acc --- /dev/null +++ b/src/TestableHttpClient/Utils/NetStandardPollyFill.cs @@ -0,0 +1,18 @@ +#if NETSTANDARD + +namespace TestableHttpClient.Utils; + +internal static class NetStandardPollyFill +{ + public static Task ReadAsByteArrayAsync(this HttpContent content, CancellationToken cancellationToken = default) + { + return content.ReadAsByteArrayAsync(); + } + + public static string Replace(this string input, string oldValue, string newValue, StringComparison comparisonType) + { + return input.Replace(oldValue, newValue); + } +} + +#endif diff --git a/src/TestableHttpClient/Utils/StringMatcher.cs b/src/TestableHttpClient/Utils/StringMatcher.cs index 1055bf6..bbcb2e2 100644 --- a/src/TestableHttpClient/Utils/StringMatcher.cs +++ b/src/TestableHttpClient/Utils/StringMatcher.cs @@ -7,11 +7,8 @@ internal static class StringMatcher internal static bool Matches(string value, string pattern, bool ignoreCase = false) { var escapedPattern = Regex.Escape(pattern); -#if NETSTANDARD2_0 - var regex = escapedPattern.Replace("\\*", "(.*)"); -#else + var regex = escapedPattern.Replace("\\*", "(.*)", StringComparison.InvariantCultureIgnoreCase); -#endif RegexOptions options = ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; return Regex.IsMatch(value, $"^{regex}$", options); } diff --git a/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs b/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs index 6407ef1..5f1f317 100644 --- a/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs +++ b/test/TestableHttpClient.IntegrationTests/AssertingRequests.cs @@ -162,17 +162,12 @@ public async Task AssertingContent() using StringContent content = new("my special content"); _ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken); -#if NETFRAMEWORK - // On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test. - testHandler.ShouldHaveMadeRequests(); -#else testHandler.ShouldHaveMadeRequests().WithContent("my special content"); testHandler.ShouldHaveMadeRequests().WithContent("my*content"); testHandler.ShouldHaveMadeRequests().WithContent("*"); Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("")); Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("my")); -#endif } [Fact] @@ -186,17 +181,12 @@ public async Task AssertingContent_WhenOriginalContentIsDisposed() _ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken); } -#if NETFRAMEWORK - // On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test. - testHandler.ShouldHaveMadeRequests(); -#else testHandler.ShouldHaveMadeRequests().WithContent("my special content"); testHandler.ShouldHaveMadeRequests().WithContent("my*content"); testHandler.ShouldHaveMadeRequests().WithContent("*"); Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("")); Assert.Throws(() => testHandler.ShouldHaveMadeRequests().WithContent("my")); -#endif } [Fact] @@ -208,12 +198,7 @@ public async Task AssertJsonContent() using StringContent content = new("{}", Encoding.UTF8, "application/json"); _ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken); -#if NETFRAMEWORK - // On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test. - testHandler.ShouldHaveMadeRequests(); -#else testHandler.ShouldHaveMadeRequests().WithJsonContent(new { }); -#endif } [Fact] diff --git a/test/TestableHttpClient.IntegrationTests/CustomizeJsonSerialization.cs b/test/TestableHttpClient.IntegrationTests/CustomizeJsonSerialization.cs index 40d13bd..a47b6c8 100644 --- a/test/TestableHttpClient.IntegrationTests/CustomizeJsonSerialization.cs +++ b/test/TestableHttpClient.IntegrationTests/CustomizeJsonSerialization.cs @@ -14,11 +14,7 @@ public async Task ByDefault_CamelCasing_is_used_for_json_serialization() sut.RespondWith(Json(new { Name = "Charlie" })); using HttpClient client = sut.CreateClient(); -#if NETFRAMEWORK - string json = await client.GetStringAsync("http://localhost/myjson"); -#else string json = await client.GetStringAsync("http://localhost/myjson", TestContext.Current.CancellationToken); -#endif Assert.Equal("{\"name\":\"Charlie\"}", json); } @@ -31,11 +27,7 @@ public async Task But_this_can_be_changed() sut.RespondWith(Json(new { Name = "Charlie" })); using HttpClient client = sut.CreateClient(); -#if NETFRAMEWORK - string json = await client.GetStringAsync("http://localhost/myjson"); -#else string json = await client.GetStringAsync("http://localhost/myjson", TestContext.Current.CancellationToken); -#endif Assert.Equal("{\"Name\":\"Charlie\"}", json); } @@ -47,11 +39,7 @@ public async Task But_Also_directly_on_the_response() sut.RespondWith(Json(new { Name = "Charlie" }, jsonSerializerOptions: new JsonSerializerOptions())); using HttpClient client = sut.CreateClient(); -#if NETFRAMEWORK - string json = await client.GetStringAsync("http://localhost/myjson"); -#else string json = await client.GetStringAsync("http://localhost/myjson", TestContext.Current.CancellationToken); -#endif Assert.Equal("{\"Name\":\"Charlie\"}", json); } @@ -63,12 +51,7 @@ public async Task Asserting_also_works_this_way() using HttpClient client = sut.CreateClient(); await client.PostAsJsonAsync("http://localhost", new { Name = "Charlie" }, cancellationToken: TestContext.Current.CancellationToken); -#if NETFRAMEWORK - // Well this doesn't really work on .NET Framework. - sut.ShouldHaveMadeRequests(); -#else sut.ShouldHaveMadeRequests().WithJsonContent(new { Name = "Charlie" }); -#endif } [Fact] @@ -84,11 +67,6 @@ public async Task And_we_can_go_crazy_with_it() await client.PostAsJsonAsync("http://localhost", new { Name = "Charlie" }, options, cancellationToken: TestContext.Current.CancellationToken); -#if NETFRAMEWORK - // Well this doesn't really work on .NET Framework. - sut.ShouldHaveMadeRequests(); -#else sut.ShouldHaveMadeRequests().WithJsonContent(new { Name = "Charlie" }, options); -#endif } } diff --git a/test/TestableHttpClient.IntegrationTests/NetFrameworkPollyFill.cs b/test/TestableHttpClient.IntegrationTests/NetFrameworkPollyFill.cs index 2d036e7..64a09c1 100644 --- a/test/TestableHttpClient.IntegrationTests/NetFrameworkPollyFill.cs +++ b/test/TestableHttpClient.IntegrationTests/NetFrameworkPollyFill.cs @@ -1,8 +1,9 @@ -using System.Threading; +#if NETFRAMEWORK + +using System.Threading; namespace TestableHttpClient.IntegrationTests; -#if NETFRAMEWORK internal static class NetFrameworkPollyFill { @@ -10,6 +11,11 @@ public static Task ReadAsStringAsync(this HttpContent content, Cancellat { return content.ReadAsStringAsync(); } + + public static Task GetStringAsync(this HttpClient client, string requestUri, CancellationToken cancellationToken = default) + { + return client.GetStringAsync(requestUri); + } } #endif