@@ -620,6 +620,17 @@ protected override void OnEntryEvicted(object key, object value, EvictionReason
620620 base . OnEntryEvicted ( key , value , reason , state ) ;
621621 OnAfterEntryEvicted ? . Invoke ( ) ;
622622 }
623+
624+ public void TriggerEviction ( object key , object value , EvictionReason reason )
625+ {
626+ OnEntryEvicted ( key , value , reason , null ) ;
627+ }
628+
629+ public async Task SimulateEvictionAndDispose ( CircuitHost circuitHost )
630+ {
631+ // Directly call PauseAndDisposeCircuitHost which is what eviction does
632+ await PauseAndDisposeCircuitHost ( circuitHost , saveStateToClient : false ) ;
633+ }
623634 }
624635
625636 private class TestCircuitPersistenceProvider : ICircuitPersistenceProvider
@@ -678,4 +689,126 @@ private static (CircuitRegistry Registry, TestCircuitPersistenceProvider Provide
678689 persistenceManager ) ;
679690 return ( registry , provider ) ;
680691 }
692+
693+ [ Fact ]
694+ public async Task PauseAfterTermination_DoesNotThrow ( )
695+ {
696+ var circuitIdFactory = TestCircuitIdFactory . CreateTestFactory ( ) ;
697+ var options = new CircuitOptions ( ) ;
698+
699+ var circuitHost = new TestCircuitHostForRaceConditions (
700+ circuitIdFactory . CreateCircuitId ( ) ,
701+ CreateServiceScope ( ) ,
702+ options ) ;
703+
704+ var persistenceProvider = new TestCircuitPersistenceProvider ( ) ;
705+ var registry = new TestCircuitRegistry ( circuitIdFactory , options , persistenceProvider ) ;
706+ registry . Register ( circuitHost ) ;
707+
708+ // First terminate the circuit - it calls circuitHost.DisposeAsync()
709+ await registry . TerminateAsync ( circuitHost . CircuitId ) ;
710+
711+ // Then try to pause - it will try to resolve services from the DI scope that is already disposed
712+ await registry . PauseAndDisposeCircuitHost ( circuitHost , saveStateToClient : true ) ;
713+ }
714+
715+ [ Fact ]
716+ public async Task PauseAfterEviction_DoesNotThrow ( )
717+ {
718+ var circuitIdFactory = TestCircuitIdFactory . CreateTestFactory ( ) ;
719+ var options = new CircuitOptions ( ) ;
720+
721+ var circuitHost = new TestCircuitHostForRaceConditions (
722+ circuitIdFactory . CreateCircuitId ( ) ,
723+ CreateServiceScope ( ) ,
724+ options ) ;
725+
726+ var persistenceProvider = new TestCircuitPersistenceProvider ( ) ;
727+ var registry = new TestCircuitRegistry ( circuitIdFactory , options , persistenceProvider ) ;
728+ registry . Register ( circuitHost ) ;
729+
730+ // First simulate eviction by calling the same method that eviction calls
731+ await registry . SimulateEvictionAndDispose ( circuitHost ) ;
732+
733+ // Then try to pause - it will try to resolve services from the DI scope that is already disposed
734+ await registry . PauseAndDisposeCircuitHost ( circuitHost , saveStateToClient : true ) ;
735+ }
736+
737+ [ Fact ]
738+ public async Task EvictionAndTermination_DoesNotThrow ( )
739+ {
740+ var circuitIdFactory = TestCircuitIdFactory . CreateTestFactory ( ) ;
741+ var options = new CircuitOptions ( ) ;
742+
743+ var circuitHost = new TestCircuitHostForRaceConditions (
744+ circuitIdFactory . CreateCircuitId ( ) ,
745+ CreateServiceScope ( ) ,
746+ options ) ;
747+
748+ var persistenceProvider = new TestCircuitPersistenceProvider ( ) ;
749+ var registry = new TestCircuitRegistry ( circuitIdFactory , options , persistenceProvider ) ;
750+ registry . Register ( circuitHost ) ;
751+
752+ // Simulate race condition: eviction and termination happening concurrently
753+ await registry . SimulateEvictionAndDispose ( circuitHost ) ;
754+ await registry . TerminateAsync ( circuitHost . CircuitId ) ;
755+ }
756+
757+ private static AsyncServiceScope CreateServiceScope ( )
758+ {
759+ var serviceCollection = new ServiceCollection ( ) ;
760+ serviceCollection . AddSingleton ( sp => new ComponentStatePersistenceManager (
761+ NullLoggerFactory . Instance . CreateLogger < ComponentStatePersistenceManager > ( ) , sp ) ) ;
762+ serviceCollection . AddSingleton ( sp => sp . GetRequiredService < ComponentStatePersistenceManager > ( ) . State ) ;
763+ var serviceProvider = serviceCollection . BuildServiceProvider ( ) ;
764+ return serviceProvider . CreateAsyncScope ( ) ;
765+ }
766+
767+ private class TestCircuitHostForRaceConditions : CircuitHost
768+ {
769+ public TestCircuitHostForRaceConditions (
770+ CircuitId circuitId ,
771+ AsyncServiceScope scope ,
772+ CircuitOptions options )
773+ : base (
774+ circuitId ,
775+ scope ,
776+ options ,
777+ new CircuitClientProxy ( Mock . Of < ISingleClientProxy > ( ) , Guid . NewGuid ( ) . ToString ( ) ) ,
778+ CreateRemoteRenderer ( ) ,
779+ Array . Empty < ComponentDescriptor > ( ) ,
780+ new RemoteJSRuntime ( Options . Create ( new CircuitOptions ( ) ) , Options . Create ( new HubOptions < ComponentHub > ( ) ) , Mock . Of < ILogger < RemoteJSRuntime > > ( ) ) ,
781+ new RemoteNavigationManager ( Mock . Of < ILogger < RemoteNavigationManager > > ( ) ) ,
782+ Array . Empty < CircuitHandler > ( ) ,
783+ new CircuitMetrics ( new TestMeterFactory ( ) ) ,
784+ new CircuitActivitySource ( ) ,
785+ NullLogger < CircuitHost > . Instance )
786+ {
787+ }
788+
789+ private static RemoteRenderer CreateRemoteRenderer ( )
790+ {
791+ var clientProxy = new CircuitClientProxy ( Mock . Of < ISingleClientProxy > ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
792+ var jsRuntime = new RemoteJSRuntime ( Options . Create ( new CircuitOptions ( ) ) , Options . Create ( new HubOptions < ComponentHub > ( ) ) , Mock . Of < ILogger < RemoteJSRuntime > > ( ) ) ;
793+ var componentsActivitySource = new ComponentsActivitySource ( ) ;
794+ var serviceProvider = new Mock < IServiceProvider > ( ) ;
795+ serviceProvider
796+ . Setup ( services => services . GetService ( typeof ( IJSRuntime ) ) )
797+ . Returns ( jsRuntime ) ;
798+ serviceProvider
799+ . Setup ( services => services . GetService ( typeof ( ComponentsActivitySource ) ) )
800+ . Returns ( componentsActivitySource ) ;
801+ var serverComponentDeserializer = Mock . Of < IServerComponentDeserializer > ( ) ;
802+
803+ return new RemoteRenderer (
804+ serviceProvider . Object ,
805+ NullLoggerFactory . Instance ,
806+ new CircuitOptions ( ) ,
807+ clientProxy ,
808+ serverComponentDeserializer ,
809+ NullLogger . Instance ,
810+ jsRuntime ,
811+ new CircuitJSComponentInterop ( new CircuitOptions ( ) ) ) ;
812+ }
813+ }
681814}
0 commit comments