diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs index a834695bf2b..49005d259d4 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/NoRemoteCallHandler.cs @@ -9,14 +9,18 @@ namespace Microsoft.Extensions.Http.Resilience.PerformanceTests; internal sealed class NoRemoteCallHandler : DelegatingHandler { + private readonly HttpResponseMessage _response; private readonly Task _completedResponse; + private volatile bool _disposed; public NoRemoteCallHandler() { - _completedResponse = Task.FromResult(new HttpResponseMessage + _response = new HttpResponseMessage { StatusCode = System.Net.HttpStatusCode.OK - }); + }; + + _completedResponse = Task.FromResult(_response); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -25,4 +29,20 @@ protected override Task SendAsync(HttpRequestMessage reques return _completedResponse; #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks } + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _response; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + _response.Dispose(); + } + + base.Dispose(disposing); + } } diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/RetryBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/RetryBenchmark.cs index 16da92acd4a..b1f17d18709 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/RetryBenchmark.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/RetryBenchmark.cs @@ -76,4 +76,10 @@ public Task Retry_Polly_V8() { return _v8!.SendAsync(Request, _cancellationToken); } + + [Benchmark] + public HttpResponseMessage Retry_Polly_V8_Sync() + { + return _v8!.Send(Request, _cancellationToken); + } } diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/StandardResilienceBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/StandardResilienceBenchmark.cs index d7cc50f090e..75dd440fc11 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/StandardResilienceBenchmark.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/StandardResilienceBenchmark.cs @@ -78,4 +78,10 @@ public Task StandardPipeline_Polly_V8() { return _v8!.SendAsync(Request, _cancellationToken); } + + [Benchmark] + public HttpResponseMessage StandardPipeline_Polly_V8_Sync() + { + return _v8!.Send(Request, _cancellationToken); + } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandler.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandler.cs index a79d62e0975..41a408fa718 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandler.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/ResilienceHandler.cs @@ -54,28 +54,18 @@ protected override async Task SendAsync(HttpRequestMessage { _ = Throw.IfNull(request); - var pipeline = _pipelineProvider(request); - var created = false; - if (request.GetResilienceContext() is not ResilienceContext context) - { - context = ResilienceContextPool.Shared.Get(cancellationToken); - created = true; - request.SetResilienceContext(context); - } + ResiliencePipeline pipeline = _pipelineProvider(request); - if (request.GetRequestMetadata() is RequestMetadata requestMetadata) - { - context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata); - } - - context.Properties.Set(ResilienceKeys.RequestMessage, request); + ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created); + TrySetRequestMetadata(context, request); + SetRequestMessage(context, request); try { - var outcome = await pipeline.ExecuteOutcomeAsync( + Outcome outcome = await pipeline.ExecuteOutcomeAsync( static async (context, state) => { - var request = context.Properties.GetValue(ResilienceKeys.RequestMessage, state.request); + HttpRequestMessage request = GetRequestMessage(context, state.request); // Always re-assign the context to this request message before execution. // This is because for primary actions the context is also cloned and we need to re-assign it @@ -84,7 +74,10 @@ static async (context, state) => try { - var response = await state.instance.SendCoreAsync(request, context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + HttpResponseMessage response = await state.instance + .SendCoreAsync(request, context.CancellationToken) + .ConfigureAwait(context.ContinueOnCapturedContext); + return Outcome.FromResult(response); } #pragma warning disable CA1031 // Do not catch general exception types @@ -104,19 +97,99 @@ static async (context, state) => } finally { - if (created) - { - ResilienceContextPool.Shared.Return(context); - request.SetResilienceContext(null); - } - else - { - // Restore the original context - request.SetResilienceContext(context); - } + RestoreResilienceContext(context, request, created); + } + } + +#if NET6_0_OR_GREATER + /// + /// Sends an HTTP request to the inner handler to send to the server as a synchronous operation. + /// + /// The HTTP request message to send to the server. + /// A cancellation token to cancel operation. + /// An HTTP response received from the server. + /// If is . + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + _ = Throw.IfNull(request); + + ResiliencePipeline pipeline = _pipelineProvider(request); + + ResilienceContext context = GetOrSetResilienceContext(request, cancellationToken, out bool created); + TrySetRequestMetadata(context, request); + SetRequestMessage(context, request); + + try + { + return pipeline.Execute( + static (context, state) => + { + HttpRequestMessage request = GetRequestMessage(context, state.request); + + // Always re-assign the context to this request message before execution. + // This is because for primary actions the context is also cloned and we need to re-assign it + // here because Polly doesn't have any other events that we can hook into. + request.SetResilienceContext(context); + + return state.instance.SendCore(request, context.CancellationToken); + }, + context, + (instance: this, request)); + } + finally + { + RestoreResilienceContext(context, request, created); + } + } +#endif + + private static ResilienceContext GetOrSetResilienceContext(HttpRequestMessage request, CancellationToken cancellationToken, out bool created) + { + created = false; + + if (request.GetResilienceContext() is not ResilienceContext context) + { + context = ResilienceContextPool.Shared.Get(cancellationToken); + created = true; + request.SetResilienceContext(context); + } + + return context; + } + + private static void TrySetRequestMetadata(ResilienceContext context, HttpRequestMessage request) + { + if (request.GetRequestMetadata() is RequestMetadata requestMetadata) + { + context.Properties.Set(ResilienceKeys.RequestMetadata, requestMetadata); + } + } + + private static void SetRequestMessage(ResilienceContext context, HttpRequestMessage request) + => context.Properties.Set(ResilienceKeys.RequestMessage, request); + + private static HttpRequestMessage GetRequestMessage(ResilienceContext context, HttpRequestMessage request) + => context.Properties.GetValue(ResilienceKeys.RequestMessage, request); + + private static void RestoreResilienceContext(ResilienceContext context, HttpRequestMessage request, bool created) + { + if (created) + { + ResilienceContextPool.Shared.Return(context); + request.SetResilienceContext(null); + } + else + { + // Restore the original context + request.SetResilienceContext(context); } } private Task SendCoreAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken) => base.SendAsync(requestMessage, cancellationToken); + +#if NET6_0_OR_GREATER + private HttpResponseMessage SendCore(HttpRequestMessage requestMessage, CancellationToken cancellationToken) + => base.Send(requestMessage, cancellationToken); +#endif } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs index 6b81d189ee6..2dd8cf0721a 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs @@ -71,8 +71,13 @@ public void Dispose() } } - [Fact] - public async Task SendAsync_EnsureContextFlows() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_EnsureContextFlows(bool asynchronous = true) { var key = new ResiliencePropertyKey("custom-data"); using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -99,13 +104,18 @@ public async Task SendAsync_EnsureContextFlows() using var client = CreateClientWithHandler(); - using var _ = await client.SendAsync(request, _cancellationTokenSource.Token); + using var _ = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); Assert.Equal(2, calls); } - [Fact] - public async Task SendAsync_NoErrors_ShouldReturnSingleResponse() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_NoErrors_ShouldReturnSingleResponse(bool asynchronous = true) { SetupRouting(); SetupRoutes(1, "https://enpoint-{0}:80/"); @@ -114,15 +124,20 @@ public async Task SendAsync_NoErrors_ShouldReturnSingleResponse() AddResponse(HttpStatusCode.OK); - using var _ = await client.SendAsync(request, _cancellationTokenSource.Token); + using var _ = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); AssertNoResponse(); Assert.Single(Requests); Assert.Equal("https://enpoint-1:80/some-path?query", Requests[0]); } - [Fact] - public async Task SendAsync_NoRoutes_Throws() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_NoRoutes_Throws(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -133,12 +148,18 @@ public async Task SendAsync_NoRoutes_Throws() using var client = CreateClientWithHandler(); - var exception = await Assert.ThrowsAsync(async () => await client.SendAsync(request, _cancellationTokenSource.Token)); + var exception = await Assert.ThrowsAsync( + async () => await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token)); Assert.Equal("The routing strategy did not provide any route URL on the first attempt.", exception.Message); } - [Fact] - public async Task SendAsync_NoRoutesLeftAndNoResult_ShouldThrow() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_NoRoutesLeftAndNoResult_ShouldThrow(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -149,15 +170,21 @@ public async Task SendAsync_NoRoutesLeftAndNoResult_ShouldThrow() using var client = CreateClientWithHandler(); - var exception = await Assert.ThrowsAsync(async () => await client.SendAsync(request, _cancellationTokenSource.Token)); + var exception = await Assert.ThrowsAsync( + async () => await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token)); Assert.Equal("Something went wrong!", exception.Message); Assert.Equal(2, Requests.Count); Assert.Equal(2, Requests.Distinct().Count()); } - [Fact] - public async Task SendAsync_NoRoutesLeftAndSomeResultPresent_ShouldReturn() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_NoRoutesLeftAndSomeResultPresent_ShouldReturn(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -170,13 +197,18 @@ public async Task SendAsync_NoRoutesLeftAndSomeResultPresent_ShouldReturn() using var client = CreateClientWithHandler(); - using var result = await client.SendAsync(request, _cancellationTokenSource.Token); + using var result = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); Assert.Equal(DefaultHedgingAttempts + 1, Requests.Count); Assert.Equal(HttpStatusCode.ServiceUnavailable, result.StatusCode); } - [Fact] - public async Task SendAsync_EnsureDistinctContextForEachAttempt() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_EnsureDistinctContextForEachAttempt(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -189,13 +221,18 @@ public async Task SendAsync_EnsureDistinctContextForEachAttempt() using var client = CreateClientWithHandler(); - using var _ = await client.SendAsync(request, _cancellationTokenSource.Token); + using var _ = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); RequestContexts.Distinct().OfType().Should().HaveCount(3); } - [Fact] - public async Task SendAsync_EnsureContextReplacedInRequestMessage() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_EnsureContextReplacedInRequestMessage(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); var originalContext = ResilienceContextPool.Shared.Get(); @@ -210,15 +247,20 @@ public async Task SendAsync_EnsureContextReplacedInRequestMessage() using var client = CreateClientWithHandler(); - using var _ = await client.SendAsync(request, _cancellationTokenSource.Token); + using var _ = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); RequestContexts.Distinct().OfType().Should().HaveCount(3); request.GetResilienceContext().Should().Be(originalContext); } - [Fact] - public async Task SendAsync_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -232,12 +274,17 @@ public async Task SendAsync_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts() using var client = CreateClientWithHandler(); - using var _ = await client.SendAsync(request, _cancellationTokenSource.Token); + using var _ = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); Assert.Equal(2, Requests.Count); } - [Fact] - public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_FailedExecution_ShouldReturnResponseFromHedging(bool asynchronous = true) { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); @@ -250,7 +297,7 @@ public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() using var client = CreateClientWithHandler(); - using var result = await client.SendAsync(request, _cancellationTokenSource.Token); + using var result = await SendRequest(client, request, asynchronous, _cancellationTokenSource.Token); Assert.Equal(3, Requests.Count); Assert.Equal(HttpStatusCode.OK, result.StatusCode); Assert.Equal("https://enpoint-1:80/some-path?query", Requests[0]); @@ -258,6 +305,23 @@ public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() Assert.Equal("https://enpoint-3:80/some-path?query", Requests[2]); } + protected static Task SendRequest( + HttpClient client, HttpRequestMessage request, bool asynchronous, CancellationToken cancellationToken = default) + { +#if NET6_0_OR_GREATER + if (asynchronous) + { + return client.SendAsync(request, cancellationToken); + } + else + { + return Task.FromResult(client.Send(request, cancellationToken)); + } +#else + return client.SendAsync(request, cancellationToken); +#endif + } + protected void AssertNoResponse() => Assert.Empty(_responses); protected void AddResponse(HttpStatusCode statusCode) => AddResponse(statusCode, 1); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs index debafe03127..237ccfa6e73 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs @@ -191,10 +191,17 @@ public void VerifyPipeline() inner.Strategies[2].Options.Should().BeOfType(); } + [Theory] +#if NET6_0_OR_GREATER + [InlineData(null, true)] + [InlineData("custom-key", true)] + [InlineData(null, false)] + [InlineData("custom-key", false)] +#else [InlineData(null)] [InlineData("custom-key")] - [Theory] - public async Task VerifyPipelineSelection(string? customKey) +#endif + public async Task VerifyPipelineSelection(string? customKey, bool asynchronous = true) { var noPolicy = ResiliencePipeline.Empty; var provider = new Mock>(MockBehavior.Strict); @@ -216,13 +223,18 @@ public async Task VerifyPipelineSelection(string? customKey) using var request = new HttpRequestMessage(HttpMethod.Get, "https://key:80/discarded"); AddResponse(HttpStatusCode.OK); - using var response = await client.SendAsync(request, CancellationToken.None); + using var response = await SendRequest(client, request, asynchronous); provider.VerifyAll(); } - [Fact] - public async Task DynamicReloads_Ok() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task DynamicReloads_Ok(bool asynchronous = true) { // arrange var requests = new List(); @@ -242,19 +254,24 @@ public async Task DynamicReloads_Ok() // act && assert AddResponse(HttpStatusCode.InternalServerError, 3); using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - using var _ = await client.SendAsync(firstRequest); + using var _ = await SendRequest(client, firstRequest, asynchronous); AssertNoResponse(); reloadAction(new() { { "standard:Hedging:MaxHedgedAttempts", "6" } }); AddResponse(HttpStatusCode.InternalServerError, 7); using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - using var __ = await client.SendAsync(secondRequest); + using var __ = await SendRequest(client, secondRequest, asynchronous); AssertNoResponse(); } - [Fact] - public async Task NoRouting_Ok() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task NoRouting_Ok(bool asynchronous = true) { // arrange Builder.Services.Configure(Builder.RoutingStrategyBuilder.Name, options => options.RoutingStrategyProvider = null); @@ -263,15 +280,20 @@ public async Task NoRouting_Ok() // act && assert AddResponse(HttpStatusCode.InternalServerError, 3); - using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://some-endpoint:1234/some-path?query"); - using var _ = await client.SendAsync(firstRequest); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://some-endpoint:1234/some-path?query"); + using var _ = await SendRequest(client, request, asynchronous); AssertNoResponse(); Requests.Should().AllSatisfy(r => r.Should().Be("https://some-endpoint:1234/some-path?query")); } - [Fact] - public async Task SendAsync_FailedConnect_ShouldReturnResponseFromHedging() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_FailedConnect_ShouldReturnResponseFromHedging(bool asynchronous = true) { const string FailingEndpoint = "www.failing-host.com"; @@ -305,10 +327,11 @@ public async Task SendAsync_FailedConnect_ShouldReturnResponseFromHedging() await using var provider = services.BuildServiceProvider(); var clientFactory = provider.GetRequiredService(); using var client = clientFactory.CreateClient(ClientId); + using var request = new HttpRequestMessage(HttpMethod.Get, $"https://{FailingEndpoint}:3000"); var ex = await Record.ExceptionAsync(async () => { - using var _ = await client.GetAsync($"https://{FailingEndpoint}:3000"); + using var _ = await SendRequest(client, request, asynchronous); }); Assert.Null(ex); @@ -329,5 +352,21 @@ protected override async Task SendAsync(HttpRequestMessage await Task.Delay(1000, cancellationToken); return new HttpResponseMessage(HttpStatusCode.OK); } + +#if NET6_0_OR_GREATER + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + if (request.RequestUri?.Host == failingEndpoint) + { + Task.Delay(100, cancellationToken).GetAwaiter().GetResult(); + throw new OperationCanceledExceptionMock(new TimeoutException()); + } + + Task.Delay(1000, cancellationToken).GetAwaiter().GetResult(); + return new HttpResponseMessage(HttpStatusCode.OK); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } +#endif } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs index 1f4a299285f..752c4ee90e1 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Helpers/TestHandlerStub.cs @@ -27,4 +27,13 @@ protected override Task SendAsync(HttpRequestMessage reques { return _handlerFunc(request, cancellationToken); } + +#if NET6_0_OR_GREATER + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { +#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits + return _handlerFunc(request, cancellationToken).GetAwaiter().GetResult(); +#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + } +#endif } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs index 06f21c7d9f1..6d1713150d7 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.Http.Resilience.Test.Internal; public class RequestMessageSnapshotStrategyTests { [Fact] - public async Task SendAsync_EnsureSnapshotAttached() + public async Task ExecuteAsync_EnsureSnapshotAttached() { var strategy = Create(); var context = ResilienceContextPool.Shared.Get(); @@ -32,7 +32,7 @@ public async Task SendAsync_EnsureSnapshotAttached() } [Fact] - public void ExecuteAsync_requestMessageNotFound_Throws() + public void ExecuteAsync_RequestMessageNotFound_Throws() { var strategy = Create(); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/GrpcResilienceTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/GrpcResilienceTests.cs index a4fc4a7ddeb..a1bb703ae5c 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/GrpcResilienceTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/GrpcResilienceTests.cs @@ -40,40 +40,59 @@ public GrpcResilienceTests() _handler = _host.GetTestServer().CreateHandler(); } - [Fact] - public async Task SayHello_NoResilience_OK() + [Theory] + [CombinatorialData] + public async Task SayHello_NoResilience_OK(bool asynchronous) { - var response = await CreateClient().SayHelloAsync(new HelloRequest { Name = "dummy" }); + var response = await SendRequest(CreateClient(), asynchronous); response.Message.Should().Be("HI!"); } - [Fact] - public async Task SayHello_StandardResilience_OK() + [Theory] + [CombinatorialData] + public async Task SayHello_StandardResilience_OK(bool asynchronous) { - var response = await CreateClient(builder => builder.AddStandardResilienceHandler()).SayHelloAsync(new HelloRequest { Name = "dummy" }); + var client = CreateClient(builder => builder.AddStandardResilienceHandler()); + var response = await SendRequest(client, asynchronous); response.Message.Should().Be("HI!"); } - [Fact] - public async Task SayHello_StandardHedging_OK() + [Theory] + [CombinatorialData] + public async Task SayHello_StandardHedging_OK(bool asynchronous) { - var response = await CreateClient(builder => builder.AddStandardHedgingHandler()).SayHelloAsync(new HelloRequest { Name = "dummy" }); + var client = CreateClient(builder => builder.AddStandardHedgingHandler()); + var response = await SendRequest(client, asynchronous); response.Message.Should().Be("HI!"); } - [Fact] - public async Task SayHello_CustomResilience_OK() + [Theory] + [CombinatorialData] + public async Task SayHello_CustomResilience_OK(bool asynchronous) { var client = CreateClient(builder => builder.AddResilienceHandler("custom", builder => builder.AddTimeout(TimeSpan.FromSeconds(1)))); - - var response = await client.SayHelloAsync(new HelloRequest { Name = "dummy" }); + var response = await SendRequest(client, asynchronous); response.Message.Should().Be("HI!"); } + private static Task SendRequest(Greeter.GreeterClient client, bool asynchronous) + { + var request = new HelloRequest { Name = "dummy" }; + + if (asynchronous) + { + return client.SayHelloAsync(request).ResponseAsync; + } + else + { + return Task.FromResult(client.SayHello(request)); + } + } + private Greeter.GreeterClient CreateClient(Action? configure = null) { var services = new ServiceCollection(); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs index 67b9af4eeed..920f831fe2d 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.BySelector.cs @@ -89,12 +89,24 @@ public void SelectPipelineBy_Ok(bool standardResilience, string url, string expe Assert.NotSame(provider(request), provider(request)); } + [Theory] +#if NET6_0_OR_GREATER + [InlineData(true, "https://dummy:21/path", "https://dummy:21", true)] + [InlineData(true, "https://dummy123", "https://dummy123", true)] + [InlineData(false, "https://dummy:21/path", "https://dummy:21", true)] + [InlineData(false, "https://dummy123", "https://dummy123", true)] + [InlineData(true, "https://dummy:21/path", "https://dummy:21", false)] + [InlineData(true, "https://dummy123", "https://dummy123", false)] + [InlineData(false, "https://dummy:21/path", "https://dummy:21", false)] + [InlineData(false, "https://dummy123", "https://dummy123", false)] +#else [InlineData(true, "https://dummy:21/path", "https://dummy:21")] [InlineData(true, "https://dummy123", "https://dummy123")] [InlineData(false, "https://dummy:21/path", "https://dummy:21")] [InlineData(false, "https://dummy123", "https://dummy123")] - [Theory] - public async Task SelectPipelineByAuthority_EnsureResiliencePipelineProviderCall(bool standardResilience, string url, string expectedPipelineKey) +#endif + public async Task SelectPipelineByAuthority_EnsureResiliencePipelineProviderCall( + bool standardResilience, string url, string expectedPipelineKey, bool asynchronous = true) { var provider = new Mock>(MockBehavior.Strict); @@ -120,7 +132,8 @@ public async Task SelectPipelineByAuthority_EnsureResiliencePipelineProviderCall .Setup(p => p.GetPipeline(new HttpKey(pipelineName, expectedPipelineKey))) .Returns(ResiliencePipeline.Empty); - await CreateClient().GetAsync(url); + var client = CreateClient(); + await SendRequest(client, url, asynchronous); provider.VerifyAll(); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs index 11c9dd4c340..5ceb74f2217 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Resilience.cs @@ -54,8 +54,13 @@ public void AddResilienceHandler_EnsureCorrectServicesRegistered() Assert.Contains(services, s => s.ServiceType == typeof(ResiliencePipelineProvider)); } - [Fact] - public async Task AddResilienceHandler_OnPipelineDisposed_EnsureCalled() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task AddResilienceHandler_OnPipelineDisposed_EnsureCalled(bool asynchronous = true) { var onPipelineDisposedCalled = false; var services = new ServiceCollection(); @@ -72,9 +77,7 @@ public async Task AddResilienceHandler_OnPipelineDisposed_EnsureCalled() using (var serviceProvider = services.BuildServiceProvider()) { var client = serviceProvider.GetRequiredService().CreateClient("client"); - using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); - - await client.GetStringAsync("https://dummy"); + await SendRequest(client, "https://dummy", asynchronous); } onPipelineDisposedCalled.Should().BeTrue(); @@ -103,8 +106,13 @@ public void AddResilienceHandler_EnsureServicesNotAddedTwice() builder.Services.Should().HaveCount(count + 2); } - [Fact] - public async Task AddResilienceHandler_EnsureErrorType() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task AddResilienceHandler_EnsureErrorType(bool asynchronous = true) { using var metricCollector = new MetricCollector(null, "Polly", "resilience.polly.strategy.events"); var enricher = new TestMetricsEnricher(); @@ -122,15 +130,19 @@ public async Task AddResilienceHandler_EnsureErrorType() clientBuilder.Services.Configure(o => o.MeteringEnrichers.Add(enricher)); var client = clientBuilder.Services.BuildServiceProvider().GetRequiredService().CreateClient("client"); - using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); - using var response = await client.SendAsync(request); + using var response = await SendRequest(client, "https://dummy", asynchronous); enricher.Tags["error.type"].Should().BeOfType().Subject.Should().Be("500"); } - [Fact] - public async Task AddResilienceHandler_EnsureResilienceHandlerContext() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task AddResilienceHandler_EnsureResilienceHandlerContext(bool asynchronous = true) { var verified = false; _builder @@ -145,7 +157,9 @@ public async Task AddResilienceHandler_EnsureResilienceHandlerContext() _builder.AddHttpMessageHandler(() => new TestHandlerStub(HttpStatusCode.InternalServerError)); - await CreateClient(BuilderName).GetAsync("https://dummy"); + var client = CreateClient(BuilderName); + await SendRequest(client, "https://dummy", asynchronous); + verified.Should().BeTrue(); } @@ -175,10 +189,14 @@ public enum PolicyType CircuitBreaker, } + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else [InlineData(true)] [InlineData(false)] - [Theory] - public async Task AddResilienceHandler_EnsureProperPipelineInstanceRetrieved(bool bySelector) +#endif + public async Task AddResilienceHandler_EnsureProperPipelineInstanceRetrieved(bool bySelector, bool asynchronous = true) { // arrange var resilienceProvider = new Mock>(MockBehavior.Strict); @@ -211,14 +229,19 @@ public async Task AddResilienceHandler_EnsureProperPipelineInstanceRetrieved(boo var client = provider.GetRequiredService().CreateClient("client"); // act - await client.GetAsync("https://dummy1"); + await SendRequest(client, "https://dummy1", asynchronous); // assert resilienceProvider.VerifyAll(); } - [Fact] - public async Task AddResilienceHandlerBySelector_EnsureResiliencePipelineProviderCalled() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task AddResilienceHandlerBySelector_EnsureResiliencePipelineProviderCalled(bool asynchronous = true) { // arrange var services = new ServiceCollection().AddLogging().AddMetrics(); @@ -242,7 +265,7 @@ public async Task AddResilienceHandlerBySelector_EnsureResiliencePipelineProvide var pipelineProvider = provider.GetRequiredService>(); // act - await client.GetAsync("https://dummy1"); + await SendRequest(client, "https://dummy1", asynchronous); // assert providerMock.VerifyAll(); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs index 7f0143ded57..79ce7aa654d 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/HttpClientBuilderExtensionsTests.Standard.cs @@ -35,6 +35,24 @@ public HttpClientBuilderExtensionsTests() public void Dispose() => _serviceProvider?.Dispose(); + private static Task SendRequest(HttpClient client, string url, bool asynchronous) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + +#if NET6_0_OR_GREATER + if (asynchronous) + { + return client.SendAsync(request, default); + } + else + { + return Task.FromResult(client.Send(request, default)); + } +#else + return client.SendAsync(request, default); +#endif + } + private HttpClient CreateClient(string name = BuilderName) { _serviceProvider ??= _builder.Services.BuildServiceProvider(); @@ -197,8 +215,13 @@ public void AddStandardResilienceHandler_EnsureConfigured(MethodArgs mode) Assert.NotNull(pipeline); } - [Fact] - public async Task DynamicReloads_Ok() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task DynamicReloads_Ok(bool asynchronous = true) { // arrange var requests = new List(); @@ -224,13 +247,13 @@ public async Task DynamicReloads_Ok() var client = CreateClient(); // act && assert - await client.GetAsync("https://dummy"); + await SendRequest(client, "https://dummy", asynchronous); requests.Should().HaveCount(7); requests.Clear(); reloadAction(new() { { "standard:Retry:MaxRetryAttempts", "10" } }); - await client.GetAsync("https://dummy"); + await SendRequest(client, "https://dummy", asynchronous); requests.Should().HaveCount(11); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs index 45840010e87..8a1831d67b8 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/ResilienceHandlerTest.cs @@ -17,10 +17,14 @@ namespace Microsoft.Extensions.Http.Resilience.Test.Internals; public class ResilienceHandlerTest { + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else [InlineData(true)] [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureRequestMetadataFlows(bool resilienceContextSet) +#endif + public async Task Send_EnsureRequestMetadataFlows(bool resilienceContextSet, bool asynchronous = true) { using var handler = new ResilienceHandler(ResiliencePipeline.Empty); using var invoker = new HttpMessageInvoker(handler); @@ -35,7 +39,7 @@ public async Task SendAsync_EnsureRequestMetadataFlows(bool resilienceContextSet handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); - await invoker.SendAsync(request, default); + await InvokeHandler(invoker, request, asynchronous); if (resilienceContextSet) { @@ -51,10 +55,14 @@ public async Task SendAsync_EnsureRequestMetadataFlows(bool resilienceContextSet } } + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else [InlineData(true)] [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) +#endif + public async Task Send_EnsureExecutionContext(bool executionContextSet, bool asynchronous = true) { using var handler = new ResilienceHandler(_ => ResiliencePipeline.Empty); using var invoker = new HttpMessageInvoker(handler); @@ -67,7 +75,7 @@ public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) handler.InnerHandler = new TestHandlerStub(HttpStatusCode.OK); - await invoker.SendAsync(request, default); + await InvokeHandler(invoker, request, asynchronous); if (executionContextSet) { @@ -79,10 +87,14 @@ public async Task SendAsync_EnsureExecutionContext(bool executionContextSet) } } + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else [InlineData(true)] [InlineData(false)] - [Theory] - public async Task SendAsync_EnsureInvoker(bool executionContextSet) +#endif + public async Task Send_EnsureInvoker(bool executionContextSet, bool asynchronous = true) { using var handler = new ResilienceHandler(_ => ResiliencePipeline.Empty); using var invoker = new HttpMessageInvoker(handler); @@ -101,13 +113,18 @@ public async Task SendAsync_EnsureInvoker(bool executionContextSet) return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); }); - var response = await invoker.SendAsync(request, default); + var response = await InvokeHandler(invoker, request, asynchronous); Assert.Equal(HttpStatusCode.Created, response.StatusCode); } - [Fact] - public async Task SendAsync_EnsureCancellationTokenFlowsToResilienceContext() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_EnsureCancellationTokenFlowsToResilienceContext(bool asynchronous = true) { using var source = new CancellationTokenSource(); using var handler = new ResilienceHandler(_ => ResiliencePipeline.Empty); @@ -121,13 +138,18 @@ public async Task SendAsync_EnsureCancellationTokenFlowsToResilienceContext() return Task.FromResult(new HttpResponseMessage { StatusCode = HttpStatusCode.Created }); }); - var response = await invoker.SendAsync(request, source.Token); + var response = await InvokeHandler(invoker, request, asynchronous, source.Token); Assert.Equal(HttpStatusCode.Created, response.StatusCode); } - [Fact] - public async Task SendAsync_Exception_EnsureRethrown() + [Theory] +#if NET6_0_OR_GREATER + [CombinatorialData] +#else + [InlineData(true)] +#endif + public async Task Send_Exception_EnsureRethrown(bool asynchronous = true) { using var handler = new ResilienceHandler(_ => ResiliencePipeline.Empty); using var invoker = new HttpMessageInvoker(handler); @@ -135,6 +157,26 @@ public async Task SendAsync_Exception_EnsureRethrown() handler.InnerHandler = new TestHandlerStub((_, _) => throw new InvalidOperationException()); - await invoker.Invoking(i => i.SendAsync(request, default)).Should().ThrowAsync(); + await Assert.ThrowsAsync(() => InvokeHandler(invoker, request, asynchronous)); + } + + private static Task InvokeHandler( + HttpMessageInvoker invoker, + HttpRequestMessage request, + bool asynchronous, + CancellationToken cancellationToken = default) + { +#if NET6_0_OR_GREATER + if (asynchronous) + { + return invoker.SendAsync(request, cancellationToken); + } + else + { + return Task.FromResult(invoker.Send(request, cancellationToken)); + } +#else + return invoker.SendAsync(request, cancellationToken); +#endif } }