从内存中流式传输未知大小的 CSV 的正确方法

Correct way of streaming a CSV of unknown size from memory

我有一个 table 数据库,其中包含大约 20 列的超过 200 万条记录。用户可以查询数据库并限制返回的记录数,因此记录集可能为 1 到 200 万。

因为它是表格信息,所以我想以 CSV 格式发送数据。我正在使用 StreamWriter 将数据写入内存,文件完成后我将其作为 HttpResponseMessage 发送。我的代码在下面,只要我不 运行 内存不足,它就可以正常工作。有没有办法让我在处理文件时流式传输文件,以便使用的内存最少?

<HttpGet, Route("api/records/export")>
Public Function ExportRecords() As HttpResponseMessage

    Dim stream As New MemoryStream
    Dim writer As New StreamWriter(stream)
    writer.WriteLine("")

    ' Processing of data here
    writer.WriteLine("""Write Data to MemoryStream"")

    writer.Flush()
    stream.Position = 0

    Dim result As New HttpResponseMessage(HttpStatusCode.OK)
    result.Content = New StreamContent(stream)
    result.Content.Headers.ContentType = New Headers.MediaTypeHeaderValue("text/csv")
    result.Content.Headers.ContentDisposition = New Headers.ContentDispositionHeaderValue("attachment") _ 
        With {.FileName = "I" & Format(Date.Now, "yyMMdd") & ".csv"}

    Return result

End Function

我在 Whosebug 上阅读了对 Returning binary file from controller in ASP.NET Web API 等问题的回答,但这些都涉及从存储在磁盘上的文件而非内存中流式传输 Web 响应。

正如对我的问题的评论所建议的那样,我使用 PushStreamContent 将我的数据库内容以 CSV 格式流式传输到浏览器。我还使其异步以从导出中获得更多性能。

使用 PushStreamContent 将内容从服务器流式传输到客户端有一个重要的限制。由于 200 OK header 在流式传输任何内容之前首先发送,因此响应无法提前知道返回结果中是否会出现错误。如果在发送结果时出现问题,客户端只会在其端看到一般网络错误。由服务器记录任何错误,以便您可以检查服务器日志以查找特定错误。

这是我使用的 PushStreamContent 的代码(为简洁起见删除了错误检查)。

Dim result As HttpResponseMessage = New HttpResponseMessage(HttpStatusCode.OK) With {
    .Content = New PushStreamContent(Async Function(outStream, httpContent, context)
        Dim writer As StreamWriter = New StreamWriter(outStream)
        Await writer.WriteLineAsync("""Header 1"",""Header 2"",""Header 3""")
        For Each item In returnItems
            Await writer.WriteLineAsync("""" & item.Col1.ToString & """,=""" & item.Col2.ToString & """,=""" & item.Col3.ToString & """")
            Await writer.FlushAsync()
        Next
        outputStream.Close()
    End Function)}

result.Content.Headers.ContentType = New Headers.MediaTypeHeaderValue("text/csv")
result.Content.Headers.ContentDisposition = New _
  Headers.ContentDispositionHeaderValue("attachment") With {.FileName = "MyCSV.csv"}

Return result