Blazorise icon indicating copy to clipboard operation
Blazorise copied to clipboard

Bar BreakpointAdapter throws DisposedException after quick redirect

Open njannink opened this issue 2 years ago • 8 comments

When using the Bar component from Blazorise 1.0.6 I see the following exception passing by:

crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
      Unhandled exception rendering component: Cannot access a disposed object.
      Object name: 'DotNetObjectReference`1'.
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'DotNetObjectReference`1'.
   at Microsoft.JSInterop.DotNetObjectReference`1[[Blazorise.BreakpointActivatorAdapter, Blazorise, Version=1.0.6.0, Culture=neutral, PublicKeyToken=null]].ThrowIfDisposed()
   at Microsoft.JSInterop.JSRuntime.TrackObjectReference[BreakpointActivatorAdapter](DotNetObjectReference`1 dotNetObjectReference)
   at Microsoft.JSInterop.Infrastructure.DotNetObjectReferenceJsonConverter`1[[Blazorise.BreakpointActivatorAdapter, Blazorise, Version=1.0.6.0, Culture=neutral, PublicKeyToken=null]].Write(Utf8JsonWriter writer, DotNetObjectReference`1 value, JsonSerializerOptions options)
   at System.Text.Json.Serialization.JsonConverter`1[[Microsoft.JSInterop.DotNetObjectReference`1[[Blazorise.BreakpointActivatorAdapter, Blazorise, Version=1.0.6.0, Culture=neutral, PublicKeyToken=null]], Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].TryWrite(Utf8JsonWriter writer, DotNetObjectReference`1& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1[[Microsoft.JSInterop.DotNetObjectReference`1[[Blazorise.BreakpointActivatorAdapter, Blazorise, Version=1.0.6.0, Culture=neutral, PublicKeyToken=null]], Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].TryWriteAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1[[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].TryWrite(Utf8JsonWriter writer, Object& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.Converters.ArrayConverter`2[[System.Object[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].OnWriteResume(Utf8JsonWriter writer, Object[] array, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonCollectionConverter`2[[System.Object[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].OnTryWrite(Utf8JsonWriter writer, Object[] value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1[[System.Object[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].TryWrite(Utf8JsonWriter writer, Object[]& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.Serialization.JsonConverter`1[[System.Object[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].WriteCore(Utf8JsonWriter writer, Object[]& value, JsonSerializerOptions options, WriteStack& state)
   at System.Text.Json.JsonSerializer.WriteUsingSerializer[Object[]](Utf8JsonWriter writer, Object[]& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.WriteStringUsingSerializer[Object[]](Object[]& value, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Serialize[Object[]](Object[] value, JsonSerializerOptions options)
   at Microsoft.JSInterop.JSRuntime.InvokeAsync[IJSVoidResult](Int64 targetInstanceId, String identifier, CancellationToken cancellationToken, Object[] args)
   at Microsoft.JSInterop.JSRuntime.<InvokeAsync>d__16`1[[Microsoft.JSInterop.Infrastructure.IJSVoidResult, Microsoft.JSInterop, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].MoveNext()
   at Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeVoidAsync(IJSObjectReference jsObjectReference, String identifier, Object[] args)
   at Blazorise.Modules.JSBreakpointModule.RegisterBreakpoint(DotNetObjectReference`1 dotNetObjectRef, String elementId)
   at Blazorise.Bar.OnFirstAfterRenderAsync()
   at Blazorise.BaseComponent.OnAfterRenderAsync(Boolean firstRender)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

This is probably happening because my blazor app detects there is no user authenticated yet and redirect to the authentication service so not allowing all services to correctly setup/render before being disposed again.

Some additional check is probably required in the RegisterBreakpoint on if the JsModule is disposed again

njannink avatar Aug 23 '22 22:08 njannink

We had similar error reports when Bar was used with authentication, so I would guess the same thing is happening again. But without being able to reproduce it on our side it would be hard to fix.

stsrki avatar Aug 24 '22 08:08 stsrki

If I look at the code then I can see there is a possible race condition:

            1. var moduleInstance = await Module;
            
            2. await moduleInstance.InvokeVoidAsync( "registerBreakpointComponent", dotNetObjectRef, elementId );

Between step 1 and step 2 the Module can be disposed so before invoking on the module it should be checked if its disposed.

var moduleInstance = await Module;

if (!AsyncDisposed)             
            await moduleInstance.InvokeVoidAsync( "registerBreakpointComponent", dotNetObjectRef, elementId );

njannink avatar Aug 25 '22 14:08 njannink

I see this construction everywhere so possibly everywhere this dispose race-condition could occur

njannink avatar Aug 25 '22 14:08 njannink

Maybe a helper method for save invocation:

        protected async ValueTask InvokeVoidSafeAsync( string identifier, params object[] args )
        {
            if ( IsUnsafe ) return;

            var module = await moduleTask;

            if (AsyncDisposed) return;

            await module.InvokeVoidAsync( identifier, args );
        }


        protected async ValueTask<T> InvokeSafeAsync<T>( string identifier, params object[] args )
        {
            if ( IsUnsafe )
                return default;

            var module = await moduleTask;

            if ( AsyncDisposed )
                return default;

            return await module.InvokeAsync<T>( identifier, args );
        }

njannink avatar Aug 25 '22 14:08 njannink

To add onto this, I think I have seen Microsoft's implementations doing a try catch on JSDisconnectedException.

David-Moreira avatar Aug 25 '22 20:08 David-Moreira

I think the helper method might be the cleanest way to go. Maybe it can even include JSDisconnectedException.

stsrki avatar Aug 26 '22 09:08 stsrki

what would you like that happens on a JSDisconnectedException? Also return the default value?

njannink avatar Aug 26 '22 13:08 njannink

protected async ValueTask InvokeSafeAsync<T>( string identifier, params object[] args )
{
    if ( IsUnsafe ) return default;

    try
    {
        var module = await moduleTask;

        if (AsyncDisposed) return default;

        await module.InvokeVoidAsync( identifier, args );
    }
    catch (Exception e) 
        when (e is JSDisconnectedException 
        or ObjectDisposedException 
        or TaskCanceledException)
    {
        return default;
    }
}

njannink avatar Aug 26 '22 13:08 njannink

I just tested this with 1.0.7 and the exception is gone. Thanks @stsrki

njannink avatar Oct 13 '22 13:10 njannink

I just tested this with 1.0.7 and the exception is gone. Thanks @stsrki

Great, Thanks for testing.

stsrki avatar Oct 13 '22 13:10 stsrki