1+ // Licensed to the .NET Foundation under one or more agreements.
2+ // The .NET Foundation licenses this file to you under the MIT license.
3+
4+ using Microsoft . AspNetCore . Components ;
5+ using Microsoft . AspNetCore . Components . QuickGrid ;
6+ using Microsoft . JSInterop ;
7+ using System . Reflection ;
8+
9+ namespace Microsoft . AspNetCore . Components . QuickGrid . Test ;
10+
11+ /// <summary>
12+ /// A QuickGrid implementation that simulates the behavior before the race condition fix.
13+ /// This class intentionally does NOT set _disposeBool during disposal to simulate the race condition.
14+ /// </summary>
15+ /// <typeparam name="TGridItem">The type of data represented by each row in the grid.</typeparam>
16+ internal class FailingQuickGrid < TGridItem > : QuickGrid < TGridItem > , IAsyncDisposable
17+ {
18+ [ Inject ] private IJSRuntime JS { get ; set ; } = default ! ;
19+
20+ private readonly TaskCompletionSource _onAfterRenderCompleted = new ( ) ;
21+ private bool _completionSignaled ;
22+
23+ public bool DisposeAsyncWasCalled { get ; private set ; }
24+
25+ /// <summary>
26+ /// Task that completes when OnAfterRenderAsync has finished executing.
27+ /// This allows tests to wait deterministically for the race condition to occur.
28+ /// </summary>
29+ public Task OnAfterRenderCompleted => _onAfterRenderCompleted . Task ;
30+
31+ /// <summary>
32+ /// Intentionally does NOT call base.DisposeAsync() to prevent _disposeBool from being set.
33+ /// This simulates the behavior before the fix was implemented.
34+ /// </summary>
35+ public new async ValueTask DisposeAsync ( )
36+ {
37+ DisposeAsyncWasCalled = true ;
38+ // Intentionally do nothing to prevent _disposeBool from being set to true
39+ // This means the OnAfterRenderAsync method will not detect that the component is disposed
40+ // and will proceed to call init() even after disposal, demonstrating the race condition
41+
42+ // DO NOT call base.DisposeAsync() - this is the key to simulating the race condition
43+ await Task . CompletedTask ;
44+ }
45+
46+ /// <summary>
47+ /// Explicit interface implementation to ensure our disposal method is called.
48+ /// </summary>
49+ async ValueTask IAsyncDisposable . DisposeAsync ( )
50+ {
51+ await DisposeAsync ( ) ;
52+ }
53+
54+ /// <summary>
55+ /// Check if _disposeBool is false, proving we didn't call base.DisposeAsync().
56+ /// This is used by tests to verify that our simulation is working correctly.
57+ /// </summary>
58+ public bool IsDisposeBoolFalse ( )
59+ {
60+ var field = typeof ( QuickGrid < TGridItem > ) . GetField ( "_disposeBool" , BindingFlags . NonPublic | BindingFlags . Instance ) ;
61+ return field ? . GetValue ( this ) is false ;
62+ }
63+
64+ /// <summary>
65+ /// Override OnAfterRenderAsync to simulate the race condition by NOT checking _disposeBool.
66+ /// This exactly replicates the code path that existed before the race condition fix.
67+ /// </summary>
68+ protected override async Task OnAfterRenderAsync ( bool firstRender )
69+ {
70+ try
71+ {
72+ if ( firstRender )
73+ {
74+ // Get the IJSRuntime (same as base class)
75+ if ( JS != null )
76+ {
77+ // Import the JS module (this will trigger our TestJsRuntime's import logic)
78+ var jsModule = await JS . InvokeAsync < IJSObjectReference > ( "import" ,
79+ "./_content/Microsoft.AspNetCore.Components.QuickGrid/QuickGrid.razor.js" ) ;
80+
81+ // THE KEY DIFFERENCE: The original code did NOT check _disposeBool here
82+ // The fix added: if (_disposeBool) return;
83+ // By omitting this check, we demonstrate the race condition where init gets called on disposed components
84+
85+ // Call init - this happens even if component was disposed during import
86+ // For our test, we don't need a real table reference, just need to trigger the JS call
87+ await jsModule . InvokeAsync < IJSObjectReference > ( "init" , new object ( ) ) ;
88+
89+ // Signal completion only after the init call has completed, and only once
90+ if ( ! _completionSignaled )
91+ {
92+ _completionSignaled = true ;
93+ _onAfterRenderCompleted . TrySetResult ( ) ;
94+ }
95+ return ;
96+ }
97+ }
98+ }
99+ finally
100+ {
101+ // Only signal completion if we haven't already done it and this is the first render
102+ if ( firstRender && ! _completionSignaled )
103+ {
104+ _completionSignaled = true ;
105+ _onAfterRenderCompleted . TrySetResult ( ) ;
106+ }
107+ }
108+ }
109+ }
0 commit comments