diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs index 21b3420b8f78..686d0d2c1833 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs @@ -104,7 +104,15 @@ public async ValueTask DisposeAsync() { Disposed = true; - await _jsRuntime.InvokeVoidAsync("DotNet.disposeJSObjectReferenceById", Id); + try + { + await _jsRuntime.InvokeVoidAsync("DotNet.disposeJSObjectReferenceById", Id); + } + catch (JSDisconnectedException) + { + // If the JavaScript runtime is disconnected, there's no need to dispose the JS object reference + // as the JS side is already gone. We can safely ignore this exception. + } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs index 2864c7fcc327..1c153eecf439 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceTest.cs @@ -66,6 +66,37 @@ public void JSInProcessObjectReference_Dispose_DisallowsFurtherInteropCalls() Assert.Throws(() => jsObject.Invoke("test", "arg1", "arg2")); } + [Fact] + public async Task JSObjectReference_DisposeAsync_IgnoresJSDisconnectedException() + { + // Arrange + var jsRuntime = new TestJSRuntimeThatThrowsJSDisconnectedException(); + var jsObject = new JSObjectReference(jsRuntime, 0); + + // Act & Assert - Should not throw + await jsObject.DisposeAsync(); + + // Verify dispose was attempted + Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount); + } + + [Fact] + public async Task JSObjectReference_DisposeAsync_IgnoresJSDisconnectedException_OnMultipleCalls() + { + // Arrange + var jsRuntime = new TestJSRuntimeThatThrowsJSDisconnectedException(); + var jsObject = new JSObjectReference(jsRuntime, 0); + + // Act & Assert - Should not throw on first call + await jsObject.DisposeAsync(); + + // Act & Assert - Should not throw on second call (no-op) + await jsObject.DisposeAsync(); + + // Verify dispose was only attempted once + Assert.Equal(1, jsRuntime.BeginInvokeJSInvocationCount); + } + class TestJSRuntime : JSRuntime { public int BeginInvokeJSInvocationCount { get; private set; } @@ -85,6 +116,26 @@ protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocation } } + class TestJSRuntimeThatThrowsJSDisconnectedException : JSRuntime + { + public int BeginInvokeJSInvocationCount { get; private set; } + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + BeginInvokeJSInvocationCount++; + throw new JSDisconnectedException("JavaScript interop calls cannot be issued at this time. This is because the circuit has disconnected and is being disposed."); + } + + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + { + } + } + class TestJSInProcessRuntime : JSInProcessRuntime { public int InvokeJSInvocationCount { get; private set; }