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

ASP.NET 6 包装类场景下IAsyncEnumerable属性异步流式序列化方案问询

解决方案

你可以通过两种方式实现带元数据包装的异步流式序列化:

方法1:修改包装类属性类型 + 配置System.Text.Json序列化选项

  • 首先把DataSourceResult<T>的Data属性从IEnumerable<T>改为IAsyncEnumerable<T>,这样内置JSON序列化器可以直接识别异步枚举类型
  • ASP.NET Core 6+默认已经开启IAsyncEnumerable流式序列化支持,如果你之前自定义过JSON序列化配置,确保没有关闭该特性即可:
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // 该配置默认已开启,仅当你重置过Converters时需要显式保留
        // options.JsonSerializerOptions.Converters.Add(new JsonIAsyncEnumerableConverterFactory());
    });
  • 控制器直接返回包装类即可,无需缓冲全量数据:
[HttpGet]
public DataSourceResult<AspNetUser> GetData()
{
    return new DataSourceResult<AspNetUser>
    {
        Data = _context.AspNetUsers.AsAsyncEnumerable(),
        PageIndex = 1,
        PageSize = 100,
        TotalCount = _context.AspNetUsers.Count() // TotalCount单独查询,不会触发数据集缓冲
    };
}

注意:TotalCount这类统计类元数据需要提前单独查询,不能从异步枚举流中统计,否则会触发全量枚举缓冲

方法2:自定义结果执行器(适合不想修改原有包装类定义的场景)

如果老项目已经大量使用IEnumerable<T>类型的Data属性,不想改动现有类定义,可以通过自定义IActionResult实现手动流式写入:

  1. 实现自定义流式结果类
public class StreamedDataSourceResult<T> : IActionResult
{
    private readonly DataSourceResult<T> _result;
    public StreamedDataSourceResult(DataSourceResult<T> result)
    {
        _result = result;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        var encoder = context.HttpContext.RequestServices.GetRequiredService<JavaScriptEncoder>();
        using var writer = new Utf8JsonWriter(response.BodyWriter, new JsonWriterOptions
        {
            Encoder = encoder
        });

        writer.WriteStartObject();
        // 先写入所有元数据字段
        writer.WriteNumber(nameof(DataSourceResult<T>.PageIndex), _result.PageIndex);
        writer.WriteNumber(nameof(DataSourceResult<T>.PageSize), _result.PageSize);
        writer.WriteNumber(nameof(DataSourceResult<T>.TotalCount), _result.TotalCount);
        // 开始写入Data数组
        writer.WritePropertyName(nameof(DataSourceResult<T>.Data));
        writer.WriteStartArray();
        // 异步枚举逐个写入元素,全程无需缓冲全量数据集
        await foreach (var item in _result.Data as IAsyncEnumerable<T> ?? _result.Data.ToAsyncEnumerable())
        {
            JsonSerializer.Serialize(writer, item);
            await writer.FlushAsync();
            // 可选:增加缓冲阈值减少IO次数,平衡内存占用和传输性能
        }
        writer.WriteEndArray();
        writer.WriteEndObject();
        await writer.FlushAsync();
    }
}
  1. 控制器返回自定义结果即可
[HttpGet]
public IActionResult GetData()
{
    var result = new DataSourceResult<AspNetUser>
    {
        Data = _context.AspNetUsers.AsAsyncEnumerable(),
        PageIndex = 1,
        PageSize = 100,
        TotalCount = _context.AspNetUsers.Count()
    };
    return new StreamedDataSourceResult<AspNetUser>(result);
}

注意事项

  • 不要在返回前对IAsyncEnumerable执行任何会触发全量枚举的操作(比如ToListAsync、Count()、同步重载的LINQ操作),否则会失去流式传输能力
  • 使用EF Core作为数据源时,不要提前释放DbContext,否则枚举数据集时会报错
  • 流式传输下无法设置Content-Length响应头,客户端会收到Transfer-Encoding: chunked格式的响应,符合HTTP标准,绝大多数客户端都能正常处理

内容的提问来源于stack exchange,提问作者Rafi Henig

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 17:27:04