I’m encountering an issue in a Blazor app where the Dispose() method of a parent component is called before the Dispose() of its child components.
Context:
We have a component called DataFence which creates its own scoped IServiceScope. This scope provides services (e.g., a DbContext) to child components via CascadingValue.
In DataFence.Dispose, the scoped service provider is disposed, which disposes the services created from it, including the DbContext.In a child component’s Dispose, I attempt to unsubscribe from an event on the cascaded DbContext (specifically, ChangeTracker.StateChanged). However, since the parent’s Dispose runs first and disposes the DbContext, the child’s attempt to unsubscribe throws an exception because the DbContext is already disposed.
DbContext does not expose an IsDisposed property or similar to check if it’s still valid. When the parent is disposed, there’s no real need to unsubscribe since the whole scope is gone. But if only the child is disposed (for example, it’s conditionally rendered and removed from the UI), it still needs to unsubscribe the event to prevent leaks.
Questions:
Can I rely on the order in which Blazor calls
Disposemethods? Specifically, is child componentDisposeguaranteed to run before the parent’s? Probably not as I have found out. The documentation ms docs states that one should not rely onDisposebeing called only afterOnInitializedAsyncorOnParametersSetAsynchave completed. But I see no mention of Dispose call hierarchy.If the order is not guaranteed, and the cascaded service (
DbContext) has noIsDisposedproperty, what is the best pattern to safely unsubscribe from events in the child’s Dispose? The only workaround I’ve found so far is wrapping the unsubscribe call in a try-catch that ignores the disposed exception, which feels “ugly”.
Thanks for any help!
Simplified illustrative code example (may or may not compile):
@* DataFence *@@implements IDisposable<CascadingValue Value="_dbContext"> @ChildContent</CascadingValue>@code { [Inject] IServiceProvider ServiceProvider { get; set; } = default!; [Parameter] public RenderFragment ChildContent { get; set; } = default!; private IServiceScope? _scope; private MyDbContext? _dbContext; protected override void OnInitialized() { _scope = ServiceProvider.CreateScope(); _dbContext = _scope.ServiceProvider.GetRequiredService<MyDbContext>(); } public void Dispose() { _scope?.Dispose(); // Disposes DbContext here }}@* Child component example *@@implements IDisposable@code { [CascadingParameter] MyDbContext? FencedDbContext { get; set; } protected override void OnInitialized() { if (FencedDbContext is not null) FencedDbContext.ChangeTracker.StateChanged += OnStateChanged; } public void Dispose() { if (FencedDbContext is not null) FencedDbContext.ChangeTracker.StateChanged -= OnStateChanged; }}