From 50330cb5a4dd8fdd4baff5fe678d86372895971d Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Fri, 30 Jun 2023 08:03:39 +0200 Subject: [PATCH] Cleanup Microsoft.Extensions.Http.Resilience internals (#4141) --- .../EmptyHandler.cs | 16 ++++ .../HttpClientFactory.cs | 10 +- .../HttpResilienceBenchmark.cs | 8 ++ .../MeteringUtil.cs | 2 +- ...ensions.Resilience.PerformanceTests.csproj | 4 - .../ResilienceEnrichmentBenchmark.cs | 1 - .../HttpClientBuilderExtensions.Hedging.cs | 11 ++- .../RequestMessageSnapshotStrategy.cs | 9 +- ...HedgingResilienceOptionsCustomValidator.cs | 12 --- .../Internal/IHttpRequestMessageSnapshot.cs | 19 ---- .../Internal/IRandomizer.cs | 24 ----- .../Internal/IRequestCloner.cs | 19 ---- .../Internal/NamedOptionsCache.cs | 28 ++++++ .../Internal/Randomizer.cs | 7 +- .../Internal/RequestCloner.cs | 94 ------------------ .../Internal/RequestMessageSnapshot.cs | 96 +++++++++++++++++++ .../Internal/ResilienceKeys.cs | 5 +- ...icrosoft.Extensions.Http.Resilience.csproj | 5 - .../HttpClientBuilderExtensions.Resilience.cs | 11 +-- .../ByAuthorityStrategyKeyProvider.cs | 2 +- .../ByCustomSelectorStrategyKeyProvider.cs | 19 ---- ...gyKeyProvider.cs => StrategyKeyOptions.cs} | 10 +- .../Internal/StrategyKeyProviderHelper.cs | 16 ++-- .../Routing/IRequestRoutingStrategyFactory.cs | 22 ----- .../OrderedGroupsRoutingStrategy.cs | 18 ++-- .../OrderedGroupsRoutingStrategyFactory.cs | 30 +++++- .../Routing/Internal/RequestRoutingOptions.cs | 13 +++ .../RequestRoutingStrategy.cs} | 19 +++- .../Internal/RequestRoutingStrategyFactory.cs | 47 --------- .../RequestRoutingStrategyOptionsValidator.cs | 12 +++ .../Routing/Internal/RoutingHelper.cs | 2 +- .../Internal/RoutingResilienceStrategy.cs | 17 +--- .../Internal/RoutingStrategyBuilder.cs | 16 +++- .../WeightedGroupsRoutingStrategy.cs | 21 ++-- .../WeightedGroupsRoutingStrategyFactory.cs | 30 +++++- .../RoutingStrategyBuilderExtensions.cs | 61 ++++++------ .../Hedging/HedgingTests.cs | 61 ++---------- ...ngResilienceOptionsCustomValidatorTests.cs | 27 ++---- .../Hedging/StandardHedgingTests.cs | 10 +- .../RequestMessageSnapshotStrategyTests.cs | 14 +-- ...ft.Extensions.Http.Resilience.Tests.csproj | 4 - ...ClientBuilderExtensionsTests.BySelector.cs | 12 +-- ...ClientBuilderExtensionsTests.Resilience.cs | 5 +- ...ests.cs => RequestMessageSnapshotTests.cs} | 21 ++-- .../Routing/MockRoutingStrategy.cs | 17 +++- .../Routing/OrderedRoutingStrategyTest.cs | 4 +- .../Routing/RoutingHelperTest.cs | 4 +- .../Routing/RoutingResilienceStrategyTests.cs | 2 +- .../Routing/RoutingStrategyTest.cs | 27 +++--- .../Routing/WeightedRoutingStrategyTest.cs | 4 +- ...ilienceServiceCollectionExtensionsTests.cs | 52 ++++------ 51 files changed, 450 insertions(+), 550 deletions(-) create mode 100644 bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/EmptyHandler.cs rename {test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience => bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests}/MeteringUtil.cs (94%) delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/NamedOptionsCache.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs rename src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/{IStrategyKeyProvider.cs => StrategyKeyOptions.cs} (51%) delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingOptions.cs rename src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/{IRequestRoutingStrategy.cs => Internal/RequestRoutingStrategy.cs} (53%) delete mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyOptionsValidator.cs rename test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/{DefaultRequestClonerTests.cs => RequestMessageSnapshotTests.cs} (85%) diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/EmptyHandler.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/EmptyHandler.cs new file mode 100644 index 00000000000..08a53910c2b --- /dev/null +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/EmptyHandler.cs @@ -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.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Http.Resilience.Bench; + +internal sealed class EmptyHandler : DelegatingHandler +{ + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs index 769b4e1b12b..d2ba34ac68f 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpClientFactory.cs @@ -29,8 +29,8 @@ public enum HedgingClientType internal static class HttpClientFactory { internal const string EmptyClient = "Empty"; - internal const string StandardClient = "Standard"; + internal const string SingleHandlerClient = "SingleHandler"; private const string HedgingEndpoint1 = "http://localhost1"; private const string HedgingEndpoint2 = "http://localhost2"; @@ -47,10 +47,14 @@ public static ServiceProvider InitializeServiceProvider(HedgingClientType client .AddStandardResilienceHandler() .Services .AddHttpClient(StandardClient) - .AddHttpMessageHandler() + .ConfigurePrimaryHttpMessageHandler(() => new NoRemoteCallHandler()) .Services .AddHttpClient(EmptyClient, client => client.Timeout = Timeout.InfiniteTimeSpan) - .AddHttpMessageHandler(); + .ConfigurePrimaryHttpMessageHandler(() => new NoRemoteCallHandler()) + .Services + .AddHttpClient(SingleHandlerClient, client => client.Timeout = Timeout.InfiniteTimeSpan) + .AddHttpMessageHandler(() => new EmptyHandler()) + .ConfigurePrimaryHttpMessageHandler(() => new NoRemoteCallHandler()); services.RemoveAll(); services.AddSingleton(NullLoggerFactory.Instance); diff --git a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpResilienceBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpResilienceBenchmark.cs index 6f719b3cc8e..787a3790457 100644 --- a/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpResilienceBenchmark.cs +++ b/bench/Libraries/Microsoft.Extensions.Http.Resilience.PerformanceTests/HttpResilienceBenchmark.cs @@ -16,6 +16,7 @@ public class HttpResilienceBenchmark private HttpClient _client = null!; private HttpClient _standardClient = null!; + private HttpClient _singleHandlerClient = null!; private HttpClient _hedgingClient = null!; private static HttpRequestMessage Request @@ -35,6 +36,7 @@ public void GlobalSetup() var factory = serviceProvider.GetRequiredService(); _client = factory.CreateClient(HttpClientFactory.EmptyClient); _standardClient = factory.CreateClient(HttpClientFactory.StandardClient); + _singleHandlerClient = factory.CreateClient(HttpClientFactory.SingleHandlerClient); _hedgingClient = factory.CreateClient(nameof(HedgingClientType.Ordered)); } @@ -55,4 +57,10 @@ public Task StandardHedgingHandler() { return _hedgingClient.SendAsync(Request, CancellationToken.None); } + + [Benchmark] + public Task SingleHandler() + { + return _singleHandlerClient.SendAsync(Request, CancellationToken.None); + } } diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/MeteringUtil.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/MeteringUtil.cs similarity index 94% rename from test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/MeteringUtil.cs rename to bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/MeteringUtil.cs index 6b52c0b1d0d..6897eb82fd2 100644 --- a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/MeteringUtil.cs +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/MeteringUtil.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; -namespace Microsoft.Extensions.Resilience.Test.Resilience; +namespace Microsoft.Extensions.Resilience.Bench; internal class MeteringUtil { diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj index 37a81e26436..a6f614b1cb8 100644 --- a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/Microsoft.Extensions.Resilience.PerformanceTests.csproj @@ -3,10 +3,6 @@ Microsoft.Extensions.Resilience Benchmarks for Microsoft.Extensions.Resilience. - - - - diff --git a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/ResilienceEnrichmentBenchmark.cs b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/ResilienceEnrichmentBenchmark.cs index ef1eeb761de..85a0a6cc6ff 100644 --- a/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/ResilienceEnrichmentBenchmark.cs +++ b/bench/Libraries/Microsoft.Extensions.Resilience.PerformanceTests/ResilienceEnrichmentBenchmark.cs @@ -7,7 +7,6 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.ExceptionSummarization; -using Microsoft.Extensions.Resilience.Test.Resilience; using Polly; using Polly.Registry; using Polly.Telemetry; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs index 0da3f8cab12..c4022245db5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/HttpClientBuilderExtensions.Hedging.cs @@ -73,7 +73,9 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt var optionsName = builder.Name; var routingBuilder = new RoutingStrategyBuilder(builder.Name, builder.Services); - builder.Services.TryAddSingleton(); + + builder.Services.TryAddSingleton(); + _ = builder.Services.AddValidatedOptions(optionsName); _ = builder.Services.AddValidatedOptions(optionsName); _ = builder.Services.PostConfigure(optionsName, options => @@ -96,7 +98,7 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt return null; } - var requestMessage = snapshot.Create().ReplaceHost(route); + var requestMessage = snapshot.CreateRequestMessage().ReplaceHost(route); // replace the request message args.ActionContext.Properties.Set(ResilienceKeys.RequestMessage, requestMessage); @@ -110,10 +112,11 @@ public static IStandardHedgingHandlerBuilder AddStandardHedgingHandler(this IHtt { var options = context.GetOptions(optionsName); context.EnableReloads(optionsName); + var routingOptions = context.GetOptions(routingBuilder.Name); _ = builder - .AddStrategy(new RoutingResilienceStrategy(context.ServiceProvider.GetRoutingFactory(routingBuilder.Name))) - .AddStrategy(new RequestMessageSnapshotStrategy(context.ServiceProvider.GetRequiredService())) + .AddStrategy(new RoutingResilienceStrategy(routingOptions.RoutingStrategyProvider!)) + .AddStrategy(new RequestMessageSnapshotStrategy()) .AddTimeout(options.TotalRequestTimeoutOptions) .AddHedging(options.HedgingOptions); }); diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs index c4c99e95cbf..62b99bce49d 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Hedging/Internals/RequestMessageSnapshotStrategy.cs @@ -15,13 +15,6 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// internal sealed class RequestMessageSnapshotStrategy : ResilienceStrategy { - private readonly IRequestCloner _requestCloner; - - public RequestMessageSnapshotStrategy(IRequestCloner requestCloner) - { - _requestCloner = requestCloner; - } - protected override async ValueTask> ExecuteCoreAsync( Func>> callback, ResilienceContext context, @@ -32,7 +25,7 @@ protected override async ValueTask> ExecuteCoreAsync { private const int CircuitBreakerTimeoutMultiplier = 2; - private readonly INamedServiceProvider _namedServiceProvider; - - public HttpStandardHedgingResilienceOptionsCustomValidator(INamedServiceProvider namedServiceProvider) - { - _namedServiceProvider = namedServiceProvider; - } public ValidateOptionsResult Validate(string? name, HttpStandardHedgingResilienceOptions options) { var builder = new ValidateOptionsResultBuilder(); - if (_namedServiceProvider.GetService(name!) is null) - { - builder.AddError($"The hedging routing is not configured for '{name}' HTTP client."); - } - if (options.EndpointOptions.TimeoutOptions.Timeout > options.TotalRequestTimeoutOptions.Timeout) { builder.AddError($"Total request timeout strategy must have a greater timeout than the attempt timeout strategy. " + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs deleted file mode 100644 index c7a401a8fba..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IHttpRequestMessageSnapshot.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// The snapshot of created by . -/// -internal interface IHttpRequestMessageSnapshot : IDisposable -{ - /// - /// Creates a new instance of from the snapshot. - /// - /// A instance. - HttpRequestMessage Create(); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs deleted file mode 100644 index 392b56cf1fa..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRandomizer.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Providers thread safe random support for this package. -/// -internal interface IRandomizer -{ - /// - /// Gets the next random double. - /// - /// The max value. - /// The next double. - double NextDouble(double maxValue); - - /// - /// Gets the next random int. - /// - /// The max value. - /// The next int. - int NextInt(int maxValue); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs deleted file mode 100644 index b3aca149237..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/IRequestCloner.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Http; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Internal interface for cloning an instance. -/// -internal interface IRequestCloner -{ - /// - /// Creates a snapshot of that can be then used for cloning. - /// - /// The request message. - /// The snapshot instance. - IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/NamedOptionsCache.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/NamedOptionsCache.cs new file mode 100644 index 00000000000..d9dfbf61310 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/NamedOptionsCache.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +/// +/// The cache for named options that allows accessing the last valid options instance. +/// +/// The type of options. +internal sealed class NamedOptionsCache +{ + public NamedOptionsCache(string optionsName, IOptionsMonitor optionsMonitor) + { + Options = optionsMonitor.Get(optionsName); + + _ = optionsMonitor.OnChange((options, name) => + { + if (name == optionsName) + { + Options = options; + } + }); + } + + public TOptions Options { get; private set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs index d5ad29836f0..89f248d835e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/Randomizer.cs @@ -7,13 +7,12 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; #pragma warning disable CA5394 // Do not use insecure randomness -#pragma warning disable CPR138 // Random class instances should be shared as statics. -internal sealed class Randomizer : IRandomizer +internal class Randomizer { private static readonly ThreadLocal _randomInstance = new(() => new Random()); - public double NextDouble(double maxValue) => _randomInstance.Value!.NextDouble() * maxValue; + public virtual double NextDouble(double maxValue) => _randomInstance.Value!.NextDouble() * maxValue; - public int NextInt(int maxValue) => _randomInstance.Value!.Next(maxValue); + public virtual int NextInt(int maxValue) => _randomInstance.Value!.Next(maxValue); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs deleted file mode 100644 index 1c297b96edc..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestCloner.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Net.Http; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Shared.Diagnostics; -using Microsoft.Shared.Pools; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -/// -/// Default implementation of interface for cloning requests. -/// -/// -/// The request content is only copied, not deeply cloned. -/// If the request is cloned outside the middlewares, the content must be cloned as well. -/// -internal sealed class RequestCloner : IRequestCloner -{ - public IHttpRequestMessageSnapshot CreateSnapshot(HttpRequestMessage request) => new Snapshot(request); - - private sealed class Snapshot : IHttpRequestMessageSnapshot - { - private static readonly ObjectPool>>> _headersPool = PoolFactory.CreateListPool>>(); - private static readonly ObjectPool>> _propertiesPool = PoolFactory.CreateListPool>(); - - private readonly HttpMethod _method; - private readonly Uri? _requestUri; - private readonly Version _version; - private readonly HttpContent? _content; - private readonly List>> _headers; - private readonly List> _properties; - - public Snapshot(HttpRequestMessage request) - { - if (request.Content is StreamContent) - { - Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned using the {nameof(RequestCloner)}."); - } - - _method = request.Method; - _version = request.Version; - _requestUri = request.RequestUri; - _content = request.Content; - - // headers - _headers = _headersPool.Get(); - _headers.AddRange(request.Headers); - - // props - _properties = _propertiesPool.Get(); -#if NET5_0_OR_GREATER - _properties.AddRange(request.Options); -#else - _properties.AddRange(request.Properties); -#endif - } - - public HttpRequestMessage Create() - { - var clone = new HttpRequestMessage(_method, _requestUri) - { - Content = _content, - Version = _version - }; - -#if NET5_0_OR_GREATER - foreach (var prop in _properties) - { - _ = clone.Options.TryAdd(prop.Key, prop.Value); - } -#else - foreach (var prop in _properties) - { - clone.Properties.Add(prop); - } -#endif - foreach (KeyValuePair> header in _headers) - { - _ = clone.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - - return clone; - } - - public void Dispose() - { - _propertiesPool.Return(_properties); - _headersPool.Return(_headers); - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs new file mode 100644 index 00000000000..7f6412bd15c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/RequestMessageSnapshot.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Shared.Diagnostics; +using Microsoft.Shared.Pools; + +namespace Microsoft.Extensions.Http.Resilience.Internal; + +internal sealed class RequestMessageSnapshot : IResettable, IDisposable +{ + private static readonly ObjectPool _snapshots = PoolFactory.CreateResettingPool(); + + private readonly List>> _headers = new(); + private readonly List> _properties = new(); + + private HttpMethod? _method; + private Uri? _requestUri; + private Version? _version; + private HttpContent? _content; + + public static RequestMessageSnapshot Create(HttpRequestMessage request) + { + var snapshot = _snapshots.Get(); + snapshot.Initialize(request); + return snapshot; + } + + public HttpRequestMessage CreateRequestMessage() + { + var clone = new HttpRequestMessage(_method!, _requestUri) + { + Content = _content, + Version = _version! + }; + +#if NET5_0_OR_GREATER + foreach (var prop in _properties) + { + _ = clone.Options.TryAdd(prop.Key, prop.Value); + } +#else + foreach (var prop in _properties) + { + clone.Properties.Add(prop); + } +#endif + foreach (KeyValuePair> header in _headers) + { + _ = clone.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return clone; + } + + bool IResettable.TryReset() + { + _properties.Clear(); + _headers.Clear(); + + _method = null; + _version = null; + _requestUri = null; + _content = null; + + return true; + } + + void IDisposable.Dispose() => _snapshots.Return(this); + + private void Initialize(HttpRequestMessage request) + { + if (request.Content is StreamContent) + { + Throw.InvalidOperationException($"{nameof(StreamContent)} content cannot by cloned."); + } + + _method = request.Method; + _version = request.Version; + _requestUri = request.RequestUri; + _content = request.Content; + + // headers + _headers.AddRange(request.Headers); + + // props +#if NET5_0_OR_GREATER + _properties.AddRange(request.Options); +#else + _properties.AddRange(request.Properties); +#endif + } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs index 636ef482b8e..d96e1a39ca0 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Internal/ResilienceKeys.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Telemetry; using Polly; @@ -11,9 +12,9 @@ internal static class ResilienceKeys { public static readonly ResiliencePropertyKey RequestMessage = new("Resilience.Http.RequestMessage"); - public static readonly ResiliencePropertyKey RoutingStrategy = new("Resilience.Http.RequestRoutingStrategy"); + public static readonly ResiliencePropertyKey RoutingStrategy = new("Resilience.Http.RequestRoutingStrategy"); - public static readonly ResiliencePropertyKey RequestSnapshot = new("Resilience.Http.Snapshot"); + public static readonly ResiliencePropertyKey RequestSnapshot = new("Resilience.Http.Snapshot"); public static readonly ResiliencePropertyKey RequestMetadata = new(TelemetryConstants.RequestMetadataKey); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj index 96d95609e35..8d7046a0e7c 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Microsoft.Extensions.Http.Resilience.csproj @@ -27,12 +27,7 @@ - - - - - diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs index a119b667fa2..9d71af7b17e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/HttpClientBuilderExtensions.Resilience.cs @@ -91,23 +91,20 @@ private static Func> return request => { - var key = strategyKeyProvider.GetStrategyKey(request); + var key = strategyKeyProvider(request); return resilienceProvider.GetStrategy(new HttpKey(strategyName, key)); }; } } - private static void TouchStrategyKey(IStrategyKeyProvider provider) + private static void TouchStrategyKey(Func provider) { // this piece of code eagerly checks that the strategy key provider is correctly configured // combined with HttpClient auto-activation we can detect any issues on startup - if (provider is ByAuthorityStrategyKeyProvider) - { #pragma warning disable S1075 // URIs should not be hardcoded - this URL is not used for any real request, nor in any telemetry - using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:123"); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:123"); #pragma warning restore S1075 // URIs should not be hardcoded - _ = provider.GetStrategyKey(request); - } + _ = provider(request); } private static IHttpResilienceStrategyBuilder AddHttpResilienceStrategy( diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs index 4ddf7af5d90..5b9e59c5c7f 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByAuthorityStrategyKeyProvider.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; -internal sealed class ByAuthorityStrategyKeyProvider : IStrategyKeyProvider +internal sealed class ByAuthorityStrategyKeyProvider { private readonly Redactor _redactor; private readonly ConcurrentDictionary<(string scheme, string host, int port), string> _cache = new(); diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs deleted file mode 100644 index be2dea427f9..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/ByCustomSelectorStrategyKeyProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Net.Http; - -namespace Microsoft.Extensions.Http.Resilience.Internal; - -internal sealed class ByCustomSelectorStrategyKeyProvider : IStrategyKeyProvider -{ - private readonly Func _selector; - - public ByCustomSelectorStrategyKeyProvider(Func selector) - { - _selector = selector; - } - - public string GetStrategyKey(HttpRequestMessage requestMessage) => _selector(requestMessage); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyOptions.cs similarity index 51% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyOptions.cs index fce2abc4f7a..cc10294aaa6 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/IStrategyKeyProvider.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyOptions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Net.Http; namespace Microsoft.Extensions.Http.Resilience.Internal; @@ -8,12 +9,7 @@ namespace Microsoft.Extensions.Http.Resilience.Internal; /// /// The provider that returns the strategy key from the request message. /// -internal interface IStrategyKeyProvider +internal sealed class StrategyKeyOptions { - /// - /// Returns the strategy key from the request message. - /// - /// The request message. - /// The strategy key. - string GetStrategyKey(HttpRequestMessage requestMessage); + public Func? KeyProvider { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs index 0ed8b754186..bcf8e58e0f7 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Resilience/Internal/StrategyKeyProviderHelper.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Compliance.Classification; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Http.Resilience.Internal; @@ -17,25 +18,22 @@ public static void SelectStrategyByAuthority(IServiceCollection services, string { var redactor = serviceProvider.GetRequiredService().GetRedactor(classification); - return new ByAuthorityStrategyKeyProvider(redactor); + return new ByAuthorityStrategyKeyProvider(redactor).GetStrategyKey; }); } public static void SelectStrategyBy(IServiceCollection services, string strategyName, Func> selectorFactory) { - UseStrategyKeyProvider(services, strategyName, serviceProvider => - { - return new ByCustomSelectorStrategyKeyProvider(selectorFactory(serviceProvider)); - }); + UseStrategyKeyProvider(services, strategyName, serviceProvider => selectorFactory(serviceProvider)); } - public static IStrategyKeyProvider? GetStrategyKeyProvider(this IServiceProvider provider, string strategyName) + public static Func? GetStrategyKeyProvider(this IServiceProvider provider, string strategyName) { - return provider.GetService>()?.GetService(strategyName); + return provider.GetRequiredService>().Get(strategyName).KeyProvider; } - private static void UseStrategyKeyProvider(IServiceCollection services, string strategyName, Func factory) + private static void UseStrategyKeyProvider(IServiceCollection services, string strategyName, Func> factory) { - _ = services.AddNamedSingleton(strategyName, factory); + _ = services.AddOptions(strategyName).Configure((options, provider) => options.KeyProvider = factory(provider)); } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs deleted file mode 100644 index 21951240a07..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategyFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Http.Resilience; - -/// -/// Defines a factory for creation of request routing strategies. -/// -internal interface IRequestRoutingStrategyFactory -{ - /// - /// Creates a new instance of . - /// - /// The RequestRoutingStrategy for providing the routes. - IRequestRoutingStrategy CreateRoutingStrategy(); - - /// - /// Returns the strategy instance to the pool. - /// - /// The strategy instance. - void ReturnRoutingStrategy(IRequestRoutingStrategy strategy); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategy.cs index 309662b9bc1..b0f5e503ba5 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategy.cs @@ -10,16 +10,16 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.OrderedGroups; -internal sealed class OrderedGroupsRoutingStrategy : IRequestRoutingStrategy, IResettable +internal sealed class OrderedGroupsRoutingStrategy : RequestRoutingStrategy, IResettable { - private readonly IRandomizer _randomizer; - + private readonly ObjectPool _pool; private int _lastUsedIndex; private IList? _groups; - public OrderedGroupsRoutingStrategy(IRandomizer randomizer) + public OrderedGroupsRoutingStrategy(Randomizer randomizer, ObjectPool pool) + : base(randomizer) { - _randomizer = randomizer; + _pool = pool; } public void Initialize(IList groups) @@ -29,7 +29,7 @@ public void Initialize(IList groups) _groups = groups; } - public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) + public override bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) { if (_groups == null) { @@ -38,7 +38,7 @@ public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) if (TryGetNextGroup(out var group)) { - nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, _randomizer!).Uri!; + nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, Randomizer!).Uri!; return true; } @@ -46,7 +46,9 @@ public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) return false; } - public bool TryReset() + public override void Dispose() => _pool.Return(this); + + public override bool TryReset() { _groups = null; _lastUsedIndex = 0; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs index ac19625a694..1dc12762f2d 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/OrderedGroups/OrderedGroupsRoutingStrategyFactory.cs @@ -1,17 +1,37 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; +using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.OrderedGroups; -internal sealed class OrderedGroupsRoutingStrategyFactory : RequestRoutingStrategyFactory +internal sealed class OrderedGroupsRoutingStrategyFactory : IPooledObjectPolicy { - public OrderedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) - : base(clientId, pool, optionsMonitor) + private readonly Randomizer _randomizer; + private readonly NamedOptionsCache _cache; + private readonly ObjectPool _pool; + + public OrderedGroupsRoutingStrategyFactory(Randomizer randomizer, NamedOptionsCache cache) + { + _randomizer = randomizer; + _cache = cache; +#pragma warning disable S3366 // "this" should not be exposed from constructors + _pool = PoolFactory.CreatePool(this); +#pragma warning restore S3366 // "this" should not be exposed from constructors + } + + public OrderedGroupsRoutingStrategy Get() { + var strategy = _pool.Get(); + strategy.Initialize(_cache.Options.Groups); + return strategy; } - protected override void Initialize(OrderedGroupsRoutingStrategy strategy, OrderedGroupsRoutingOptions options) => strategy.Initialize(options.Groups); + public void Return(OrderedGroupsRoutingStrategy strategy) => _pool.Return(strategy); + + OrderedGroupsRoutingStrategy IPooledObjectPolicy.Create() => new(_randomizer, _pool); + + bool IPooledObjectPolicy.Return(OrderedGroupsRoutingStrategy obj) => obj.TryReset(); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingOptions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingOptions.cs new file mode 100644 index 00000000000..9e9531403c8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingOptions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; + +internal class RequestRoutingOptions +{ + [Required] + public Func? RoutingStrategyProvider { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategy.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs rename to src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategy.cs index e086c6c5416..2136784bbb2 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/IRequestRoutingStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategy.cs @@ -3,19 +3,32 @@ using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.ObjectPool; -namespace Microsoft.Extensions.Http.Resilience; +namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; /// /// Defines a strategy for retrieval of route URLs, /// used to route one request across a set of different endpoints. /// -internal interface IRequestRoutingStrategy +internal abstract class RequestRoutingStrategy : IResettable, IDisposable { + protected RequestRoutingStrategy(Randomizer randomizer) + { + Randomizer = randomizer; + } + + public Randomizer Randomizer { get; } + /// /// Gets the next route Uri. /// /// Holds next route value, or . /// if next route available, otherwise. - bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute); + public abstract bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute); + + public abstract bool TryReset(); + + public abstract void Dispose(); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs deleted file mode 100644 index 634d06ee69f..00000000000 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyFactory.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; - -internal abstract class RequestRoutingStrategyFactory : IRequestRoutingStrategyFactory - where T : class, IRequestRoutingStrategy, IResettable -{ - private readonly ObjectPool _pool; - private TOptions _options; - - protected RequestRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) - { - _pool = pool; - _options = optionsMonitor.Get(clientId); - - _ = optionsMonitor.OnChange((options, name) => - { - if (name == clientId) - { - _options = options; - } - }); - } - - public IRequestRoutingStrategy CreateRoutingStrategy() - { - var strategy = _pool.Get(); - - Initialize(strategy, _options); - - return strategy; - } - - public void ReturnRoutingStrategy(IRequestRoutingStrategy strategy) - { - _ = Throw.IfNull(strategy); - - _pool.Return((T)strategy); - } - - protected abstract void Initialize(T strategy, TOptions options); -} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyOptionsValidator.cs new file mode 100644 index 00000000000..3e39a7669bf --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RequestRoutingStrategyOptionsValidator.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; + +[OptionsValidator] +internal partial class RequestRoutingStrategyOptionsValidator : IValidateOptions +{ +} + diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingHelper.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingHelper.cs index fe786adeec1..9ca3097596e 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingHelper.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingHelper.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; internal static class RoutingHelper { - public static T SelectByWeight(this IList endpoints, Func weightProvider, IRandomizer randomizer) + public static T SelectByWeight(this IList endpoints, Func weightProvider, Randomizer randomizer) { var accumulatedProbability = 0d; var weightSum = 0d; diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs index 0a27be086f7..59744c5ac66 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/RoutingResilienceStrategy.cs @@ -14,11 +14,11 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal; /// internal sealed class RoutingResilienceStrategy : ResilienceStrategy { - private readonly IRequestRoutingStrategyFactory _factory; + private readonly Func _provider; - public RoutingResilienceStrategy(IRequestRoutingStrategyFactory factory) + public RoutingResilienceStrategy(Func provider) { - _factory = factory; + _provider = provider; } protected override async ValueTask> ExecuteCoreAsync( @@ -31,7 +31,7 @@ protected override async ValueTask> ExecuteCoreAsync> ExecuteCoreAsync(name); + } + + public string Name { get; } + + public IServiceCollection Services { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategy.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategy.cs index fea671c2e55..4b767bc1c49 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategy.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategy.cs @@ -10,18 +10,19 @@ namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.WeightedGroups; -internal sealed class WeightedGroupsRoutingStrategy : IRequestRoutingStrategy, IResettable +internal sealed class WeightedGroupsRoutingStrategy : RequestRoutingStrategy { - private readonly IRandomizer _randomizer; private readonly List _groups; + private readonly ObjectPool _pool; private bool _initialGroupPicked; private WeightedGroupSelectionMode _mode; private bool _initialized; - public WeightedGroupsRoutingStrategy(IRandomizer randomizer) + public WeightedGroupsRoutingStrategy(Randomizer randomizer, ObjectPool pool) + : base(randomizer) { - _randomizer = randomizer; _groups = new List(); + _pool = pool; } public void Initialize(IEnumerable groups, WeightedGroupSelectionMode mode) @@ -33,7 +34,9 @@ public void Initialize(IEnumerable groups, WeightedGroupS _groups.AddRange(groups); } - public bool TryReset() + public override void Dispose() => _pool.Return(this); + + public override bool TryReset() { _initialized = false; _mode = WeightedGroupSelectionMode.EveryAttempt; @@ -42,7 +45,7 @@ public bool TryReset() return true; } - public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) + public override bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) { if (!_initialized) { @@ -51,7 +54,7 @@ public bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) if (TryGetNextGroup(out var group)) { - nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, _randomizer).Uri!; + nextRoute = group!.Endpoints.SelectByWeight(e => e.Weight, Randomizer).Uri!; return true; } @@ -77,7 +80,7 @@ private WeightedEndpointGroup PickGroup() if (!_initialGroupPicked) { _initialGroupPicked = true; - return _groups.SelectByWeight(g => g.Weight, _randomizer); + return _groups.SelectByWeight(g => g.Weight, Randomizer); } if (_mode == WeightedGroupSelectionMode.InitialAttempt) @@ -86,7 +89,7 @@ private WeightedEndpointGroup PickGroup() } else { - return _groups.SelectByWeight(g => g.Weight, _randomizer); + return _groups.SelectByWeight(g => g.Weight, Randomizer); } } } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs index 00fd018cad7..d5147ae119b 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/Internal/WeightedGroups/WeightedGroupsRoutingStrategyFactory.cs @@ -1,17 +1,37 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; +using Microsoft.Shared.Pools; namespace Microsoft.Extensions.Http.Resilience.Routing.Internal.WeightedGroups; -internal sealed class WeightedGroupsRoutingStrategyFactory : RequestRoutingStrategyFactory +internal sealed class WeightedGroupsRoutingStrategyFactory : IPooledObjectPolicy { - public WeightedGroupsRoutingStrategyFactory(string clientId, ObjectPool pool, IOptionsMonitor optionsMonitor) - : base(clientId, pool, optionsMonitor) + private readonly Randomizer _randomizer; + private readonly NamedOptionsCache _cache; + private readonly ObjectPool _pool; + + public WeightedGroupsRoutingStrategyFactory(Randomizer randomizer, NamedOptionsCache cache) + { + _randomizer = randomizer; + _cache = cache; +#pragma warning disable S3366 // "this" should not be exposed from constructors + _pool = PoolFactory.CreatePool(this); +#pragma warning restore S3366 // "this" should not be exposed from constructors + } + + public WeightedGroupsRoutingStrategy Get() { + var strategy = _pool.Get(); + strategy.Initialize(_cache.Options.Groups, _cache.Options.SelectionMode); + return strategy; } - protected override void Initialize(WeightedGroupsRoutingStrategy strategy, WeightedGroupsRoutingOptions options) => strategy.Initialize(options.Groups, options.SelectionMode); + public void Return(WeightedGroupsRoutingStrategy strategy) => _pool.Return(strategy); + + WeightedGroupsRoutingStrategy IPooledObjectPolicy.Create() => new(_randomizer, _pool); + + bool IPooledObjectPolicy.Return(WeightedGroupsRoutingStrategy obj) => obj.TryReset(); } diff --git a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/RoutingStrategyBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/RoutingStrategyBuilderExtensions.cs index b61d352be8f..3af3821ca72 100644 --- a/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/RoutingStrategyBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Http.Resilience/Routing/RoutingStrategyBuilderExtensions.cs @@ -5,12 +5,11 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.Http.Resilience.Internal.Routing; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Resilience.Routing.Internal.OrderedGroups; using Microsoft.Extensions.Http.Resilience.Routing.Internal.WeightedGroups; -using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Options.Validation; using Microsoft.Shared.Diagnostics; @@ -38,9 +37,8 @@ public static IRoutingStrategyBuilder ConfigureOrderedGroups(this IRoutingStrate _ = Throw.IfNull(builder); _ = Throw.IfNull(section); - _ = builder.Services.AddPooled(); - - return builder.ConfigureRoutingStrategy(options => options.Bind(section)); + _ = builder.ConfigureOrderedGroupsCore().Bind(section); + return builder; } /// @@ -73,9 +71,9 @@ public static IRoutingStrategyBuilder ConfigureOrderedGroups(this IRoutingStrate _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - _ = builder.Services.AddPooled(); + _ = builder.ConfigureOrderedGroupsCore().Configure(configure); - return builder.ConfigureRoutingStrategy(options => options.Configure(configure)); + return builder; } /// @@ -92,9 +90,9 @@ public static IRoutingStrategyBuilder ConfigureWeightedGroups(this IRoutingStrat _ = Throw.IfNull(builder); _ = Throw.IfNull(section); - _ = builder.Services.AddPooled(); + _ = builder.ConfigureWeightedGroupsCore().Bind(section); - return builder.ConfigureRoutingStrategy(options => options.Bind(section)); + return builder; } /// @@ -127,40 +125,41 @@ public static IRoutingStrategyBuilder ConfigureWeightedGroups(this IRoutingStrat _ = Throw.IfNull(builder); _ = Throw.IfNull(configure); - _ = builder.Services.AddPooled(); + _ = builder.ConfigureWeightedGroupsCore().Configure(configure); - return builder.ConfigureRoutingStrategy(options => options.Configure(configure)); + return builder; } - internal static IRequestRoutingStrategyFactory GetRoutingFactory(this IServiceProvider serviceProvider, string routingName) + internal static IRoutingStrategyBuilder ConfigureRoutingStrategy(this IRoutingStrategyBuilder builder, Func> factory) { - return serviceProvider.GetRequiredService>().GetRequiredService(routingName); + _ = builder.Services + .AddOptions(builder.Name) + .Configure((options, provider) => options.RoutingStrategyProvider = factory(provider)); + + return builder; } - internal static IRoutingStrategyBuilder ConfigureRoutingStrategy( - this IRoutingStrategyBuilder builder, - Action> configure) - where TRoutingStrategyFactory : class, IRequestRoutingStrategyFactory - where TRoutingStrategyOptions : class - where TRoutingStrategyOptionsValidator : class, IValidateOptions + private static OptionsBuilder ConfigureOrderedGroupsCore(this IRoutingStrategyBuilder builder) { - builder.Services.TryAddSingleton(); - - var optionsBuilder = builder.Services.AddValidatedOptions(builder.Name); - configure(optionsBuilder); - - return builder.ConfigureRoutingStrategy(serviceProvider => + _ = builder.ConfigureRoutingStrategy(serviceProvider => { - return (IRequestRoutingStrategyFactory)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TRoutingStrategyFactory), builder.Name); + var optionsCache = new NamedOptionsCache(builder.Name, serviceProvider.GetRequiredService>()); + var factory = new OrderedGroupsRoutingStrategyFactory(serviceProvider.GetRequiredService(), optionsCache); + return () => factory.Get(); }); + + return builder.Services.AddValidatedOptions(builder.Name); } - internal static IRoutingStrategyBuilder ConfigureRoutingStrategy(this IRoutingStrategyBuilder builder, Func factory) + private static OptionsBuilder ConfigureWeightedGroupsCore(this IRoutingStrategyBuilder builder) { - builder.Services.TryAddSingleton(); - - _ = builder.Services.AddNamedSingleton(builder.Name, factory); + _ = builder.ConfigureRoutingStrategy(serviceProvider => + { + var optionsCache = new NamedOptionsCache(builder.Name, serviceProvider.GetRequiredService>()); + var factory = new WeightedGroupsRoutingStrategyFactory(serviceProvider.GetRequiredService(), optionsCache); + return () => factory.Get(); + }); - return builder; + return builder.Services.AddValidatedOptions(builder.Name); } } 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 3a1c4f5a23c..af187dda595 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HedgingTests.cs @@ -12,8 +12,8 @@ using FluentAssertions; using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Telemetry.Metering; using Moq; @@ -32,54 +32,37 @@ public abstract class HedgingTests : IDisposable public const int DefaultHedgingAttempts = 3; private readonly CancellationTokenSource _cancellationTokenSource; - private readonly Mock _requestCloneHandlerMock; - private readonly Mock _requestRoutingStrategyMock; - private readonly Mock _requestRoutingStrategyFactoryMock; + private readonly Mock _requestRoutingStrategyMock; + private readonly Func _requestRoutingStrategyFactory; private readonly IServiceCollection _services; private readonly List _requests = new(); private readonly Queue _responses = new(); - private readonly Func _createDefaultBuilder; private bool _failure; - private protected HedgingTests(Func createDefaultBuilder) + private protected HedgingTests(Func, TBuilder> createDefaultBuilder) { _cancellationTokenSource = new CancellationTokenSource(); - _requestCloneHandlerMock = new Mock(MockBehavior.Strict); - _requestRoutingStrategyMock = new Mock(MockBehavior.Strict); - _requestRoutingStrategyFactoryMock = new Mock(MockBehavior.Strict); + _requestRoutingStrategyMock = new Mock(MockBehavior.Strict, new Randomizer()); + _requestRoutingStrategyFactory = () => _requestRoutingStrategyMock.Object; _services = new ServiceCollection().RegisterMetering().AddLogging(); - _services.AddSingleton(_requestCloneHandlerMock.Object); _services.AddSingleton(NullRedactorProvider.Instance); var httpClient = _services.AddHttpClient(ClientId); - Builder = createDefaultBuilder(httpClient, _requestRoutingStrategyFactoryMock.Object); + Builder = createDefaultBuilder(httpClient, _requestRoutingStrategyFactory); _ = httpClient.AddHttpMessageHandler(() => new TestHandlerStub(InnerHandlerFunction)); - _createDefaultBuilder = createDefaultBuilder; } public TBuilder Builder { get; private set; } public void Dispose() { - _requestCloneHandlerMock.VerifyAll(); _requestRoutingStrategyMock.VerifyAll(); - _requestRoutingStrategyFactoryMock.VerifyAll(); _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); } - [Fact] - public void AddHedging_EnsureRequestCloner() - { - var services = new ServiceCollection(); - - _createDefaultBuilder(services.AddHttpClient("dummy"), _requestRoutingStrategyFactoryMock.Object); - - Assert.NotNull(services.BuildServiceProvider().GetRequiredService()); - } - [Fact] public async Task SendAsync_EnsureContextFlows() { @@ -92,8 +75,6 @@ public async Task SendAsync_EnsureContextFlows() SetupRouting(); SetupRoutes(3, "https://enpoint-{0}:80"); - _services.RemoveAll(); - _services.TryAddSingleton(); ConfigureHedgingOptions(options => { options.OnHedging = args => @@ -122,7 +103,6 @@ public async Task SendAsync_NoErrors_ShouldReturnSingleResponse() SetupRoutes(1, "https://enpoint-{0}:80/"); using var client = CreateClientWithHandler(); using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - SetupCloner(request, false); AddResponse(HttpStatusCode.OK); @@ -138,7 +118,7 @@ public async Task SendAsync_NoRoutes_Throws() { using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - SetupRouting(false); + SetupRouting(); SetupRoutes(0); _failure = true; @@ -155,7 +135,6 @@ public async Task SendAsync_NoRoutesLeftAndNoResult_ShouldThrow() using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); SetupRouting(); - SetupCloner(request, true); SetupRoutes(2); _failure = true; @@ -175,7 +154,6 @@ public async Task SendAsync_NoRoutesLeftAndSomeResultPresent_ShouldReturn() using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); SetupRouting(); - SetupCloner(request, true); SetupRoutes(4); AddResponse(HttpStatusCode.ServiceUnavailable); @@ -195,7 +173,6 @@ public async Task SendAsync_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts() using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); SetupRouting(); - SetupCloner(request, true); SetupRoutes(2); AddResponse(HttpStatusCode.InternalServerError); @@ -207,8 +184,6 @@ public async Task SendAsync_NoRoutesLeft_EnsureLessThanMaxHedgedAttempts() var result = await client.SendAsync(request, _cancellationTokenSource.Token); Assert.Equal(2, _requests.Count); - - _requestCloneHandlerMock.Verify(o => o.CreateSnapshot(It.IsAny()), Times.Exactly(1)); } [Fact] @@ -217,7 +192,6 @@ public async Task SendAsync_FailedExecution_ShouldReturnResponseFromHedging() using var request = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); SetupRouting(); - SetupCloner(request, true); SetupRoutes(3, "https://enpoint-{0}:80"); AddResponse(HttpStatusCode.InternalServerError); @@ -252,17 +226,6 @@ protected void AddResponse(HttpStatusCode statusCode, int count) protected HttpClient CreateClientWithHandler() => _services.BuildServiceProvider().GetRequiredService().CreateClient(ClientId); - protected void SetupCloner(HttpRequestMessage request, bool createCalled) - { - var snapshot = createCalled ? - Mock.Of(v => v.Create() == request) : - Mock.Of(); - - _requestCloneHandlerMock - .Setup(mock => mock.CreateSnapshot(It.IsAny())) - .Returns(snapshot); - } - private Task InnerHandlerFunction(HttpRequestMessage request, CancellationToken cancellationToken) { _requests.Add(request.RequestUri!.ToString()); @@ -290,12 +253,8 @@ protected void SetupRoutes(int totalAttempts, string pattern = "https://dummy-{0 .Returns(() => attemptCount <= totalAttempts); } - protected void SetupRouting(bool mustReturn = true) + protected void SetupRouting() { - _requestRoutingStrategyFactoryMock.Setup(s => s.CreateRoutingStrategy()).Returns(() => _requestRoutingStrategyMock.Object); - if (mustReturn) - { - _requestRoutingStrategyFactoryMock.Setup(s => s.ReturnRoutingStrategy(_requestRoutingStrategyMock.Object)).Verifiable(); - } + _requestRoutingStrategyMock.Setup(s => s.Dispose()); } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs index c575ed64a45..2156818c330 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/HttpStandardHedgingResilienceOptionsCustomValidatorTests.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http.Resilience.Internal.Validators; -using Moq; using Xunit; namespace Microsoft.Extensions.Http.Resilience.Test.Hedging; @@ -21,7 +19,7 @@ public void Validate_InvalidOptions_EnsureValidationErrors() options.EndpointOptions.CircuitBreakerOptions.SamplingDuration = TimeSpan.FromSeconds(1); options.TotalRequestTimeoutOptions.Timeout = TimeSpan.FromSeconds(1); - var validationResult = CreateValidator("dummy").Validate("dummy", options); + var validationResult = CreateValidator().Validate("dummy", options); Assert.True(validationResult.Failed); @@ -37,22 +35,11 @@ public void Validate_ValidOptions_NoValidationErrors() { HttpStandardHedgingResilienceOptions options = new(); - var validationResult = CreateValidator("dummy").Validate("dummy", options); + var validationResult = CreateValidator().Validate("dummy", options); validationResult.Succeeded.Should().BeTrue(); } - [Fact] - public void Validate_ValidOptionsWithoutRouting_ValidationErrors() - { - HttpStandardHedgingResilienceOptions options = new(); - - var validationResult = CreateValidator("dummy").Validate("other", options); - - validationResult.Failed.Should().BeTrue(); - validationResult.FailureMessage.Should().Be("The hedging routing is not configured for 'other' HTTP client."); - } - public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data { get @@ -84,7 +71,7 @@ public static IEnumerable GetOptions_ValidOptions_EnsureNoErrors_Data [Theory] public void Validate_ValidOptions_EnsureNoErrors(HttpStandardHedgingResilienceOptions options) { - var validationResult = CreateValidator("dummy").Validate("dummy", options); + var validationResult = CreateValidator().Validate("dummy", options); Assert.False(validationResult.Failed); } @@ -117,15 +104,13 @@ public static IEnumerable GetOptions_InvalidOptions_EnsureErrors_Data [Theory] public void Validate_InvalidOptions_EnsureErrors(HttpStandardHedgingResilienceOptions options) { - var validationResult = CreateValidator("dummy").Validate("dummy", options); + var validationResult = CreateValidator().Validate("dummy", options); Assert.True(validationResult.Failed); } - private static HttpStandardHedgingResilienceOptionsCustomValidator CreateValidator(string name) + private static HttpStandardHedgingResilienceOptionsCustomValidator CreateValidator() { - var mock = Mock.Of>(v => v.GetService(name) == Mock.Of()); - - return new HttpStandardHedgingResilienceOptionsCustomValidator(mock); + return new HttpStandardHedgingResilienceOptionsCustomValidator(); } } 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 be2e59a9f29..0d70057e143 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Hedging/StandardHedgingTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Options; using Moq; @@ -29,7 +30,7 @@ public StandardHedgingTests() { } - private static IStandardHedgingHandlerBuilder ConfigureDefaultBuilder(IHttpClientBuilder builder, IRequestRoutingStrategyFactory factory) + private static IStandardHedgingHandlerBuilder ConfigureDefaultBuilder(IHttpClientBuilder builder, Func factory) { return builder .AddStandardHedgingHandler(routing => routing.ConfigureRoutingStrategy(_ => factory)) @@ -113,7 +114,9 @@ public void ActionGenerator_Ok() var args = new HedgingActionGeneratorArguments(primary, secondary, 0, _ => Outcome.FromResultAsTask(response)); generator.Invoking(g => g(args)).Should().Throw().WithMessage("Request message snapshot is not attached to the resilience context."); - primary.Properties.Set(ResilienceKeys.RequestSnapshot, Mock.Of()); + using var request = new HttpRequestMessage(); + using var snapshot = RequestMessageSnapshot.Create(request); + primary.Properties.Set(ResilienceKeys.RequestSnapshot, snapshot); generator.Invoking(g => g(args)).Should().Throw().WithMessage("Routing strategy is not attached to the resilience context."); } @@ -177,7 +180,6 @@ public async Task VerifyPipeline() SetupRouting(); SetupRoutes(1); - SetupCloner(request, false); AddResponse(HttpStatusCode.OK); using var client = CreateClientWithHandler(); @@ -249,7 +251,6 @@ 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"); - SetupCloner(firstRequest, true); await client.SendAsync(firstRequest); AssertNoResponse(); @@ -257,7 +258,6 @@ public async Task DynamicReloads_Ok() AddResponse(HttpStatusCode.InternalServerError, 7); using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "https://to-be-replaced:1234/some-path?query"); - SetupCloner(secondRequest, true); await client.SendAsync(secondRequest); AssertNoResponse(); } 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 2ae0d60a69f..f1ba316f6d6 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Internal/RequestMessageSnapshotStrategyTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Http.Resilience.Internal; -using Moq; using Polly; using Xunit; @@ -17,11 +16,7 @@ public class RequestMessageSnapshotStrategyTests [Fact] public async Task SendAsync_EnsureSnapshotAttached() { - var snapshot = new Mock(MockBehavior.Strict); - snapshot.Setup(s => s.Dispose()); - var cloner = new Mock(MockBehavior.Strict); - cloner.Setup(c => c.CreateSnapshot(It.IsAny())).Returns(snapshot.Object); - var strategy = new RequestMessageSnapshotStrategy(cloner.Object); + var strategy = new RequestMessageSnapshotStrategy(); var context = ResilienceContext.Get(); using var request = new HttpRequestMessage(); context.Properties.Set(ResilienceKeys.RequestMessage, request); @@ -29,19 +24,16 @@ public async Task SendAsync_EnsureSnapshotAttached() using var response = await strategy.ExecuteAsync( context => { - context.Properties.GetValue(ResilienceKeys.RequestSnapshot, null!).Should().Be(snapshot.Object); + context.Properties.GetValue(ResilienceKeys.RequestSnapshot, null!).Should().NotBeNull(); return new ValueTask(new HttpResponseMessage()); }, context); - - cloner.VerifyAll(); - snapshot.VerifyAll(); } [Fact] public void ExecuteAsync_requestMessageNotFound_Throws() { - var strategy = new RequestMessageSnapshotStrategy(Mock.Of()); + var strategy = new RequestMessageSnapshotStrategy(); strategy.Invoking(s => s.Execute(() => { })).Should().Throw(); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj index be5a929375c..1ada25e19a3 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Microsoft.Extensions.Http.Resilience.Tests.csproj @@ -9,10 +9,6 @@ true - - - - 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 4d7bf3eb12f..389fb8d7517 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 @@ -40,10 +40,10 @@ public void SelectStrategyByAuthority_Ok(bool standardResilience, string url, st using var request = new HttpRequestMessage(HttpMethod.Head, url); - var key = provider.GetStrategyKey(request); + var key = provider(request); Assert.Equal(expectedStrategyKey, key); - Assert.Same(provider.GetStrategyKey(request), provider.GetStrategyKey(request)); + Assert.Same(provider(request), provider(request)); } [Fact] @@ -55,7 +55,7 @@ public void SelectStrategyByAuthority_Ok_NullURL_Throws() using var request = new HttpRequestMessage(); - Assert.Throws(() => provider.GetStrategyKey(request)); + Assert.Throws(() => provider(request)); } [Fact] @@ -67,7 +67,7 @@ public void SelectStrategyByAuthority_ErasingRedactor_InvalidOperationException( using var request = new HttpRequestMessage(HttpMethod.Get, "https://dummy"); - Assert.Throws(() => provider.GetStrategyKey(request)); + Assert.Throws(() => provider(request)); } [InlineData(true, "https://dummy:21/path", "https://")] @@ -98,10 +98,10 @@ public void SelectStrategyBy_Ok(bool standardResilience, string url, string expe using var request = new HttpRequestMessage(HttpMethod.Head, url); - var key = provider.GetStrategyKey(request); + var key = provider(request); Assert.Equal(expectedStrategyKey, key); - Assert.NotSame(provider.GetStrategyKey(request), provider.GetStrategyKey(request)); + Assert.NotSame(provider(request), provider(request)); } [InlineData(true, "https://dummy:21/path", "https://dummy:21")] 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 2703a55497b..82885bf2989 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 @@ -11,13 +11,14 @@ using Microsoft.Extensions.Compliance.Redaction; using Microsoft.Extensions.Compliance.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Http.Resilience.Internal; using Microsoft.Extensions.Http.Resilience.Test.Helpers; using Microsoft.Extensions.Http.Telemetry; using Microsoft.Extensions.Options; using Microsoft.Extensions.Resilience; -using Microsoft.Extensions.Resilience.Test.Resilience; using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Extensions.Telemetry.Testing.Metering; using Moq; using Polly; using Polly.Extensions.Telemetry; @@ -78,7 +79,7 @@ public void AddResilienceHandler_EnsureServicesNotAddedTwice() [Fact] public async Task AddResilienceHandler_EnsureFailureResultContext() { - using var listener = MeteringUtil.ListenPollyMetrics(); + using var metricCollector = new MetricCollector(null, "Polly", "resilience-events"); var asserted = false; var services = new ServiceCollection() diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs similarity index 85% rename from test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs rename to test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs index 6cdfe0f88a0..4a5214f2a40 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/DefaultRequestClonerTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Resilience/RequestMessageSnapshotTests.cs @@ -14,15 +14,8 @@ namespace Microsoft.Extensions.Http.Resilience.Test.Resilience; #pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete -public class DefaultRequestClonerTests +public class RequestMessageSnapshotTests { - private readonly RequestCloner _cloneHandler; - - public DefaultRequestClonerTests() - { - _cloneHandler = new RequestCloner(); - } - [Fact] public void CreateSnapshot_StreamContent_ShouldThrow() { @@ -33,8 +26,8 @@ public void CreateSnapshot_StreamContent_ShouldThrow() Content = new StreamContent(new MemoryStream()) }; - var exception = Assert.Throws(() => _cloneHandler.CreateSnapshot(initialRequest)); - Assert.Equal("StreamContent content cannot by cloned using the RequestCloner.", exception.Message); + var exception = Assert.Throws(() => RequestMessageSnapshot.Create(initialRequest)); + Assert.Equal("StreamContent content cannot by cloned.", exception.Message); initialRequest.Dispose(); } @@ -42,8 +35,8 @@ public void CreateSnapshot_StreamContent_ShouldThrow() public void CreateSnapshot_CreatesClone() { using var request = CreateRequest(); - var snapshot = _cloneHandler.CreateSnapshot(request); - var cloned = snapshot.Create(); + using var snapshot = RequestMessageSnapshot.Create(request); + var cloned = snapshot.CreateRequestMessage(); AssertClonedMessage(request, cloned); } @@ -51,10 +44,10 @@ public void CreateSnapshot_CreatesClone() public void CreateSnapshot_OriginalMessageChanged_SnapshotReturnsOriginalData() { using var request = CreateRequest(); - var snapshot = _cloneHandler.CreateSnapshot(request); + using var snapshot = RequestMessageSnapshot.Create(request); request.Properties["some-new-prop"] = "ABC"; - var cloned = snapshot.Create(); + var cloned = snapshot.CreateRequestMessage(); cloned.Properties.Should().NotContainKey("some-new-prop"); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs index 40f207ea122..9fcd16f3c5c 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/MockRoutingStrategy.cs @@ -2,17 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; namespace Microsoft.Extensions.Http.Resilience.Test.Routing; // Can't use NotNullWhenAttribute since it's defined in two reference assemblies with InternalVisibleTo #pragma warning disable CS8767 -internal class MockRoutingStrategy : IRequestRoutingStrategy +internal class MockRoutingStrategy : RequestRoutingStrategy { private readonly IStubRoutingService _mockService; public MockRoutingStrategy(IStubRoutingService mockService, string name) + : base(new Randomizer()) { _mockService = mockService; Name = name; @@ -20,9 +24,18 @@ public MockRoutingStrategy(IStubRoutingService mockService, string name) public string Name { get; private set; } - public bool TryGetNextRoute(out Uri? nextRoute) + public override void Dispose() + { + } + + public override bool TryGetNextRoute([NotNullWhen(true)] out Uri? nextRoute) { nextRoute = _mockService.Route; return true; } + + public override bool TryReset() + { + return true; + } } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs index eefde7a8bdd..860f767fa43 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/OrderedRoutingStrategyTest.cs @@ -6,7 +6,9 @@ using System.Linq; using FluentAssertions; using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Resilience.Routing.Internal.OrderedGroups; +using Microsoft.Extensions.ObjectPool; using Moq; using Xunit; @@ -76,7 +78,7 @@ protected override IEnumerable ConfigureMinRoutes(IRoutingStrategyBuilde yield return "https://dummy-route/"; } - internal override IRequestRoutingStrategy CreateEmptyStrategy() => new OrderedGroupsRoutingStrategy(Mock.Of()); + internal override RequestRoutingStrategy CreateEmptyStrategy() => new OrderedGroupsRoutingStrategy(Mock.Of(), Mock.Of>()); protected override IEnumerable> ConfigureInvalidRoutes() { diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingHelperTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingHelperTest.cs index adc5935ba60..2b85ecb6bb9 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingHelperTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingHelperTest.cs @@ -17,7 +17,7 @@ public class RoutingHelperTest [Theory] public void SelectEndpoint_Ok(double nextResult, int expectedEndpoint) { - var randomizer = new Mock(MockBehavior.Strict); + var randomizer = new Mock(MockBehavior.Strict); randomizer.Setup(v => v.NextDouble(10)).Returns(nextResult); var result = new List { 1, 2, 3, 4 }.SelectByWeight(v => v, randomizer.Object); @@ -28,7 +28,7 @@ public void SelectEndpoint_Ok(double nextResult, int expectedEndpoint) [Fact] public void SelectEndpoint_Invalid() { - var randomizer = new Mock(MockBehavior.Strict); + var randomizer = new Mock(MockBehavior.Strict); randomizer.Setup(v => v.NextDouble(10)).Returns(10000); Assert.Throws(() => new List { 1, 2, 3, 4 }.SelectByWeight(v => v, randomizer.Object)); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs index d5ccfe22ffb..4e0312e8093 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingResilienceStrategyTests.cs @@ -14,7 +14,7 @@ public class RoutingResilienceStrategyTests [Fact] public void NoRequestMessage_Throws() { - RoutingResilienceStrategy strategy = new RoutingResilienceStrategy(Mock.Of()); + RoutingResilienceStrategy strategy = new RoutingResilienceStrategy(() => Mock.Of()); strategy.Invoking(s => s.Execute(() => { })).Should().Throw().WithMessage("The HTTP request message was not found in the resilience context."); } diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs index 168da3a211f..5baca9a1982 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/RoutingStrategyTest.cs @@ -29,7 +29,7 @@ protected RoutingStrategyTest() public IRoutingStrategyBuilder Builder { get; set; } - internal Mock Randomizer { get; } = new Mock(MockBehavior.Strict); + internal Mock Randomizer { get; } = new Mock(MockBehavior.Strict); public virtual bool CompareOrder => true; @@ -38,7 +38,7 @@ public void Validate_Ok() { Configure(Builder); - Assert.Throws(() => CreateStrategy("unknown")); + Assert.Throws(() => CreateStrategy("unknown")); } [Fact] @@ -49,13 +49,12 @@ public void CreateStrategy_EnsurePooled() Configure(Builder); var factory = CreateRoutingFactory(); - var strategies = new HashSet(); + var strategies = new HashSet(); for (int i = 0; i < 10; i++) { - var strategy = factory.CreateRoutingStrategy(); + using var strategy = factory(); strategies.Add(strategy); - factory.ReturnRoutingStrategy(strategy); } // assert that some strategies were pooled @@ -148,17 +147,23 @@ internal void StrategyResultHelper(params string[] expectedUrls) var factory = CreateRoutingFactory(); - CollectUrls(factory.CreateRoutingStrategy()).Should().Equal(expectedUrls); + CollectUrls(factory()).Should().Equal(expectedUrls); // intentionally, we check that the output on subsequent calls is the same - CollectUrls(factory.CreateRoutingStrategy()).Should().Equal(expectedUrls); + CollectUrls(factory()).Should().Equal(expectedUrls); } - internal IRequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name).CreateRoutingStrategy(); + internal RequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name)(); - internal IRequestRoutingStrategyFactory CreateRoutingFactory(string? name = null) => Builder.Services.BuildServiceProvider().GetRoutingFactory(name ?? Builder.Name); + internal Func CreateRoutingFactory(string? name = null) + { + return Builder.Services + .BuildServiceProvider() + .GetRequiredService>() + .Get(name ?? Builder.Name).RoutingStrategyProvider!; + } - private static IEnumerable CollectUrls(IRequestRoutingStrategy strategy) + private static IEnumerable CollectUrls(RequestRoutingStrategy strategy) { while (strategy.TryGetNextRoute(out var route)) { @@ -177,7 +182,7 @@ protected static IConfigurationSection GetSection(IDictionary va protected abstract IEnumerable> ConfigureInvalidRoutes(); - internal abstract IRequestRoutingStrategy CreateEmptyStrategy(); + internal abstract RequestRoutingStrategy CreateEmptyStrategy(); protected void SetupRandomizer(double result) => Randomizer.Setup(r => r.NextDouble(It.IsAny())).Returns(result); diff --git a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs index 4942f3e5977..9ea6cf2435c 100644 --- a/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs +++ b/test/Libraries/Microsoft.Extensions.Http.Resilience.Tests/Routing/WeightedRoutingStrategyTest.cs @@ -6,7 +6,9 @@ using System.Linq; using FluentAssertions; using Microsoft.Extensions.Http.Resilience.Internal; +using Microsoft.Extensions.Http.Resilience.Routing.Internal; using Microsoft.Extensions.Http.Resilience.Routing.Internal.WeightedGroups; +using Microsoft.Extensions.ObjectPool; using Moq; using Xunit; @@ -160,7 +162,7 @@ protected override IEnumerable> ConfigureInvalid }); } - internal override IRequestRoutingStrategy CreateEmptyStrategy() => new WeightedGroupsRoutingStrategy(Mock.Of()); + internal override RequestRoutingStrategy CreateEmptyStrategy() => new WeightedGroupsRoutingStrategy(Mock.Of(), Mock.Of>()); private static WeightedEndpointGroup CreateGroup(params string[] endpoints) { diff --git a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceServiceCollectionExtensionsTests.cs index 96f65710acf..2eb776ea60a 100644 --- a/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceServiceCollectionExtensionsTests.cs +++ b/test/Libraries/Microsoft.Extensions.Resilience.Tests/Resilience/ResilienceServiceCollectionExtensionsTests.cs @@ -3,17 +3,15 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Linq; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.ExceptionSummarization; using Microsoft.Extensions.Http.Telemetry; using Microsoft.Extensions.Resilience.Resilience; +using Microsoft.Extensions.Telemetry.Testing.Metering; using Moq; using Polly; -using Polly.Extensions.Telemetry; using Polly.Registry; using Polly.Telemetry; using Xunit; @@ -28,14 +26,12 @@ public class ResilienceServiceCollectionExtensionsTests : IDisposable private readonly Mock _summarizer = new(MockBehavior.Strict); private ResilienceStrategyTelemetry? _telemetry; - private MeterListener _listener; + private MetricCollector _metricCollector; private IServiceCollection _services; - private Dictionary? _reportedTags; public ResilienceServiceCollectionExtensionsTests() { - _listener = MeteringUtil.ListenPollyMetrics(); - + _metricCollector = new MetricCollector(null, "Polly", "resilience-events"); _services = new ServiceCollection() .AddResilienceEnrichment() .AddResilienceStrategy("dummy", builder => @@ -49,17 +45,11 @@ public ResilienceServiceCollectionExtensionsTests() }); _services.TryAddSingleton(_summarizer.Object); - - _services.PostConfigure(options => - { - options.Enrichers.Add(context => - { - _reportedTags = context.Tags.ToDictionary(t => t.Key, t => t.Value); - }); - }); } - public void Dispose() => _listener.Dispose(); + public void Dispose() => _metricCollector.Dispose(); + + private IReadOnlyDictionary Tags => _metricCollector.LastMeasurement!.Tags; [Fact] public void AddResilienceEnrichment_NoOutcome_EnsureDimensions() @@ -67,11 +57,11 @@ public void AddResilienceEnrichment_NoOutcome_EnsureDimensions() Build(); _telemetry!.Report("dummy-event", ResilienceContext.Get(), string.Empty); - _reportedTags!["failure-reason"].Should().BeNull(); - _reportedTags!["failure-source"].Should().BeNull(); - _reportedTags!["failure-summary"].Should().BeNull(); - _reportedTags!["dep-name"].Should().BeNull(); - _reportedTags!["req-name"].Should().BeNull(); + Tags["failure-reason"].Should().BeNull(); + Tags["failure-source"].Should().BeNull(); + Tags["failure-summary"].Should().BeNull(); + Tags["dep-name"].Should().BeNull(); + Tags["req-name"].Should().BeNull(); } [Fact] @@ -94,9 +84,9 @@ public void AddResilienceEnrichment_Exception_EnsureDimensions() "dummy-event", new OutcomeArguments(ResilienceContext.Get(), Outcome.FromException(new InvalidOperationException { Source = "my-source" }), string.Empty)); - _reportedTags!["failure-reason"].Should().Be("InvalidOperationException"); - _reportedTags!["failure-summary"].Should().Be("type:desc:details"); - _reportedTags!["failure-source"].Should().Be("my-source"); + Tags["failure-reason"].Should().Be("InvalidOperationException"); + Tags["failure-summary"].Should().Be("type:desc:details"); + Tags["failure-source"].Should().Be("my-source"); } [Fact] @@ -109,9 +99,9 @@ public void AddResilienceEnrichment_Outcome_EnsureDimensions() "dummy-event", new OutcomeArguments(ResilienceContext.Get(), Outcome.FromResult("string-result"), string.Empty)); - _reportedTags!["failure-source"].Should().Be("my-source"); - _reportedTags!["failure-reason"].Should().Be("my-reason"); - _reportedTags!["failure-summary"].Should().Be("string-result"); + Tags["failure-source"].Should().Be("my-source"); + Tags["failure-reason"].Should().Be("my-reason"); + Tags["failure-summary"].Should().Be("string-result"); } [Fact] @@ -123,8 +113,8 @@ public void AddResilienceEnrichment_RequestMetadata_EnsureDimensions() Build(); _telemetry!.Report("dummy-event", context, string.Empty); - _reportedTags!["dep-name"].Should().Be("my-dep"); - _reportedTags!["req-name"].Should().Be("my-req"); + Tags["dep-name"].Should().Be("my-dep"); + Tags["req-name"].Should().Be("my-req"); } [Fact] @@ -136,8 +126,8 @@ public void AddResilienceEnrichment_RequestMetadataFromOutgoingRequestContext_En Build(); _telemetry!.Report("dummy-event", ResilienceContext.Get(), string.Empty); - _reportedTags!["dep-name"].Should().Be("my-dep"); - _reportedTags!["req-name"].Should().Be("my-req"); + Tags["dep-name"].Should().Be("my-dep"); + Tags["req-name"].Should().Be("my-req"); } private void Build()