ASP.NET MVC的FileStreamResult比直接写入Response Output Stream不够高效吗,还是我漏掉了什么?

10

首先,我非常喜欢ASP.NET MVC。这个问题并不是在批评它,而是想确认自己所看到的并确保没有遗漏什么重要信息。请耐心听我讲述一下背景...

这个问题涉及到在HTTP Post请求中返回数据流。在ASP.NET MVC之前的旧时代,你可以通过将数据直接传送到响应流中来完成此操作。例如,你可能会像这样做:

someObjectThatDumpsOutputToWhateverStreamYouHandIt.WriteTo(Response.OutputStream); 
请注意这段代码的一个关键方面:我没有实现任何后备存储。我不必将信息流式传输到字节数组中,也不必将其转储到临时文件中。相反,ASP.NET已经为与浏览器通信返回响应而设置了一个流,并且我直接将所需输出转储到该流中。与将所有数据复制到某个临时位置然后将其移动到响应流中相比,这样更有效率,更节省CPU、内存和执行时间。
然而,这对于单元测试来说并不太友好。事实上,在ASP.NET MVC中仍然可以编写这种代码,但惯例是避免这样做。相反,ASP.NET MVC会鼓励你返回ActionResult。ActionResult是一个ASP.NET MVC概念,主要存在于单元测试友好性。它允许您“声明”您想要做什么。单元测试可以运行控制器操作并确认它获取了预期的ActionResult。该单元测试可以在浏览器之外运行。
ASP.NET MVC提供了一种返回数据流的ActionResult。它称为FileStreamResult。不要被名称中的“File”一词所迷惑。它是关于返回数据流的,就像我们上面所说的那样。
然而,这里有一个问题,也是我的问题的基础:如果您使Controller方法返回一个FileStreamResult,并将其设置为要返回的Stream,则似乎再也没有办法直接将数据转储到响应流中了。现在,您似乎被迫使用带有后备存储(例如内存或文件)的自己的Stream,将数据转储到其中,然后将该Stream交给您返回的FileStreamResult。
所以,看起来我必须像这样做(故意省略dispose/using等):
MemoryStream myIntermediateStream = new MemoryStream();
someObjectThatDumpsOutputToWhateverStreamYouHandIt.WriteTo(myIntermediateStream ); 

return new FileStreamResult(myIntermediateStream, "application/pdf");

注意,myIntermediateStream会导致大量数据的内容被临时存储在内存中,以便FileStreamResult稍后可以将其再次复制到Response输出流中。

因此,我的问题是:我是否忽略了什么,或者说使用FileStreamResult强制您拥有一个中间存储位置,如果直接写入Response的输出流,则不需要这样做?

谢谢。

2个回答

10
我认为FileStreamResult的主要思想是当你已经拥有一个流时才使用它。如果必须创建临时流来使用它,那就没有意义;这就是为什么还有FilePathResultFileContentResult的原因。
以下是几种可能已经存在流的情况:
  • 存储在SQL 2008 FILESTREAM列中的数据;
  • 直接从另一个端点(NetworkStream)转发的数据;
  • 用于发送压缩内容的GzipStreamDeflateStream
  • 从命名管道(PipeStream)发送;
  • 等等。
FileStreamResult的主要用例(据我所知)是当您有一个预先存在的流需要从中读取,然后将其写回响应时。相比于自己进行读写并解决缓冲问题,FileStreamResult可以为您处理它。
总之,如果数据已经在内存或磁盘上准备好直接写入响应流,则不需要使用FileStreamResult。因此,简而言之,FileStreamResult并不一定会强制您拥有“中间”存储位置,如果流是您的数据源。
我想补充一下:这个库的问题在于它颠倒了你实际需要的FileStreamResult功能。相反地,它期望您提供流以便它可以写入它,这是一个常见的模式,但并不非常适合这个任务。如果数据流包含大量数据,您不希望用掉过多的内存,那么您应该能够使用PipeStream派生类自己反转流。创建命名管道或匿名管道,创建服务器流(可以使用尽可能小的缓冲区大小进行初始化)并将其馈送到库中,然后在同一管道上创建客户端流并将其馈送到FileStreamResult中。
这样可以使单元测试成为可能,并允许您精确控制进程允许使用的内存量。此外,它还具有异步生成数据的优点,如果您试图处理吉字节级别的数据,可能会用到这个功能。

你给了我希望。我有一个库,其中有一个方法要求我将一个空流传递给它。然后它会填充该流。我绝对不想使用中间存储位置。但是,如果我使用FileStreamResult,似乎我被迫使用中间存储。你是说你看到了某种解决方法吗? - Charlie Flowers
@Charlie:如果你信任库直接写入响应流,那么我会直接给它。这并不是FileStreamResult的主要用例;当你只是从已提供给你的流中读取时,该结果类型更好,而不是向你创建的空流写入。这一切都取决于上下文;FileStreamResult有其用途,但听起来这不是其中之一。 - Aaronaught
好的,这很有道理。我同意它有很好的用途。得到一些确认我没有遗漏什么是非常有帮助的,所以谢谢。 - Charlie Flowers
@Charlie:我添加了几个段落,可能会对你有所帮助。你可以使用管道来创建一个“中间”存储位置,它不需要将整个 blob 存储在内存中,它将以你选择的任何大小进行缓冲。或者你可以创建自己的伪管道(流缓冲区),因为你没有进行 IPC,但如果有意义的话,我总是更喜欢使用内置的框架类。 - Aaronaught

5
我认为更准确的说法是,假设someObjectThatDumpsOutputToWhateverStreamYouHandIt不继承自System.IO.Stream,那么使用FileStreamResult将会强制你去要么 实现一个继承自System.IO.Stream的someObjectThatDumpsOutputToWhateverStreamYouHandIt包装器,要么使用一个临时的MemoryStream实例。 因此,这并不一定不高效,但似乎需要更多的工作来返回一个值。 问题提出者编辑: 我选择这个问题作为采纳的答案。两个回答都非常有用。我必须选择一个,而这个指向了Phil Haack的DelegatingActionResult,这正是我所需要的。

嗯,在这种情况下,我正在调用一个第三方库类上的方法,该方法接受流作为方法参数并写入该流。因此,它不会从Stream继承(除非我误解了您的意思)。使用哪个第三方库都无所谓...对于生成大型流状结果的库来说,将流作为参数并直接写入其中是一种相当常见的模式。但是,继承自Stream是否有助于解决问题呢? - Charlie Flowers
继承自Stream的重点在于,您可以将流直接传递给FileStreamResult。如果第三方库支持分块写入,您可以在该对象周围实现一个流包装器,然后将其包装在FileStreamResult中返回。如果该库不支持分块写入,则可以尝试创建自己的ActionResult子类。 - Joseph
是的,创建自己的ActionResult子类可能是有意义的。这个特定的库在返回之前关闭了流。我只需要让我的ActionResult这样做,而且我可以使它声明性的,从而更容易进行单元测试。不过,实际上我们需要一个“拉”接口,或者像Haskell一样的惰性评估。嗯,加一分,谢谢。 - Charlie Flowers
2
不用谢。我只是四处寻找,发现了这个链接:http://haacked.com/archive/2008/05/10/writing-a-custom-file-download-action-result-for-asp.net-mvc.aspx 。以防你还没有看到它。此外,@Aaronaught似乎有另一个解决方案,值得一看。我之前从未听说过PipeStream。 - Joseph
是的,那非常有帮助。我已经开始考虑使用基于 lambda 的 ActionResult,以便推迟整个操作直到以后。不奇怪 Phil 掌握了这个技巧 :) - Charlie Flowers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接