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

自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 15:32:04