自定义TextOutputFormatter为何无法实现IAsyncEnumerable流式输出?
问题
我希望从WebAPI端点流式返回自定义格式文本,文本应在yield生成后立即逐步显示。
我编写了如下端点:
[HttpGet] public async IAsyncEnumerable<string> StreamAnswer(string message) { for (int i = 0; i < 3; i++) { await Task.Delay(3000); yield return "test" + i; } }
它会返回JSON格式数组["test0","test1","test2"],且数组元素每3秒逐步显示,符合预期。
我希望让端点返回拼接后的文本test0test1test2,因此修改端点添加[Produces("text/plain")]特性,并创建了PlainTextOutputFormatter,在Program.cs中注册该格式化器。结果文本格式正确,但未实现流式输出,而是等待9秒后一次性返回所有数据。请问是什么原因阻止了流式传输?
解决方案
问题出在你自定义的PlainTextOutputFormatter上——默认情况下多数文本格式化器会先将整个IAsyncEnumerable枚举完成,把所有内容缓存后再一次性输出,没有适配流式场景。
要实现文本流式输出,你需要修改格式化器,让它能逐个处理IAsyncEnumerable的元素,每生成一个就立即写入响应流,同时确保不提前设置响应的Content-Length(流式内容长度不确定,ASP.NET Core会自动用Transfer-Encoding: chunked来处理)。
具体要做这几点:
- 在格式化器的
CanWriteResult方法中,明确支持IAsyncEnumerable<string>类型 - 在
WriteAsync方法里,遍历IAsyncEnumerable的每个元素,逐个写入响应流,写完一个就调用FlushAsync强制推送内容到客户端 - 避免手动设置
Content-Length响应头,让框架自动处理分块传输
修改后的格式化器示例:
public class StreamingPlainTextOutputFormatter : TextOutputFormatter { public StreamingPlainTextOutputFormatter() { SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain")); SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); } protected override bool CanWriteType(Type type) { // 同时支持流式IAsyncEnumerable<string>和普通string return typeof(IAsyncEnumerable<string>).IsAssignableFrom(type) || typeof(string).IsAssignableFrom(type); } public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { var response = context.HttpContext.Response; var asyncEnumerable = context.Object as IAsyncEnumerable<string>; if (asyncEnumerable != null) { // 逐个处理流式元素,写入后立即刷新推送 await foreach (var item in asyncEnumerable) { await response.WriteAsync(item, selectedEncoding); await response.Body.FlushAsync(); } return; } // 处理普通字符串的常规情况 var text = context.Object as string; if (text != null) { await response.WriteAsync(text, selectedEncoding); } } }
在Program.cs中注册这个格式化器:
builder.Services.AddControllers(options => { // 插入到格式化器列表最前面,确保优先使用 options.OutputFormatters.Insert(0, new StreamingPlainTextOutputFormatter()); });
端点保持特性配置:
[HttpGet] [Produces("text/plain")] public async IAsyncEnumerable<string> StreamAnswer(string message) { for (int i = 0; i < 3; i++) { await Task.Delay(3000); yield return "test" + i; } }
这样修改后,每个testX会每隔3秒推送到客户端,最终拼接成test0test1test2,实现流式输出。
内容的提问来源于stack exchange,提问作者Piotrek
相关产品推荐
相关产品推荐

