[release/8.5] Detect connection timeouts in Hedging (#5149)

This commit is contained in:
Jose Perez Rodriguez 2024-05-09 15:02:08 -07:00 коммит произвёл GitHub
Родитель a8a6e030f9 80d518bdfb
Коммит de1f3236c7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
11 изменённых файлов: 185 добавлений и 66 удалений

Просмотреть файл

@ -2,7 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
using Polly;
using Polly.CircuitBreaker;
@ -17,13 +20,26 @@ public static class HttpClientHedgingResiliencePredicates
/// <summary>
/// Determines whether an outcome should be treated by hedging as a transient failure.
/// </summary>
/// <param name="outcome">The outcome of the user-specified callback.</param>
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
public static bool IsTransient(Outcome<HttpResponseMessage> outcome) => outcome switch
{
{ Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true,
{ Exception: { } exception } when IsTransientHttpException(exception) => true,
_ => false,
};
public static bool IsTransient(Outcome<HttpResponseMessage> outcome)
=> outcome switch
{
{ Result: { } response } when HttpClientResiliencePredicates.IsTransientHttpFailure(response) => true,
{ Exception: { } exception } when IsTransientHttpException(exception) => true,
_ => false,
};
/// <summary>
/// Determines whether an <see cref="HttpResponseMessage"/> should be treated by hedging as a transient failure.
/// </summary>
/// <param name="outcome">The outcome of the user-specified callback.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> associated with the execution.</param>
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
public static bool IsTransient(Outcome<HttpResponseMessage> outcome, CancellationToken cancellationToken)
=> HttpClientResiliencePredicates.IsHttpConnectionTimeout(outcome, cancellationToken)
|| IsTransient(outcome);
/// <summary>
/// Determines whether an exception should be treated by hedging as a transient failure.
@ -35,8 +51,7 @@ public static class HttpClientHedgingResiliencePredicates
return exception switch
{
BrokenCircuitException => true,
_ when HttpClientResiliencePredicates.IsTransientHttpException(exception) => true,
_ => false,
_ => HttpClientResiliencePredicates.IsTransientHttpException(exception),
};
}
}

Просмотреть файл

@ -16,11 +16,11 @@ public class HttpHedgingStrategyOptions : HedgingStrategyOptions<HttpResponseMes
/// Initializes a new instance of the <see cref="HttpHedgingStrategyOptions"/> class.
/// </summary>
/// <remarks>
/// By default the options is set to handle only transient failures,
/// By default, the options is set to handle only transient failures,
/// i.e. timeouts, 5xx responses and <see cref="HttpRequestException"/> exceptions.
/// </remarks>
public HttpHedgingStrategyOptions()
{
ShouldHandle = args => new ValueTask<bool>(HttpClientHedgingResiliencePredicates.IsTransient(args.Outcome));
ShouldHandle = args => new ValueTask<bool>(HttpClientHedgingResiliencePredicates.IsTransient(args.Outcome, args.Context.CancellationToken));
}
}

Просмотреть файл

@ -2,8 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Threading;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
using Polly;
using Polly.Timeout;
@ -26,6 +29,17 @@ public static class HttpClientResiliencePredicates
_ => false
};
/// <summary>
/// Determines whether an <see cref="HttpResponseMessage"/> should be treated by resilience strategies as a transient failure.
/// </summary>
/// <param name="outcome">The outcome of the user-specified callback.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> associated with the execution.</param>
/// <returns><see langword="true"/> if outcome is transient, <see langword="false"/> if not.</returns>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Resilience, UrlFormat = DiagnosticIds.UrlFormat)]
public static bool IsTransient(Outcome<HttpResponseMessage> outcome, CancellationToken cancellationToken)
=> IsHttpConnectionTimeout(outcome, cancellationToken)
|| IsTransient(outcome);
/// <summary>
/// Determines whether an exception should be treated by resilience strategies as a transient failure.
/// </summary>
@ -33,10 +47,14 @@ public static class HttpClientResiliencePredicates
{
_ = Throw.IfNull(exception);
return exception is HttpRequestException ||
exception is TimeoutRejectedException;
return exception is HttpRequestException or TimeoutRejectedException;
}
internal static bool IsHttpConnectionTimeout(in Outcome<HttpResponseMessage> outcome, in CancellationToken cancellationToken)
=> !cancellationToken.IsCancellationRequested
&& outcome.Exception is OperationCanceledException { Source: "System.Private.CoreLib" }
&& outcome.Exception.InnerException is TimeoutException;
/// <summary>
/// Determines whether a response contains a transient failure.
/// </summary>
@ -52,7 +70,6 @@ public static class HttpClientResiliencePredicates
return statusCode >= InternalServerErrorCode ||
response.StatusCode == HttpStatusCode.RequestTimeout ||
statusCode == TooManyRequests;
}
private const int InternalServerErrorCode = (int)HttpStatusCode.InternalServerError;

