You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 05:24:05