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实现手动流式写入:
- 实现自定义流式结果类
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(); } }
- 控制器返回自定义结果即可
[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
相关产品推荐
相关产品推荐