Просмотреть файл

@ -26,7 +26,7 @@ public class HttpRetryStrategyOptions : RetryStrategyOptions<HttpResponseMessage
/// </remarks>
public HttpRetryStrategyOptions()
{
ShouldHandle = args => new ValueTask<bool>(HttpClientResiliencePredicates.IsTransient(args.Outcome));
ShouldHandle = args => new ValueTask<bool>(HttpClientResiliencePredicates.IsTransient(args.Outcome, args.Context.CancellationToken));
BackoffType = DelayBackoffType.Exponential;
ShouldRetryAfterHeader = true;
UseJitter = true;

Просмотреть файл

@ -35,6 +35,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
private readonly Func<RequestRoutingStrategy> _requestRoutingStrategyFactory;
private readonly IServiceCollection _services;
private readonly Queue<HttpResponseMessage> _responses = new();
private ServiceProvider? _serviceProvider;
private bool _failure;
private protected HedgingTests(Func<IHttpClientBuilder, Func<RequestRoutingStrategy>, TBuilder> createDefaultBuilder)
@ -63,6 +64,11 @@ public abstract class HedgingTests<TBuilder> : IDisposable
_requestRoutingStrategyMock.VerifyAll();
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_serviceProvider?.Dispose();
foreach (var response in _responses)
{
response.Dispose();
}
}
[Fact]
@ -93,7 +99,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
await client.SendAsync(request, _cancellationTokenSource.Token);
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
Assert.Equal(2, calls);
}
@ -108,7 +114,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
AddResponse(HttpStatusCode.OK);
var response = await client.SendAsync(request, _cancellationTokenSource.Token);
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
AssertNoResponse();
Assert.Single(Requests);
@ -164,7 +170,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
using var result = await client.SendAsync(request, _cancellationTokenSource.Token);
Assert.Equal(DefaultHedgingAttempts + 1, Requests.Count);
Assert.Equal(HttpStatusCode.ServiceUnavailable, result.StatusCode);
}
@ -183,7 +189,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
await client.SendAsync(request, _cancellationTokenSource.Token);
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
RequestContexts.Distinct().OfType<ResilienceContext>().Should().HaveCount(3);
}
@ -204,7 +210,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
await client.SendAsync(request, _cancellationTokenSource.Token);
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
RequestContexts.Distinct().OfType<ResilienceContext>().Should().HaveCount(3);
@ -226,7 +232,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
using var _ = await client.SendAsync(request, _cancellationTokenSource.Token);
Assert.Equal(2, Requests.Count);
}
@ -244,7 +250,7 @@ public abstract class HedgingTests<TBuilder> : IDisposable
using var client = CreateClientWithHandler();
var result = await client.SendAsync(request, _cancellationTokenSource.Token);
using var result = await client.SendAsync(request, _cancellationTokenSource.Token);
Assert.Equal(3, Requests.Count);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Equal("https://enpoint-1:80/some-path?query", Requests[0]);
@ -268,7 +274,12 @@ public abstract class HedgingTests<TBuilder> : IDisposable
protected abstract void ConfigureHedgingOptions(Action<HttpHedgingStrategyOptions> configure);
protected HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>().CreateClient(ClientId);
protected HttpClient CreateClientWithHandler()
{
_serviceProvider?.Dispose();
_serviceProvider = _services.BuildServiceProvider();
return _serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(ClientId);
}
private Task<HttpResponseMessage> InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken)
{

Просмотреть файл

@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
namespace Microsoft.Extensions.Http.Resilience.Test.Hedging;
internal sealed class OperationCanceledExceptionMock : OperationCanceledException
{
public OperationCanceledExceptionMock(Exception innerException)
: base(null, innerException)
{
}
public override string? Source { get => "System.Private.CoreLib"; set => base.Source = value; }
}

Просмотреть файл

@ -46,7 +46,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
{
Builder.Configure(options => options.Hedging.MaxHedgedAttempts = -1);
Assert.Throws<OptionsValidationException>(() => CreateClientWithHandler());
Assert.Throws<OptionsValidationException>(CreateClientWithHandler);
}
[Fact]
@ -54,7 +54,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
{
Builder.Configure(options => options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(1));
Assert.Throws<OptionsValidationException>(() => CreateClientWithHandler());
Assert.Throws<OptionsValidationException>(CreateClientWithHandler);
}
[Fact]
@ -62,7 +62,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
{
Builder.Configure(o => o.Hedging.MaxHedgedAttempts = 8);
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
using var serviceProvider = Builder.Services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
}
@ -76,7 +77,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
o.Hedging.MaxHedgedAttempts = 8;
});
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
using var serviceProvider = Builder.Services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
}
@ -97,7 +99,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
Builder.Configure(section);
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
using var serviceProvider = Builder.Services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
Assert.Equal(8, options.Hedging.MaxHedgedAttempts);
}
@ -105,7 +108,8 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
[Fact]
public void ActionGenerator_Ok()
{
var options = Builder.Services.BuildServiceProvider().GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
using var serviceProvider = Builder.Services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>().Get(Builder.Name);
var generator = options.Hedging.ActionGenerator;
var primary = ResilienceContextPool.Shared.Get();
var secondary = ResilienceContextPool.Shared.Get();
@ -133,9 +137,12 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
Builder.Configure(section);
Assert.Throws<InvalidOperationException>(() =>
Builder.Services.BuildServiceProvider()
.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>()
.Get(Builder.Name));
{
using var serviceProvider = Builder.Services.BuildServiceProvider();
return serviceProvider
.GetRequiredService<IOptionsMonitor<HttpStandardHedgingResilienceOptions>>()
.Get(Builder.Name);
});
}
#endif
@ -163,7 +170,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
[Fact]
public void VerifyPipeline()
{
var serviceProvider = Builder.Services.BuildServiceProvider();
using var serviceProvider = Builder.Services.BuildServiceProvider();
var pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<HttpKey>>();
// primary handler
@ -209,7 +216,7 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
using var request = new HttpRequestMessage(HttpMethod.Get, "https://key:80/discarded");
AddResponse(HttpStatusCode.OK);
var response = await client.SendAsync(request, CancellationToken.None);
using var response = await client.SendAsync(request, CancellationToken.None);
provider.VerifyAll();
}
@ -235,14 +242,14 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
// act && assert
AddResponse(HttpStatusCode.InternalServerError, 3);
using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query");
await client.SendAsync(firstRequest);
using var _ = await client.SendAsync(firstRequest);
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");
await client.SendAsync(secondRequest);
using var __ = await client.SendAsync(secondRequest);
AssertNoResponse();
}
@ -257,11 +264,70 @@ public sealed class StandardHedgingTests : HedgingTests<IStandardHedgingHandlerB
// act && assert
AddResponse(HttpStatusCode.InternalServerError, 3);
using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "https://some-endpoint:1234/some-path?query");
await client.SendAsync(firstRequest);
using var _ = await client.SendAsync(firstRequest);
AssertNoResponse();
Requests.Should().AllSatisfy(r => r.Should().Be("https://some-endpoint:1234/some-path?query"));
}
[Fact]
public async Task SendAsync_FailedConnect_ShouldReturnResponseFromHedging()
{
const string FailingEndpoint = "www.failing-host.com";
var services = new ServiceCollection();
_ = services
.AddHttpClient(ClientId)
.ConfigurePrimaryHttpMessageHandler(() => new MockHttpMessageHandler(FailingEndpoint))
.AddStandardHedgingHandler(routing =>
routing.ConfigureOrderedGroups(g =>
{
g.Groups.Add(new UriEndpointGroup
{
Endpoints = [new WeightedUriEndpoint { Uri = new Uri($"https://{FailingEndpoint}:3000") }]
});
g.Groups.Add(new UriEndpointGroup
{
Endpoints = [new WeightedUriEndpoint { Uri = new Uri("https://microsoft.com") }]
});
}))
.Configure(opt =>
{
opt.Hedging.MaxHedgedAttempts = 10;
opt.Hedging.Delay = TimeSpan.FromSeconds(11);
opt.Endpoint.CircuitBreaker.FailureRatio = 0.99;
opt.Endpoint.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(900);
opt.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(200);
opt.Endpoint.Timeout.Timeout = TimeSpan.FromSeconds(200);
});
await using var provider = services.BuildServiceProvider();
var clientFactory = provider.GetRequiredService<IHttpClientFactory>();
using var client = clientFactory.CreateClient(ClientId);
var ex = await Record.ExceptionAsync(async () =>
{
using var _ = await client.GetAsync($"https://{FailingEndpoint}:3000");
});
Assert.Null(ex);
}
protected override void ConfigureHedgingOptions(Action<HttpHedgingStrategyOptions> configure) => Builder.Configure(options => configure(options.Hedging));
private class MockHttpMessageHandler(string failingEndpoint) : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri?.Host == failingEndpoint)
{
await Task.Delay(100, cancellationToken);
throw new OperationCanceledExceptionMock(new TimeoutException());
}
await Task.Delay(1000, cancellationToken);
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
}

