Is ASP.NET MVC's FileStreamResult less efficie

2019-03-16 03:24发布

问题:

First of all, I love ASP.NET MVC. This question is not a criticism of it. Rather, I want to confirm what I think I see and make sure I haven't missed something. Bear with me ... I can't get to the question without providing a little bit of context.

The question has to do with returning data in a Stream in response to an HTTP Post. In the olden days before ASP.NET MVC, you could do this by piping your data directly into the Response stream. For example, you might do something like this:

someObjectThatDumpsOutputToWhateverStreamYouHandIt.WriteTo(Response.OutputStream); 

Notice a key aspect of this code: I have not implemented ANY backing store. I did not have to stream the info to a byte array, or dump it into a temporary file. Rather, ASP.NET had already set up a Stream for the purpose of communicating the response back to the browser, and I have dumped the output I want directly into that Stream. This is more efficient in terms of CPU, memory, and execution time than copying all the data to some temporary location and then moving it into the Response stream.

However, it is not very friendly to unit tests. In fact, you can still do that kind of code in ASP.NET MVC, but the custom would be to avoid that. Rather, ASP.NET MVC would encourage you to return an ActionResult. The ActionResult is an ASP.NET MVC concept that exists mostly to be unit test friendly. It allows you to "declare" what you want done. A unit test can exercise a Controller action and confirm that it gets the expected ActionResult. That unit test can run outside a browser.

ASP.NET MVC offers a kind of ActionResult for returning Streams of data. It is called FileStreamResult. Don't let the word "File" in the name fool you. It is about returning a Stream of data, exactly as we are talking about above.

However, here's the problem, and the basis of my question: If you make your Controller method return a FileStreamResult, and you hand it a Stream that you want to return, then it appears there is no longer any way for you to dump the data directly to the Response stream. Now, it appears you are forced to make your own Stream with a backing store (such as memory or a file), dump your data into it, and then hand that Stream to the FileStreamResult that you return.

So it appears I have to do something like this (intentionally omitting dispose / using / etc.):

MemoryStream myIntermediateStream = new MemoryStream();
someObjectThatDumpsOutputToWhateverStreamYouHandIt.WriteTo(myIntermediateStream ); 

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

Notice that myIntermediateStream causes the contents of the large stream of data to be stored in memory temporarily, so that the FileStreamResult can later on copy it again into the Response output stream.

So here's my question: Have I overlooked something, or is it accurate to say that using FileStreamResult forces you to have an intermediate storage location that you would not be forced to have if you were to write directly to the Response's output stream?

Thanks.

回答1:

I believe it would be more accurate to say that, assuming someObjectThatDumpsOutputToWhateverStreamYouHandIt does not inherit from System.IO.Stream, that using FileStreamResult forces you to either

a) implement a wrapper for someObjectThatDumpsOutputToWhateverStreamYouHandIt that does inherit from System.IO.Stream, or

b) use a temporary MemoryStream instance.

So, it is not necessarily less efficient, but does seem to require a bit more work for the purpose of returning a value.

Edit by Question Asker: I am choosing this question as the Accepted answer. Both responses were very helpful. I have to choose one, and this one pointed me to Phil Haack's DelegatingActionResult, which is exactly what I needed.



回答2:

I think the main idea behind FileStreamResult is that you use it when you already have a stream. If you have to create a temporary stream in order to use it, then there is no point; that's why there is also a FilePathResult and FileContentResult.

There are several instances when you might already have a stream:

  • Data stored in a SQL 2008 FILESTREAM column;
  • Data forwarded directly from another endpoint (NetworkStream);
  • A GzipStream or DeflateStream for sending something compressed;
  • Sending from a named pipe (PipeStream);
  • ...and so on.

The main use case for FileStreamResult (as I understand it) is when you have a pre-existing stream that you would need to read from and then write back to the response; instead of having to do the reading and writing and figuring out the buffering yourself, the FileStreamResult handles it for you.

So the short answer is no, FileStreamResult doesn't necessarily force you to have an "intermediate" storage location, if the stream is your data source. I wouldn't bother with FileStreamResult if the data is already in memory or on disk somewhere, ready and waiting to be written directly to the response stream.


I'd just like to add another clarification: The issue with this library is that it inverts the functionality you really need for FileStreamResult. Instead of handing you a stream that you can read from, it expects you to provide the stream so it can write to it. This is a common pattern, mind you, but it is not very well suited to the task.

If the stream contains a lot of data and you don't want to chew up memory with it, you should be able to invert the stream yourself using the PipeStream derivatives. Create either a named or anonymous pipe, create a server stream (which you can initialize with as small a buffer size as you want) and feed it to the library, then create a client stream on the same pipe and feed it to the FileStreamResult.

This gives you the unit-testability you want and lets you control exactly how much memory the process is allowed to use. It also has the added advantage of being able to produce the data asynchronously, which you might want if you're trying to chuck out gigabytes of data.