/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Components/Server/test/Circuits/CircuitRegistryTest.cs
708 строк
30 KB
Ilona Tomkowicz
Implement `Circuit.RequestCircuitPauseAsync` (#66265)
22 апр 2026, 17:37
Не верифицирован
22 апр 2026, 17:37
048709d
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.JSInterop; using Moq; namespace Microsoft.AspNetCore.Components.Server.Circuits; public class CircuitRegistryTest { [Fact] public void Register_AddsCircuit() { // Arrange var registry = CreateRegistry(); var circuitHost = TestCircuitHost.Create(); // Act registry.Register(circuitHost); // Assert var actual = Assert.Single(registry.ConnectedCircuits.Values); Assert.Same(circuitHost, actual); } [Fact] public async Task ConnectAsync_TransfersClientOnActiveCircuit() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = CreateRegistry(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.Register(circuitHost); var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = "new-id"; // Act var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); // Assert Assert.Same(circuitHost, result); Assert.Same(newClient, circuitHost.Client.Client); Assert.Same(newConnectionId, circuitHost.Client.ConnectionId); var actual = Assert.Single(registry.ConnectedCircuits.Values); Assert.Same(circuitHost, actual); } [Fact] public async Task ConnectAsync_MakesInactiveCircuitActive() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = CreateRegistry(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.RegisterDisconnectedCircuit(circuitHost); var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = "new-id"; // Act var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); // Assert Assert.Same(circuitHost, result); Assert.Same(newClient, circuitHost.Client.Client); Assert.Same(newConnectionId, circuitHost.Client.ConnectionId); var actual = Assert.Single(registry.ConnectedCircuits.Values); Assert.Same(circuitHost, actual); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _)); } [Fact] public async Task ConnectAsync_InvokesCircuitHandlers_WhenCircuitWasPreviouslyDisconnected() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = CreateRegistry(circuitIdFactory); var handler = new Mock<CircuitHandler> { CallBase = true }; var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { handler.Object }); registry.RegisterDisconnectedCircuit(circuitHost); var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = "new-id"; // Act var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); // Assert Assert.NotNull(result); handler.Verify(v => v.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Once()); handler.Verify(v => v.OnConnectionDownAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); } [Fact] public async Task ConnectAsync_InvokesCircuitHandlers_WhenCircuitWasConsideredConnected() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = CreateRegistry(circuitIdFactory); var handler = new Mock<CircuitHandler> { CallBase = true }; var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { handler.Object }); registry.Register(circuitHost); var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = "new-id"; // Act var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); // Assert Assert.NotNull(result); handler.Verify(v => v.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Once()); handler.Verify(v => v.OnConnectionDownAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Once()); handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); } [Fact] public async Task ConnectAsync_InvokesCircuitHandlers_DisposesCircuitOnFailure() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = CreateRegistry(circuitIdFactory); var handler = new Mock<CircuitHandler> { CallBase = true }; handler.Setup(h => h.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>())).Throws(new InvalidTimeZoneException()); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { handler.Object }); registry.Register(circuitHost); var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = "new-id"; // Act var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); // Assert Assert.Null(result); Assert.Null(circuitHost.Handle.CircuitHost); // Will be null if disposed. Assert.Empty(registry.ConnectedCircuits); Assert.Equal(0, registry.DisconnectedCircuits.Count); } [Fact] public async Task DisconnectAsync_DoesNothing_IfCircuitIsInactive() { // Arrange var registry = CreateRegistry(); var circuitHost = TestCircuitHost.Create(); registry.DisconnectedCircuits.Set(circuitHost.CircuitId.Secret, circuitHost, new MemoryCacheEntryOptions { Size = 1 }); // Act await registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId); // Assert Assert.Empty(registry.ConnectedCircuits.Values); Assert.True(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); } [Fact] public async Task DisconnectAsync_InvokesCircuitHandlers_WhenCircuitWasDisconnected() { // Arrange var registry = CreateRegistry(); var handler = new Mock<CircuitHandler> { CallBase = true }; var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object }); registry.Register(circuitHost); // Act await registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId); // Assert handler.Verify(v => v.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionDownAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Once()); handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); } [Fact] public async Task DisconnectAsync_DoesNotInvokeCircuitHandlers_WhenCircuitReconnected() { // Arrange var registry = CreateRegistry(); var handler = new Mock<CircuitHandler> { CallBase = true }; var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object }); registry.Register(circuitHost); // Act await registry.DisconnectAsync(circuitHost, "old-connection"); // Assert handler.Verify(v => v.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionDownAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); } [Fact] public async Task DisconnectAsync_DoesNotInvokeCircuitHandlers_WhenCircuitWasNotFound() { // Arrange var registry = CreateRegistry(); var handler = new Mock<CircuitHandler> { CallBase = true }; var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object }); // Act await registry.DisconnectAsync(circuitHost, "old-connection"); // Assert handler.Verify(v => v.OnCircuitOpenedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnConnectionDownAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never()); } [Fact] public async Task Connect_WhileDisconnectIsInProgress() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = new TestCircuitRegistry(circuitIdFactory); registry.BeforeDisconnect = new ManualResetEventSlim(); var tcs = new TaskCompletionSource(); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.Register(circuitHost); var client = Mock.Of<ISingleClientProxy>(); var newId = "new-connection"; // Act var disconnect = Task.Run(() => { var task = registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId); tcs.SetResult(); return task; }); var connect = Task.Run(async () => { registry.BeforeDisconnect.Set(); await tcs.Task; await registry.ConnectAsync(circuitHost.CircuitId, client, newId, default); }); registry.BeforeDisconnect.Set(); await Task.WhenAll(disconnect, connect); // Assert // We expect the disconnect to finish followed by a reconnect var actual = Assert.Single(registry.ConnectedCircuits.Values); Assert.Same(circuitHost, actual); Assert.Same(client, circuitHost.Client.Client); Assert.Equal(newId, circuitHost.Client.ConnectionId); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); } [Fact] public async Task Connect_WhilePersistingEvictedCircuit_IsInProgress() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var options = new CircuitOptions { DisconnectedCircuitMaxRetained = 0, // This will automatically trigger eviction. }; var persistenceCompletionSource = new TaskCompletionSource(); var circuitPersistenceProvider = new TestCircuitPersistenceProvider() { Persisting = persistenceCompletionSource.Task, }; var registry = new TestCircuitRegistry(circuitIdFactory, options, circuitPersistenceProvider); registry.BeforeDisconnect = new ManualResetEventSlim(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(sp => new ComponentStatePersistenceManager( NullLoggerFactory.Instance.CreateLogger<ComponentStatePersistenceManager>(), sp)); serviceCollection.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State); var serviceProvider = serviceCollection.BuildServiceProvider(); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), serviceProvider.CreateAsyncScope()); registry.Register(circuitHost); var client = Mock.Of<ISingleClientProxy>(); var newId = "new-connection"; // Act var disconnect = Task.Run(() => { var task = registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId); return task; }); var connect = Task.Run(async () => { var connectCore = registry.ConnectAsync(circuitHost.CircuitId, client, newId, default); await connectCore; }); registry.BeforeDisconnect.Set(); await Task.WhenAll(disconnect, connect); persistenceCompletionSource.SetResult(); circuitPersistenceProvider.AfterPersist.Wait(TimeSpan.FromSeconds(10)); // Assert // We expect the reconnect to fail since the circuit has already been evicted and persisted. Assert.Empty(registry.ConnectedCircuits.Values); Assert.True(circuitPersistenceProvider.PersistCalled); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); } [Fact] public async Task Disconnect_DoesNotPersistCircuits_WithPendingState() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var options = new CircuitOptions { DisconnectedCircuitMaxRetained = 0, // This will automatically trigger eviction. }; var circuitPersistenceProvider = new TestCircuitPersistenceProvider(); var registry = new TestCircuitRegistry(circuitIdFactory, options, circuitPersistenceProvider); registry.BeforeDisconnect = new ManualResetEventSlim(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(sp => new ComponentStatePersistenceManager( NullLoggerFactory.Instance.CreateLogger<ComponentStatePersistenceManager>(), sp)); serviceCollection.AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State); var serviceProvider = serviceCollection.BuildServiceProvider(); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), serviceProvider.CreateAsyncScope()); registry.Register(circuitHost); var state = new ResumedPersistedCircuitState { ApplicationState = new Dictionary<string, byte[]>(), RootComponentDescriptors = new Dictionary<int, WebRootComponentDescriptor>(), }; circuitHost.AttachPersistedState(state); var client = Mock.Of<ISingleClientProxy>(); var newId = "new-connection"; // Act var disconnect = Task.Run(() => { var task = registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId); return task; }); var connect = Task.Run(async () => { var connectCore = registry.ConnectAsync(circuitHost.CircuitId, client, newId, default); await connectCore; }); registry.BeforeDisconnect.Set(); await Task.WhenAll(disconnect, connect); circuitPersistenceProvider.AfterPersist.Wait(TimeSpan.FromSeconds(10)); // Assert // We expect the reconnect to fail since the circuit has already been evicted and persisted. Assert.Empty(registry.ConnectedCircuits.Values); Assert.False(circuitPersistenceProvider.PersistCalled); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); } [Fact] public async Task DisconnectWhenAConnectIsInProgress() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var registry = new TestCircuitRegistry(circuitIdFactory); registry.BeforeConnect = new ManualResetEventSlim(); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.Register(circuitHost); var client = Mock.Of<ISingleClientProxy>(); var oldId = circuitHost.Client.ConnectionId; var newId = "new-connection"; // Act var connect = Task.Run(() => registry.ConnectAsync(circuitHost.CircuitId, client, newId, default)); var disconnect = Task.Run(() => registry.DisconnectAsync(circuitHost, oldId)); registry.BeforeConnect.Set(); await Task.WhenAll(connect, disconnect); // Assert // We expect the disconnect to fail since the client identifier has changed. var actual = Assert.Single(registry.ConnectedCircuits.Values); Assert.Same(circuitHost, actual); Assert.Same(client, circuitHost.Client.Client); Assert.Equal(newId, circuitHost.Client.ConnectionId); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); } [Fact] public async Task DisconnectedCircuitIsRemovedAfterConfiguredTimeout() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var circuitOptions = new CircuitOptions { DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(3), }; var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions); var tcs = new TaskCompletionSource(); registry.OnAfterEntryEvicted = () => { tcs.TrySetResult(); }; var circuitHost = TestCircuitHost.Create(); registry.RegisterDisconnectedCircuit(circuitHost); // Act // Verify it's present in the dictionary. Assert.True(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out var _)); await Task.Run(() => tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10))); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out var _)); } [Fact] public async Task ReconnectBeforeTimeoutDoesNotGetEntryToBeEvicted() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var circuitOptions = new CircuitOptions { DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(8), }; var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions); var tcs = new TaskCompletionSource(); registry.OnAfterEntryEvicted = () => { tcs.TrySetResult(); }; var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.RegisterDisconnectedCircuit(circuitHost); await registry.ConnectAsync(circuitHost.CircuitId, Mock.Of<ISingleClientProxy>(), "new-connection", default); // Act await Task.Run(() => tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10))); // Verify it's still connected Assert.True(registry.ConnectedCircuits.TryGetValue(circuitHost.CircuitId, out var cacheValue)); Assert.Same(circuitHost, cacheValue); // Nothing should be disconnected. Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out var _)); } [Fact] public async Task PauseCircuitAsync_DoesNothing_IfCircuitIsDisconnected() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var (registry, persistenceProvider) = CreateRegistryWithProvider(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.RegisterDisconnectedCircuit(circuitHost); // Act await registry.PauseCircuitAsync(circuitHost, circuitHost.Client.ConnectionId); // Assert Assert.Empty(registry.ConnectedCircuits); Assert.True(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _)); Assert.False(persistenceProvider.PersistCalled); } [Fact] public async Task ConnectAsync_ReturnsNull_ForPausedCircuit() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var circuitOptions = new CircuitOptions { DisconnectedCircuitMaxRetained = 0 }; // Ensure eviction var persistenceProvider = new TestCircuitPersistenceProvider(); var registry = new TestCircuitRegistry(circuitIdFactory, circuitOptions, persistenceProvider) { BeforePause = new ManualResetEventSlim(), PauseInvoked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously), BeforeConnect = new ManualResetEventSlim(), }; var scope = new ServiceCollection() .AddSingleton(sp => new ComponentStatePersistenceManager(NullLoggerFactory.Instance.CreateLogger<ComponentStatePersistenceManager>(), sp)) .AddSingleton(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State) .BuildServiceProvider() .CreateAsyncScope(); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), scope); registry.Register(circuitHost); var originalConnectionId = circuitHost.Client.ConnectionId; var newClient = Mock.Of<ISingleClientProxy>(); var newConnectionId = originalConnectionId; // Act var pauseTask = Task.Run(() => { var pauseTask = registry.PauseCircuitAsync(circuitHost, originalConnectionId); return pauseTask; }); var connectTask = Task.Run(async () => { await registry.PauseInvoked.Task; // Wait for PauseCore to be entered and waiting on BeforePauseTcs // At this point, PauseCircuitAsync holds the main CircuitRegistryLock and its PauseCore is blocked. // ConnectAsync will block on the CircuitRegistryLock until PauseCircuitAsync releases it. var connectResultAttempt = registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default); return await connectResultAttempt; }); await registry.PauseInvoked.Task; registry.BeforeConnect.Set(); registry.BeforePause.Set(); await Task.WhenAll(pauseTask, connectTask); var connectResult = await connectTask; // Assert Assert.True(persistenceProvider.PersistCalled, "Persistence provider should have been called during pause."); Assert.Null(connectResult); Assert.False(registry.ConnectedCircuits.ContainsKey(circuitHost.CircuitId), "Circuit should not be in connected circuits."); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId.Secret, out _), "Circuit should be evicted from disconnected circuits."); } [Fact] public async Task PauseCircuitAsync_DoesNothing_IfConnectionIdIsDifferent() { // Arrange var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var (registry, persistenceProvider) = CreateRegistryWithProvider(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.Register(circuitHost); var differentConnectionId = "different-connection-id"; Assert.NotEqual(differentConnectionId, circuitHost.Client.ConnectionId); // Act await registry.PauseCircuitAsync(circuitHost, differentConnectionId); // Assert Assert.True(registry.ConnectedCircuits.TryGetValue(circuitHost.CircuitId, out var connectedCircuit)); Assert.Same(circuitHost, connectedCircuit); Assert.False(persistenceProvider.PersistCalled); } [Fact] public async Task PauseWithStaleConnectionId_AfterReconnect_DoesNotPause() { var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); var (registry, persistenceProvider) = CreateRegistryWithProvider(circuitIdFactory); var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId()); registry.Register(circuitHost); var originalConnectionId = circuitHost.Client.ConnectionId; // Client reconnects on connection B await registry.ConnectAsync(circuitHost.CircuitId, Mock.Of<ISingleClientProxy>(), "connection-B", default); // Stale pause from connection A arrives await registry.PauseCircuitAsync(circuitHost, originalConnectionId); // Circuit remains connected on connection B Assert.True(registry.ConnectedCircuits.TryGetValue(circuitHost.CircuitId, out var connected)); Assert.Same(circuitHost, connected); Assert.Equal("connection-B", circuitHost.Client.ConnectionId); Assert.False(persistenceProvider.PersistCalled); } private class TestCircuitRegistry : CircuitRegistry { public TestCircuitRegistry( CircuitIdFactory factory, CircuitOptions circuitOptions = null, TestCircuitPersistenceProvider persistenceProvider = null) : base( Options.Create(circuitOptions ?? new CircuitOptions()), NullLogger<CircuitRegistry>.Instance, factory, CreatePersistenceManager(circuitOptions ?? new CircuitOptions(), persistenceProvider ?? new TestCircuitPersistenceProvider())) { } public ManualResetEventSlim BeforeConnect { get; set; } public ManualResetEventSlim BeforeDisconnect { get; set; } public ManualResetEventSlim BeforePause { get; set; } public Action OnAfterEntryEvicted { get; set; } public TaskCompletionSource PauseInvoked { get; internal set; } protected override (CircuitHost, bool) ConnectCore(CircuitId circuitId, ISingleClientProxy clientProxy, string connectionId) { if (BeforeConnect != null) { Assert.True(BeforeConnect?.Wait(TimeSpan.FromSeconds(10)), "BeforeConnect failed to be set"); } return base.ConnectCore(circuitId, clientProxy, connectionId); } protected override bool DisconnectCore(CircuitHost circuitHost, string connectionId) { if (BeforeDisconnect != null) { Assert.True(BeforeDisconnect?.Wait(TimeSpan.FromSeconds(10)), "BeforeDisconnect failed to be set"); } return base.DisconnectCore(circuitHost, connectionId); } // In the actual CircuitRegistry, PauseCore is not virtual. We simulate its behavior for testing concurrency. // This method will be called by PauseCircuitAsync in TestCircuitRegistry due to normal method resolution. internal override Task PauseCore(CircuitHost circuitHost, string connectionId) { PauseInvoked.SetResult(); if (BeforePause != null) { Assert.True(BeforePause.Wait(TimeSpan.FromSeconds(10)), "BeforePauseTcs failed to be set"); } var result = base.PauseCore(circuitHost, connectionId); return result; } protected override void OnEntryEvicted(object key, object value, EvictionReason reason, object state) { base.OnEntryEvicted(key, value, reason, state); OnAfterEntryEvicted?.Invoke(); } } private class TestCircuitPersistenceProvider : ICircuitPersistenceProvider { public Task Persisting { get; set; } public ManualResetEventSlim AfterPersist { get; set; } = new ManualResetEventSlim(); public bool PersistCalled { get; internal set; } public async Task PersistCircuitAsync(CircuitId circuitId, PersistedCircuitState persistedCircuitState, CancellationToken cancellation = default) { PersistCalled = true; if (Persisting != null) { await Persisting; } AfterPersist.Set(); } public Task<PersistedCircuitState> RestoreCircuitAsync(CircuitId circuitId, CancellationToken cancellation = default) { throw new NotImplementedException(); } } private static CircuitPersistenceManager CreatePersistenceManager( CircuitOptions circuitOptions, TestCircuitPersistenceProvider persistenceProvider) { var manager = new CircuitPersistenceManager( Options.Create(circuitOptions), new Endpoints.ServerComponentSerializer(new EphemeralDataProtectionProvider()), persistenceProvider, // Ensure the passed provider is used new EphemeralDataProtectionProvider()); return manager; } private static CircuitRegistry CreateRegistry(CircuitIdFactory factory = null) { return new CircuitRegistry( Options.Create(new CircuitOptions()), NullLogger<CircuitRegistry>.Instance, factory ?? TestCircuitIdFactory.CreateTestFactory(), CreatePersistenceManager(new CircuitOptions(), new TestCircuitPersistenceProvider())); } private static (CircuitRegistry Registry, TestCircuitPersistenceProvider Provider) CreateRegistryWithProvider(CircuitIdFactory factory = null, CircuitOptions circuitOptions = null) { var options = circuitOptions ?? new CircuitOptions(); var provider = new TestCircuitPersistenceProvider(); var persistenceManager = CreatePersistenceManager(options, provider); var registry = new CircuitRegistry( Options.Create(options), NullLogger<CircuitRegistry>.Instance, factory ?? TestCircuitIdFactory.CreateTestFactory(), persistenceManager); return (registry, provider); } }