Просмотреть файл

@ -4,11 +4,6 @@
<Description>Unit tests for Microsoft.Extensions.Http.Resilience.</Description>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net462' ">
<!-- See https://github.com/dotnet/extensions/issues/4531 -->
<SkipTests>true</SkipTests>
</PropertyGroup>
<ItemGroup>
<None Update="configs\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Просмотреть файл

@ -6,8 +6,10 @@ using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Http.Resilience.Test.Hedging;
using Polly;
using Polly.Retry;
using Xunit;
@ -19,16 +21,15 @@ public class HttpRetryStrategyOptionsTests
#pragma warning disable S2330
public static readonly IEnumerable<object[]> HandledExceptionsClassified = new[]
{
new object[] { new InvalidCastException(), false },
new object[] { new HttpRequestException(), true }
new object[] { new InvalidCastException(), null!, false },
[new HttpRequestException(), null!, true],
[new OperationCanceledExceptionMock(new TimeoutException()), null!, true],
[new OperationCanceledExceptionMock(new TimeoutException()), default(CancellationToken), true],
[new OperationCanceledExceptionMock(new InvalidOperationException()), default(CancellationToken), false],
[new OperationCanceledExceptionMock(new TimeoutException()), new CancellationToken(canceled: true), false],
};
private readonly HttpRetryStrategyOptions _testClass;
public HttpRetryStrategyOptionsTests()
{
_testClass = new HttpRetryStrategyOptions();
}
private readonly HttpRetryStrategyOptions _testClass = new();
[Fact]
public void Ctor_Defaults()
@ -63,9 +64,10 @@ public class HttpRetryStrategyOptionsTests
[Theory]
[MemberData(nameof(HandledExceptionsClassified))]
public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, bool expectedToHandle)
public async Task ShouldHandleException_DefaultValue_ShouldClassify(Exception exception, CancellationToken? token, bool expectedToHandle)
{
var shouldHandle = await _testClass.ShouldHandle(CreateArgs(Outcome.FromException<HttpResponseMessage>(exception)));
var args = CreateArgs(Outcome.FromException<HttpResponseMessage>(exception), token ?? default);
var shouldHandle = await _testClass.ShouldHandle(args);
Assert.Equal(expectedToHandle, shouldHandle);
}
@ -86,9 +88,10 @@ public class HttpRetryStrategyOptionsTests
[Theory]
[MemberData(nameof(HandledExceptionsClassified))]
public async Task ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, bool expectedToHandle)
public async Task ShouldHandleException_DefaultInstance_ShouldClassify(Exception exception, CancellationToken? token, bool expectedToHandle)
{
var shouldHandle = await new HttpRetryStrategyOptions().ShouldHandle(CreateArgs(Outcome.FromException<HttpResponseMessage>(exception)));
var args = CreateArgs(Outcome.FromException<HttpResponseMessage>(exception), token ?? default);
var shouldHandle = await new HttpRetryStrategyOptions().ShouldHandle(args);
Assert.Equal(expectedToHandle, shouldHandle);
}
@ -96,7 +99,7 @@ public class HttpRetryStrategyOptionsTests
public async Task ShouldRetryAfterHeader_InvalidOutcomes_ShouldReturnNull()
{
var options = new HttpRetryStrategyOptions { ShouldRetryAfterHeader = true };
using var responseMessage = new HttpResponseMessage { };
using var responseMessage = new HttpResponseMessage();
Assert.NotNull(options.DelayGenerator);
@ -153,7 +156,8 @@ public class HttpRetryStrategyOptionsTests
Assert.Equal(shouldRetryAfterHeader, options.DelayGenerator != null);
}
private static RetryPredicateArguments<HttpResponseMessage> CreateArgs(Outcome<HttpResponseMessage> outcome)
=> new(ResilienceContextPool.Shared.Get(), outcome, 0);
private static RetryPredicateArguments<HttpResponseMessage> CreateArgs(
Outcome<HttpResponseMessage> outcome,
CancellationToken cancellationToken = default)
=> new(ResilienceContextPool.Shared.Get(cancellationToken), outcome, 0);
}

