基于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
相关产品推荐
相关产品推荐

