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

Asp.Net Core API返回IAsyncEnumerable<T>未实现流式传输问题

处理Asp.Net Core API大数据流式传输的内存溢出问题

问题背景

搭建了一个Asp.Net Core API控制器,意图流式返回277000+条LocationDto数据,但实际响应被缓冲,客户端调用时抛出OutOfMemoryException。

API控制器代码

[HttpPost]
public async IAsyncEnumerable<LocationDto> GetLocations([FromBody] LocationReportQueryDto locationReportQuery, CancellationToken token = default)
{
    var result = await locationReportDataAccess.GetFilteredLocationsAsync(locationReportQuery, token);

    foreach (var location in result)
    {
        yield return location;
    }
}

客户端调用代码

public async Task<List<ItemLocDto>> GetFilteredLocationsAsync(LocationReportQueryDto locationReportQuery, CancellationToken token = default)
{
    var httpClient = httpClientFactory.CreateClient("DataAccess");
    var response = await httpClient.PostAsJsonAsync("/reports/LocationReport/GetFilteredLocations", locationReportQuery, token);
    response.EnsureSuccessStatusCode();
    var list = await response.Content.ReadFromJsonAsync<List<ItemLocDto>>(cancellationToken: token);
    if (list == null) throw new HttpRequestException("LocationReportDataAccess GetFilteredLocationsAsync HTTP Call - Response is null");
    return list;
}

抛出的异常信息

blazor.server.js:1 [2022-12-18T04:51:19.544Z] Error: System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at System.IO.MemoryStream.set_Capacity(Int32 value)
   at System.IO.MemoryStream.EnsureCapacity(Int32 value)
   at System.IO.MemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at System.Net.Http.HttpContent.LimitMemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at System.IO.MemoryStream.WriteAsync(ReadOnlyMemory`1 buffer, CancellationToken cancellationToken)
--- End of stack trace from previous location ---
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionResponseContent.SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken)
   at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at Portal.Infrastructure.DataAccess.Reports.LocationReportDataAccess.GetFilteredLocationsAsync(LocationReportQueryDto locationReportQuery, CancellationToken token) in C:\Users\LOFT\RiderProjects\Portal\src\Libraries\Portal.Infrastructure\DataAccess\Reports\LocationReportDataAccess.cs:line 30
   at Portal.Services.Reporting.LocationResultsService.GetResultsAsync(LocationInputConfig config, CancellationToken token) in C:\Users\LOFT\RiderProjects\Portal\src\Libraries\Portal.Services\Reporting\LocationResultsService.cs:line 137
   at Portal.Web.Pages.Reports.LocationList.OnGenerateClick() in C:\Users\LOFT\RiderProjects\Portal\src\Presentation\Portal.Web\Pages\Reports\LocationList.razor:line 679
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

核心原因分析

  1. 服务端未实现真正的流式读取:
    locationReportDataAccess.GetFilteredLocationsAsync返回的是List<LocationDto>,这意味着执行await时,所有27万+条数据已被一次性加载到服务器内存。后续的foreach + yield return只是遍历内存中的列表,并未实现数据库层面的流式传输,ASP.NET Core无法对已加载到内存的数据进行流式输出,最终仍会缓冲整个响应。

  2. 客户端一次性加载全量数据:
    客户端使用ReadFromJsonAsync<List<ItemLocDto>>会将整个HTTP响应内容全部读取到内存,再反序列化为List<ItemLocDto>,直接触发内存溢出。

解决方案

服务端调整:实现数据库级流式读取

修改locationReportDataAccess.GetFilteredLocationsAsync,使其返回IAsyncEnumerable<LocationDto>而非List<LocationDto>,确保从数据库查询时采用流式读取(以EF Core为例):

// 数据访问层方法修改示例
public async IAsyncEnumerable<LocationDto> GetFilteredLocationsAsync(LocationReportQueryDto query, [EnumeratorCancellation] CancellationToken token)
{
    // 避免使用ToListAsync(),改用AsAsyncEnumerable()实现流式查询
    var queryable = dbContext.Locations
        .Where(/* 你的过滤条件 */)
        .Select(l => new LocationDto { /* 映射字段 */ });

    await foreach (var location in queryable.AsAsyncEnumerable().WithCancellation(token))
    {
        yield return location;
    }
}

控制器方法可简化为直接返回异步枚举:

[HttpPost]
public async IAsyncEnumerable<LocationDto> GetLocations([FromBody] LocationReportQueryDto locationReportQuery, [EnumeratorCancellation] CancellationToken token = default)
{
    return locationReportDataAccess.GetFilteredLocationsAsync(locationReportQuery, token);
}

注意:ASP.NET Core 3.0+默认支持IAsyncEnumerable的流式JSON序列化(使用System.Text.Json),无需额外配置。

客户端调整:流式读取并处理响应

客户端不再一次性将所有数据加载为List,而是流式读取响应内容、逐条处理数据:

public async IAsyncEnumerable<ItemLocDto> GetFilteredLocationsAsync(LocationReportQueryDto locationReportQuery, [EnumeratorCancellation] CancellationToken token = default)
{
    var httpClient = httpClientFactory.CreateClient("DataAccess");
    var response = await httpClient.PostAsJsonAsync(
        "/reports/LocationReport/GetFilteredLocations", 
        locationReportQuery, 
        token,
        // 关键:指定ResponseHeadersRead,避免缓冲整个响应
        HttpCompletionOption.ResponseHeadersRead
    );
    response.EnsureSuccessStatusCode();

    await using var stream = await response.Content.ReadAsStreamAsync(token);
    // 流式反序列化逐条数据
    await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable<ItemLocDto>(stream, cancellationToken: token))
    {
        if (item != null)
        {
            yield return item;
        }
    }
}

调用该方法时,需用await foreach逐条处理,避免一次性存入List:

// 示例调用
await foreach (var loc in GetFilteredLocationsAsync(query, token))
{
    // 逐条处理数据,比如添加到分页集合、写入文件等
    ProcessLocation(loc);
}

关键注意点

  • 数据库查询必须避免一次性加载全量数据,使用异步枚举从数据库流式读取。
  • 客户端需指定HttpCompletionOption.ResponseHeadersRead,让HttpClient在获取响应头后就开始读取响应流,而非等待整个响应完成。
  • 若使用第三方ORM或数据访问库,需确认其支持异步流式读取,避免底层仍全量加载数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 12:15:38