public async Task Factory_CleanupCycle_DisposesLiveHandler()
        {
            // Arrange
            var disposeHandler = new DisposeTrackingHandler();

            Options.Get("github").HttpMessageHandlerBuilderActions.Add(b =>
            {
                b.AdditionalHandlers.Add(disposeHandler);
            });

            var factory = new TestHttpClientFactory(Services, LoggerFactory, Options, EmptyFilters)
            {
                EnableExpiryTimer  = true,
                EnableCleanupTimer = true,
            };

            // Create a handler and move it to the expired state
            var client1 = factory.CreateClient("github");

            var kvp = Assert.Single(factory.ActiveEntryState);

            kvp.Value.Item1.SetResult(kvp.Key);
            await kvp.Value.Item2;

            // Our handler is now in the cleanup state.
            var cleanupEntry = Assert.Single(factory._expiredHandlers);

            Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started");

            // Nulling out the refernces to the internal state of the factory since they wouldn't exist in the non-test
            // scenario. We're holding on the client to prevent disposal - like a real use case.
            kvp = default;

            // Act - 1
            factory.CleanupTimer_Tick(null);

            // Assert
            Assert.Same(cleanupEntry, Assert.Single(factory._expiredHandlers));
            Assert.Equal(0, disposeHandler.DisposeCount);
            Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started");

            // We need to make sure that the outer handler actually gets GCed, so drop our references to it.
            // This is important because the factory relies on this possibility for correctness. We need to ensure that
            // the factory isn't keeping any references.
            client1 = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");

            // Act - 2
            factory.CleanupTimer_Tick(null);

            // Assert
            Assert.Empty(factory._expiredHandlers);
            Assert.Equal(1, disposeHandler.DisposeCount);
            Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
        }
        private async Task <ExpiredHandlerTrackingEntry> SimulateClientUse_Factory_CleanupCycle_DisposesLiveHandler(
            TestHttpClientFactory factory,
            DisposeTrackingHandler disposeHandler)
        {
            // Create a handler and move it to the expired state
            var client1 = factory.CreateClient("github");

            var kvp = Assert.Single(factory.ActiveEntryState);

            kvp.Value.Item1.SetResult(kvp.Key);
            await kvp.Value.Item2;

            // Our handler is now in the cleanup state.
            var cleanupEntry = Assert.Single(factory._expiredHandlers);

            Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started");

            // Nulling out the references to the internal state of the factory since they wouldn't exist in the non-test
            // scenario. We're holding on the client to prevent disposal - like a real use case.
            lock (this)
            {
                // Prevent reordering
                kvp = default;
            }

            // Let's verify the the ActiveHandlerTrackingEntry is gone. This would be prevent
            // the handler from being disposed if it was still rooted.
            Assert.Empty(factory.ActiveEntryState);

            // Act - 1 - Run a cleanup cycle, this will not dispose the handler, because the client is still live.
            factory.CleanupTimer_Tick();

            // Assert
            Assert.Same(cleanupEntry, Assert.Single(factory._expiredHandlers));
            Assert.Equal(0, disposeHandler.DisposeCount);
            Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started");

            // We need to make sure that the outer handler actually gets GCed, so drop our references to it.
            // This is important because the factory relies on this possibility for correctness. We need to ensure that
            // the factory isn't keeping any references.
            lock (this)
            {
                // Prevent reordering
                GC.KeepAlive(client1);
                client1 = null;
            }

            return(cleanupEntry);
        }
        public async Task Factory_CleanupCycle_DisposesLiveHandler()
        {
            // Arrange
            var disposeHandler = new DisposeTrackingHandler();

            Options.Get("github").HttpMessageHandlerBuilderActions.Add(b =>
            {
                b.AdditionalHandlers.Add(disposeHandler);
            });

            var factory = new TestHttpClientFactory(Services, ScopeFactory, LoggerFactory, Options, EmptyFilters)
            {
                EnableExpiryTimer  = true,
                EnableCleanupTimer = true,
            };

            var cleanupEntry = await SimulateClientUse(factory, disposeHandler);

            // Being pretty conservative here because we want this test to be reliable,
            // and it depends on the GC and timing.
            for (var i = 0; i < 3; i++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();

                if (cleanupEntry.CanDispose)
                {
                    break;
                }

                await Task.Delay(TimeSpan.FromSeconds(1));
            }

            Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");

            // Act - 2
            factory.CleanupTimer_Tick(null);

            // Assert
            Assert.Empty(factory._expiredHandlers);
            Assert.Equal(1, disposeHandler.DisposeCount);
            Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
        }