Просмотреть файл

@ -128,7 +128,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
AddStandardResilienceHandler(mode, builder, _invalidConfigurationSection, options => { });
Assert.Throws<InvalidOperationException>(() => HttpClientBuilderExtensionsTests.GetPipeline(builder.Services, $"test-standard"));
Assert.Throws<InvalidOperationException>(() => HttpClientBuilderExtensionsTests.GetPipeline(builder.Services, "test-standard"));
}
[Fact]
@ -175,7 +175,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
}
});
Assert.Throws<OptionsValidationException>(() => GetPipeline(builder.Services, $"test-standard"));
Assert.Throws<OptionsValidationException>(() => GetPipeline(builder.Services, "test-standard"));
}
[InlineData(MethodArgs.None)]
@ -193,7 +193,7 @@ public sealed partial class HttpClientBuilderExtensionsTests : IDisposable
AddStandardResilienceHandler(mode, builder, _validConfigurationSection, options => { });
var pipeline = GetPipeline(builder.Services, $"test-standard");
var pipeline = GetPipeline(builder.Services, "test-standard");
Assert.NotNull(pipeline);
}

Просмотреть файл

@ -9,12 +9,7 @@ namespace Microsoft.Extensions.Http.Resilience.Test.Resilience;
public class HttpStandardResilienceOptionsTests
{
private readonly HttpStandardResilienceOptions _options;
public HttpStandardResilienceOptionsTests()
{
_options = new HttpStandardResilienceOptions();
}
private readonly HttpStandardResilienceOptions _options = new();
[Fact]
public void Ctor_EnsureDefaults()