gRPC服务端流能否向Blazor WASM返回Stream而非IAsyncEnumerable<T>?
解决方案
方案1:实现自定义适配Stream,直接包装IAsyncEnumerable<byte[]>
这是兼容性最高的方案,只需要自己实现一个只读Stream的子类,内部消费IAsyncEnumerable<byte[]>的迭代器即可,无需修改gRPC服务端定义。
public class AsyncEnumerableStream : Stream { private readonly IAsyncEnumerator<byte[]> _enumerator; private byte[]? _currentBlock; private int _currentPosition; private bool _isCompleted; public AsyncEnumerableStream(IAsyncEnumerable<byte[]> source) { _enumerator = source.GetAsyncEnumerator(); } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) { if (_isCompleted) return 0; // 没有当前块或者当前块已经读完,尝试读取下一个块 while (_currentBlock == null || _currentPosition >= _currentBlock.Length) { var hasNext = await _enumerator.MoveNextAsync(cancellationToken); if (!hasNext) { _isCompleted = true; return 0; } _currentBlock = _enumerator.Current; _currentPosition = 0; } // 复制当前块的数据到输出缓冲区 var bytesToCopy = Math.Min(_currentBlock.Length - _currentPosition, buffer.Length); _currentBlock.AsSpan(_currentPosition, bytesToCopy).CopyTo(buffer.Span); _currentPosition += bytesToCopy; return bytesToCopy; } // 同步Read方法按需实现即可,Blazor场景下基本只用异步读取 public override int Read(byte[] buffer, int offset, int count) { return ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); } public override async ValueTask DisposeAsync() { await _enumerator.DisposeAsync(); await base.DisposeAsync(); } // 其余未用到的Stream方法直接抛出不支持异常即可 public override void Flush() => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); }
方案2:直接从gRPC客户端获取原生Stream(可选)
如果你使用的是Grpc.Net.Client客户端,且不需要对返回的字节块做额外处理,可以直接通过gRPC调用的响应消息获取底层流,跳过IAsyncEnumerable<byte[]>的转换步骤。不过这种方式依赖gRPC客户端的具体实现,适配性略低于方案1。
使用示例
Blazor WASM页面调用参考:
// 调用gRPC服务获取流式返回 var fileResponse = _grpcClient.DownloadFileAsync(new DownloadRequest { FileId = targetFileId }); // 包装为适配Stream await using var adaptedStream = new AsyncEnumerableStream(fileResponse.ResponseStream.ReadAllAsync()); // 构造DotNetStreamReference传入JSInterop var streamRef = new DotNetStreamReference(adaptedStream); // 调用自定义JS下载方法 await _jsRuntime.InvokeVoidAsync("downloadFileFromStream", saveFileName, streamRef);
注意事项
- 需提前在gRPC客户端配置中调大
MaxReceiveMessageSize参数,避免大文件下载时被截断 - 自定义Stream需要正确实现DisposeAsync方法,避免迭代器资源泄漏
- 可以根据实际业务的文件块大小调整内部缓冲区逻辑,进一步优化读取性能
内容的提问来源于stack exchange,提问作者Claudio
相关产品推荐
相关产品推荐

