From b1817f2c0bfb60d9fb0e42c5efad52300391c23c Mon Sep 17 00:00:00 2001 From: Eugene Odera Date: Tue, 18 Nov 2025 21:54:03 +0300 Subject: [PATCH 1/3] Refactor PullFromJSDataStream disposal logic and add unit test for Dispose --- .../Shared/src/PullFromJSDataStream.cs | 51 +++++++++++++++++-- .../test/PullFromJSDataStreamTest.cs | 23 ++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Components/Shared/src/PullFromJSDataStream.cs b/src/Components/Shared/src/PullFromJSDataStream.cs index aca42ee0cff6..7038e63650fb 100644 --- a/src/Components/Shared/src/PullFromJSDataStream.cs +++ b/src/Components/Shared/src/PullFromJSDataStream.cs @@ -14,8 +14,9 @@ internal sealed class PullFromJSDataStream : Stream private readonly IJSRuntime _runtime; private readonly IJSStreamReference _jsStreamReference; private readonly long _totalLength; - private readonly CancellationToken _streamCancellationToken; + private readonly CancellationTokenSource _streamCts; private long _offset; + private bool _isDisposed; public static PullFromJSDataStream CreateJSDataStream( IJSRuntime runtime, @@ -36,8 +37,9 @@ private PullFromJSDataStream( _runtime = runtime; _jsStreamReference = jsStreamReference; _totalLength = totalLength; - _streamCancellationToken = cancellationToken; + _streamCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _offset = 0; + } public override bool CanRead => true; @@ -88,7 +90,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation private void ThrowIfCancellationRequested(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested || - _streamCancellationToken.IsCancellationRequested) + _streamCts.IsCancellationRequested) { throw new TaskCanceledException(); } @@ -110,4 +112,47 @@ private async ValueTask RequestDataFromJSAsync(int numBytesToRead) } return bytesRead; } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + _streamCts?.Cancel(); + try + { + _ = _jsStreamReference?.DisposeAsync().Preserve(); + } + catch + { + } + + _isDisposed = true; + + base.Dispose(disposing); + } + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _streamCts?.Cancel(); + try + { + if (_jsStreamReference is not null) + { + await _jsStreamReference.DisposeAsync(); + } + } + catch + { + } + + _isDisposed = true; + + await base.DisposeAsync(); + } } diff --git a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs index bfe0f0f7a55a..407fcdb840ec 100644 --- a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.InternalTesting; using Microsoft.JSInterop; using Moq; @@ -101,6 +100,28 @@ public async Task ReceiveData_JSProvidesExcessData_Throws2() Assert.Equal("Failed to read the requested number of bytes from the stream.", ex.Message); } + [Fact] + public void Dispose_CallsDisposeAsyncOnJSStreamReference() + { + var jsStreamReferenceMock = new Mock(); + var stream = PullFromJSDataStream.CreateJSDataStream(_jsRuntime, jsStreamReferenceMock.Object, totalLength: 10, cancellationToken: CancellationToken.None); + + stream.Dispose(); + + jsStreamReferenceMock.Verify(x => x.DisposeAsync(), Times.Once); + } + + [Fact] + public async Task DisposeAsync_CallsDisposeAsyncOnJSStreamReference() + { + var jsStreamReferenceMock = new Mock(); + var stream = PullFromJSDataStream.CreateJSDataStream(_jsRuntime, jsStreamReferenceMock.Object, totalLength: 10, cancellationToken: CancellationToken.None); + + await stream.DisposeAsync(); + + jsStreamReferenceMock.Verify(x => x.DisposeAsync(), Times.Once); + } + private static PullFromJSDataStream CreateJSDataStream(byte[] data, IJSRuntime runtime = null) { runtime ??= new TestJSRuntime(data); From 64a0116d26dd991e5e0f927d50bd6c61a78ce5b4 Mon Sep 17 00:00:00 2001 From: Eugene Odera Date: Tue, 18 Nov 2025 22:08:55 +0300 Subject: [PATCH 2/3] --- src/Components/Shared/src/PullFromJSDataStream.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Components/Shared/src/PullFromJSDataStream.cs b/src/Components/Shared/src/PullFromJSDataStream.cs index 7038e63650fb..67c9200a581d 100644 --- a/src/Components/Shared/src/PullFromJSDataStream.cs +++ b/src/Components/Shared/src/PullFromJSDataStream.cs @@ -106,10 +106,6 @@ private async ValueTask RequestDataFromJSAsync(int numBytesToRead) } _offset += bytesRead.Length; - if (_offset == _totalLength) - { - Dispose(true); - } return bytesRead; } From 750c9b38cfcb8b7261d5b559bc1e3c0bef9b1d78 Mon Sep 17 00:00:00 2001 From: Eugene Odera Date: Tue, 18 Nov 2025 22:14:49 +0300 Subject: [PATCH 3/3] --- src/Components/Shared/src/PullFromJSDataStream.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Components/Shared/src/PullFromJSDataStream.cs b/src/Components/Shared/src/PullFromJSDataStream.cs index 67c9200a581d..dba94cbf5249 100644 --- a/src/Components/Shared/src/PullFromJSDataStream.cs +++ b/src/Components/Shared/src/PullFromJSDataStream.cs @@ -116,6 +116,7 @@ protected override void Dispose(bool disposing) return; } _streamCts?.Cancel(); + _streamCts?.Dispose(); try { _ = _jsStreamReference?.DisposeAsync().Preserve(); @@ -136,6 +137,8 @@ public override async ValueTask DisposeAsync() } _streamCts?.Cancel(); + _streamCts?.Dispose(); + try { if (_jsStreamReference is not null)