@@ -263,6 +263,8 @@ public async Task GetOrComputeLastValue_FollowsCorrectValueTransitionSequence()
263263 var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
264264
265265 // Act & Assert - First call: Returns restored value from state
266+ var firstCall = provider . GetCurrentValue ( componentState , cascadingParameterInfo ) ;
267+ Assert . Equal ( "first-restored-value" , firstCall ) ;
266268 Assert . Equal ( "first-restored-value" , component . State ) ;
267269
268270 // Change the component's property value
@@ -708,4 +710,213 @@ public void Constructor_WorksCorrectly_ForPublicProperty()
708710 Assert . NotNull ( subscription ) ;
709711 subscription . Dispose ( ) ;
710712 }
713+
714+ [ Fact ]
715+ public async Task ComponentRecreation_PreservesPersistedState_WhenComponentIsRecreatedDuringNavigation ( )
716+ {
717+ // This test simulates the scenario where a component is destroyed and recreated (like during navigation)
718+ // and verifies that the persisted state is correctly restored in the new component instance
719+
720+ // Arrange
721+ var appState = new Dictionary < string , byte [ ] > ( ) ;
722+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
723+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
724+ . AddSingleton ( manager )
725+ . AddSingleton ( manager . State )
726+ . AddFakeLogging ( )
727+ . BuildServiceProvider ( ) ;
728+ var renderer = new TestRenderer ( serviceProvider ) ;
729+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
730+ var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
731+
732+ // Setup initial persisted state
733+ var component1 = new TestComponent { State = "initial-property-value" } ;
734+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
735+ var componentState1 = renderer . GetComponentState ( component1 ) ;
736+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
737+
738+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value-from-previous-session" , JsonSerializerOptions . Web ) ;
739+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
740+
741+ // Act & Assert - First component instance should get the persisted value
742+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
743+ Assert . Equal ( "persisted-value-from-previous-session" , component1 . State ) ;
744+
745+ // Simulate component destruction (like during navigation away)
746+ renderer . RemoveRootComponent ( componentId1 ) ;
747+
748+ // Simulate component recreation (like during navigation back) - NEW SUBSCRIPTION CREATED
749+ var component2 = new TestComponent { State = "new-component-initial-value" } ;
750+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
751+ var componentState2 = renderer . GetComponentState ( component2 ) ;
752+
753+ // Verify the key is the same (important for components without @key)
754+ var key2 = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState2 , nameof ( TestComponent . State ) ) ;
755+ Assert . Equal ( key , key2 ) ;
756+
757+ // The state should still be available for restoration
758+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
759+
760+ // Assert - The new component instance should get the same persisted value
761+ var providerForSecondComponent = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
762+ var cascadingParameterInfoForSecondComponent = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
763+ var restoredCall = providerForSecondComponent . GetCurrentValue ( componentState2 , cascadingParameterInfoForSecondComponent ) ;
764+ Assert . Equal ( "persisted-value-from-previous-session" , restoredCall ) ;
765+ Assert . Equal ( "persisted-value-from-previous-session" , component2 . State ) ;
766+ }
767+
768+ [ Fact ]
769+ public async Task ComponentRecreation_WithStateUpdates_PreservesCorrectValueTransitionSequence ( )
770+ {
771+ // This test simulates the full lifecycle with component recreation and state updates
772+ // following the pattern from GetOrComputeLastValue_FollowsCorrectValueTransitionSequence
773+ // but with subscription recreation between state restorations
774+
775+ // Arrange
776+ var appState = new Dictionary < string , byte [ ] > ( ) ;
777+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
778+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
779+ . AddSingleton ( manager )
780+ . AddSingleton ( manager . State )
781+ . AddFakeLogging ( )
782+ . BuildServiceProvider ( ) ;
783+ var renderer = new TestRenderer ( serviceProvider ) ;
784+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
785+ var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
786+
787+ // First component lifecycle
788+ var component1 = new TestComponent { State = "initial-property-value" } ;
789+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
790+ var componentState1 = renderer . GetComponentState ( component1 ) ;
791+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
792+
793+ // Pre-populate with first persisted value
794+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "first-restored-value" , JsonSerializerOptions . Web ) ;
795+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
796+
797+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
798+
799+ // Act & Assert - First component gets restored value
800+ var firstCall = provider . GetCurrentValue ( componentState1 , cascadingParameterInfo ) ;
801+ Assert . Equal ( "first-restored-value" , firstCall ) ;
802+ Assert . Equal ( "first-restored-value" , component1 . State ) ;
803+
804+ // Update component property
805+ component1 . State = "updated-by-component-1" ;
806+ Assert . Equal ( "updated-by-component-1" , provider . GetCurrentValue ( componentState1 , cascadingParameterInfo ) ) ;
807+
808+ // Simulate component destruction and recreation (NEW SUBSCRIPTION CREATED)
809+ renderer . RemoveRootComponent ( componentId1 ) ;
810+
811+ var component2 = new TestComponent { State = "new-component-initial-value" } ;
812+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
813+ var componentState2 = renderer . GetComponentState ( component2 ) ;
814+
815+ // Restore state with a different value
816+ appState . Clear ( ) ;
817+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "second-restored-value" , JsonSerializerOptions . Web ) ;
818+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . ValueUpdate ) ;
819+
820+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
821+
822+ // Assert - New component gets the updated restored value
823+ var secondComponentCall = provider . GetCurrentValue ( componentState2 , cascadingParameterInfo ) ;
824+ Assert . Equal ( "second-restored-value" , secondComponentCall ) ;
825+ Assert . Equal ( "second-restored-value" , component2 . State ) ;
826+
827+ // Continue with property updates on the new component
828+ component2 . State = "updated-by-component-2" ;
829+ Assert . Equal ( "updated-by-component-2" , provider . GetCurrentValue ( componentState2 , cascadingParameterInfo ) ) ;
830+ }
831+
832+ [ Fact ]
833+ public async Task ComponentRecreation_WithSkipNotifications_StillRestoresCorrectly ( )
834+ {
835+ // This test verifies that the fix works even when skipNotifications is true during component recreation,
836+ // which is the core scenario that was broken before our fix
837+
838+ // Arrange
839+ var appState = new Dictionary < string , byte [ ] > ( ) ;
840+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
841+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
842+ . AddSingleton ( manager )
843+ . AddSingleton ( manager . State )
844+ . AddFakeLogging ( )
845+ . BuildServiceProvider ( ) ;
846+ var renderer = new TestRenderer ( serviceProvider ) ;
847+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
848+ var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
849+
850+ // Setup persisted state
851+ var component1 = new TestComponent { State = "component-initial-value" } ;
852+ var componentId1 = renderer . AssignRootComponentId ( component1 ) ;
853+ var componentState1 = renderer . GetComponentState ( component1 ) ;
854+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState1 , nameof ( TestComponent . State ) ) ;
855+
856+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "persisted-value" , JsonSerializerOptions . Web ) ;
857+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
858+
859+ // First component gets the persisted value
860+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId1 , ParameterView . Empty ) ) ;
861+ var firstCall = provider . GetCurrentValue ( componentState1 , cascadingParameterInfo ) ;
862+ Assert . Equal ( "persisted-value" , firstCall ) ;
863+ Assert . Equal ( "persisted-value" , component1 . State ) ;
864+
865+ // Destroy and recreate component (simulating navigation or component without @key)
866+ renderer . RemoveRootComponent ( componentId1 ) ;
867+
868+ // Create new component instance - this will create a NEW SUBSCRIPTION
869+ var component2 = new TestComponent { State = "different-initial-value" } ;
870+ var componentId2 = renderer . AssignRootComponentId ( component2 ) ;
871+ var componentState2 = renderer . GetComponentState ( component2 ) ;
872+
873+ // Render the new component - this should restore the persisted value even if skipNotifications is true
874+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId2 , ParameterView . Empty ) ) ;
875+
876+ // Assert - The new component should get the persisted value, not its initial property value
877+ var providerForLastComponent = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
878+ var cascadingParameterInfoForLastComponent = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
879+ var restoredCall2 = providerForLastComponent . GetCurrentValue ( componentState2 , cascadingParameterInfoForLastComponent ) ;
880+ Assert . Equal ( "persisted-value" , restoredCall2 ) ;
881+ Assert . Equal ( "persisted-value" , component2 . State ) ;
882+ }
883+
884+ [ Fact ]
885+ public async Task DebugTest_UnderstandIgnoreComponentPropertyValueFlag ( )
886+ {
887+ // Simple test to understand the _ignoreComponentPropertyValue flag behavior
888+ var appState = new Dictionary < string , byte [ ] > ( ) ;
889+ var manager = new ComponentStatePersistenceManager ( NullLogger < ComponentStatePersistenceManager > . Instance ) ;
890+ var serviceProvider = PersistentStateProviderServiceCollectionExtensions . AddSupplyValueFromPersistentComponentStateProvider ( new ServiceCollection ( ) )
891+ . AddSingleton ( manager )
892+ . AddSingleton ( manager . State )
893+ . AddFakeLogging ( )
894+ . BuildServiceProvider ( ) ;
895+ var renderer = new TestRenderer ( serviceProvider ) ;
896+ var provider = ( PersistentStateValueProvider ) renderer . ServiceProviderCascadingValueSuppliers . Single ( ) ;
897+ var component = new TestComponent { State = "initial-property-value" } ;
898+ var componentId = renderer . AssignRootComponentId ( component ) ;
899+ var componentState = renderer . GetComponentState ( component ) ;
900+ var cascadingParameterInfo = CreateCascadingParameterInfo ( nameof ( TestComponent . State ) , typeof ( string ) ) ;
901+
902+ // Set up state to restore
903+ var key = PersistentStateValueProviderKeyResolver . ComputeKey ( componentState , nameof ( TestComponent . State ) ) ;
904+ appState [ key ] = JsonSerializer . SerializeToUtf8Bytes ( "restored-value" , JsonSerializerOptions . Web ) ;
905+ await manager . RestoreStateAsync ( new TestStore ( appState ) , RestoreContext . InitialValue ) ;
906+
907+ // Render component - this should restore the value
908+ await renderer . Dispatcher . InvokeAsync ( ( ) => renderer . RenderRootComponentAsync ( componentId , ParameterView . Empty ) ) ;
909+
910+ // First call should return restored value
911+ var firstCall = provider . GetCurrentValue ( componentState , cascadingParameterInfo ) ;
912+ Assert . Equal ( "restored-value" , firstCall ) ;
913+ Assert . Equal ( "restored-value" , component . State ) ;
914+
915+ // Update the component's property manually
916+ component . State = "manually-updated-value" ;
917+
918+ // Second call should return the manually updated value
919+ var secondCall = provider . GetCurrentValue ( componentState , cascadingParameterInfo ) ;
920+ Assert . Equal ( "manually-updated-value" , secondCall ) ;
921+ }
711922}
0 commit comments