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

基于IAsyncEnumerable的RESTful服务:OpenAPI文档与客户端代码生成问询

针对返回IAsyncEnumerable的REST服务的OpenAPI文档化与客户端实现

一、OpenAPI文档化方法

该服务通过流式分块传输返回数据(ASP.NET Core会自动为IAsyncEnumerable启用分块编码),OpenAPI文档需要明确标注流式特性,并定义返回项的具体结构:

openapi: 3.0.3
info:
  title: 姓氏搜索统计API
  version: 1.0.0
paths:
  /api/[你的控制器名]/yieldLastName:
    get:
      summary: 流式返回匹配姓氏的统计数据
      parameters:
        - name: inName
          in: query
          required: true
          schema:
            type: string
          description: 姓氏前缀(服务端自动添加%做模糊匹配)
      responses:
        '200':
          description: 流式返回的单个姓氏统计对象,每个分块对应一条数据
          content:
            application/json:
              schema:
                type: object
                properties:
                  LastName:
                    type: string
                    description: URL转义后的姓氏字符串
                  HitRate:
                    type: integer
                    description: 该姓氏在数据库中的匹配数量
          headers:
            Transfer-Encoding:
              schema:
                type: string
                example: chunked

核心标注要点:

  • 明确Transfer-Encoding: chunked响应头,突出流式传输特性
  • 响应schema定义单个返回对象的结构(而非数组),因为每个yield return会输出独立的JSON对象
  • 若服务额外配置为Server-Sent Events(SSE),可将content类型改为text/event-stream,并调整schema适配SSE的data:前缀格式

二、C#客户端代码示例

使用原生HttpClient处理分块流式响应,逐行解析每个返回的JSON对象:

using System.Net.Http.Json;
using System.Text.Json;

public class LastNameSearchClient
{
    private readonly HttpClient _httpClient;

    public LastNameSearchClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async IAsyncEnumerable<(string LastName, int HitRate)> FetchLastNameStatsAsync(string inName, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(inName))
            throw new ArgumentNullException(nameof(inName));

        var requestUrl = $"/api/[你的控制器名]/yieldLastName?inName={Uri.EscapeDataString(inName)}";
        // 仅读取响应头,不一次性加载整个响应体,适配流式场景
        using var response = await _httpClient.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
        response.EnsureSuccessStatusCode();

        using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
        using var reader = new StreamReader(stream);

        while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
        {
            var line = await reader.ReadLineAsync(cancellationToken);
            if (string.IsNullOrWhiteSpace(line))
                continue;

            var rawResult = JsonSerializer.Deserialize<dynamic>(line);
            // 解码URL转义的姓氏
            yield return (Uri.UnescapeDataString(rawResult.LastName), rawResult.HitRate);
        }
    }
}

// 调用示例
var client = new LastNameSearchClient(new HttpClient { BaseAddress = new Uri("https://你的API域名/") });
await foreach (var stats in client.FetchLastNameStatsAsync("Smith"))
{
    Console.WriteLine($"姓氏:{stats.LastName},匹配数:{stats.HitRate}");
}

关键注意事项:

  • 使用HttpCompletionOption.ResponseHeadersRead避免一次性加载大响应体,优化内存占用
  • 逐行读取流内容,适配ASP.NET Core默认的分块输出格式(每个yield结果占一行)
  • 必须对返回的LastName做URL解码,还原原始字符串

三、JavaScript客户端代码示例

使用浏览器Fetch API处理ReadableStream,逐块解析JSON数据:

async function* fetchLastNameStats(inName) {
    if (!inName) throw new Error('inName为必填参数');

    const url = `/api/[你的控制器名]/yieldLastName?inName=${encodeURIComponent(inName)}`;
    const response = await fetch(url);
    
    if (!response.ok) throw new Error(`请求失败:${response.status} ${response.statusText}`);
    
    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');
    let buffer = '';

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        // 按换行分割数据块,适配服务端的分块输出
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
            if (!line.trim()) continue;
            const data = JSON.parse(line);
            // 解码URL转义的姓氏
            data.LastName = decodeURIComponent(data.LastName);
            yield data;
        }
    }

    // 处理最后剩余的未分割内容
    if (buffer.trim()) {
        const data = JSON.parse(buffer);
        data.LastName = decodeURIComponent(data.LastName);
        yield data;
    }
}

// 调用示例
(async () => {
    try {
        const statsStream = fetchLastNameStats('Smith');
        for await (const stats of statsStream) {
            console.log(`姓氏:${stats.LastName},匹配数:${stats.HitRate}`);
        }
    } catch (err) {
        console.error('请求出错:', err);
    }
})();

核心处理逻辑:

  • 使用ReadableStream.getReader()流式读取响应内容,避免阻塞主线程
  • 通过缓冲区拼接、按换行分割的方式,正确解析每个独立的JSON对象
  • 对返回的LastName执行URL解码,还原原始字符串

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 06:23:11