FileStreamResult 下载或导出文件
FileStreamResult的一个常用构造函数:
public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName);
下面通过循序渐进的方式 讲述 项目中的实际应用。
1. using块中的流
按照良好的编程习惯,将stream放在using块中,以确保它被释放。结果:
fail : An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Cannot access a closed Stream.
原因在于 using块一执行完stream就被关闭了,view无法读取已关闭的流。
但,流总不能不关闭。难道,有谁替我们做了这些?
是的,FileStreamResult 自动做了这些,下面是反编译的源码:
protected override void WriteFile(HttpResponseBase response) { // grab chunks of data and write to the output stream Stream outputStream = response.OutputStream; using (FileStream) { byte[] buffer = new byte[_bufferSize]; while (true) { int bytesRead = FileStream.Read(buffer, 0, _bufferSize); if (bytesRead == 0) { // no more data break; } outputStream.Write(buffer, 0, bytesRead); } } }
删掉using后的代码如下:
运行成功,文件保存至本地。
2. 注意流的当前位置
有些时候我们所调用的组件恰好收集了流数据,要利用这个流。
下面的例子 通过 EPPlus组件创建Excel数据(须引用 EPPlus.dll 并 using OfficeOpenXml;)
上述代码将ExcelPackage的数据流直接作为即将被下载的文件的数据流而输出。看起来很完美,结果
fail : An unhandled exception was thrown by the application.
System.InvalidOperationException: Response Content-Length mismatch: too few bytes written <0 of 2546>
原因在于stream的当前位置在末尾。
常见的相同状况的还有 Stream.CopyTo() 。微软对此有说明:
添加一行代码 stream.Position = 0 以重置流的当前位置至开始。
运行成功,保存至本地的文件如下:
3. 压缩流
上述2中的例子,文件很小,只简单写了两个单元格。如果文件大,则需要压缩,供客户端下载压缩包。
有些同学真的是先创建文件保存至磁盘某路径A,然后压缩那个文件至路径B,最终将B返回。
还没完,因为还得善后:清理掉文件A及压缩包B。
我倾向于:不存磁盘,直接利用流。其实在上面的第2小节中的示例已经体现。
下面的示例须添加 using System.IO.Compression
用于创建压缩包的流stream,即是将来要返回的流。
向压缩包中添加的entry并写入数据, 用的entryStream,也是流。这个entry是一个Excel文件,恰好由EPPlus组件的流填充。
换一种说法:
ExcelPackage的流,构成 System.IO.Compression某个ZipArchiveEntry的流,一个或多个ZipArchiveEntry流 构成了ZipArchive流,而后 ZipArchive流构成FileStreamResult中的流 供返回。
全程都是流之间的流转,没有写磁盘,用不着去清理什么过程文件。
美美的运行一下:oops, fail : Cannot access a closed Stream.
我并没有将 MemoryStream stream = new MemoryStream() 放入using块啊,是哪里关闭了它?
查帮助文档,微软说:ZipArchive.Dispose() 此方法完成写入存档并释放由该ZipArchive对象使用的所有资源。
那就不把 ZipArchive archive = ... 放入using块呗,这样就不调用 ZipArchive.Dispose() 就不会关闭(释放)流了。
新的代码如下:
运行成功,Happy
解压看看:
重点字眼“末端”,说明 不是写excel数据的问题。
结合“ZipArchive.Dispose() 此方法完成写入存档并释放” ,第一感觉是 没有完成存档,之前关注点都放在释放上了。
仔细阅读微软的说明:
就是说,必须得调用ZipArchive.Dispose() 以完成存档。但是,leaveOpen=true 才能保证流不关闭。
这次是真的运行成功了: