如果您的解决方案有效,则最简单的方法就是保持不变。
另一方面,我对您使用DoTNetZip库有一些评论。
首先,您的代码有点误导性。在这个部分:
byte[] byteInfo = workStream.ToArray()
zip.Save(workStream)
workStream.Write(byteInfo, 0, byteInfo.Length)
workStream.Position = 0
…你正在将workStream读入一个数组中。但此时,您尚未向workStream写入任何内容,因此该数组为空,长度为零。然后,您将zip保存到workstream中。然后,您将零长度的数组写入同一workstream中。这是无操作(NO-OP)。最后,您重置了位置。
您可以将所有这些都替换为:
zip.Save(workStream)
workStream.Position = 0
这不是DotNetZip本身的问题,而是您对流操作的误解。
其次,您不必要地分配了临时缓冲区(MemoryStreams)。将MemoryStream视为只是一个带有Stream包装器的字节数组,以支持Write()、Read()、Seek()等操作。实际上,您的代码正在将数据写入该临时缓冲区,然后告诉DotNetZip从临时缓冲区中读取数据到其自己的压缩缓冲区。您不需要那个中间缓冲区。它按您所做的方式工作,但可能效率更高。
DotNetZip有一个AddEntry()重载,接受一个writer委托。该委托是DotNetZip调用的函数,告诉您的应用程序将条目内容写入zip档案。您的代码编写未压缩的字节,而DotNetZip压缩并将其写入输出流。
在该writer委托中,你的代码直接写入DotNetZip流——通过DotNetZip传递给委托的流。没有中介缓冲区。效率高。
请记住闭包规则。如果您在for循环中调用此writer委托,您需要一种方法来检索与zipentry相对应的“bla”。该委托直到调用zip.Save()才会被执行!因此,您不能依赖于循环中'bla'的值。
public FileStreamResult DownloadPDF()
{
MemoryStream workStream = new MemoryStream();
using(var zip = new ZipFile())
{
foreach(Bla bla in Blas)
{
zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
var thisBla = GetBlaFromName(name);
Document document = new Document();
PdfWriter.GetInstance(document, stream).CloseStream = false;
document.Open();
// write PDF Content for thisBla into stream/PdfWriter
document.Close();
});
}
zip.Save(workStream);
}
workStream.Position = 0;
FileStreamResult fileResult = new FileStreamResult(workStream, System.Net.Mime.MediaTypeNames.Application.Zip);
fileResult.FileDownloadName = "MultiplePDFs.zip";
return fileResult;
}
最后,我不太喜欢你从MemoryStream
中创建一个FileStreamResult
。问题是整个zip文件都保存在内存中,这可能会对内存使用造成很大的压力。如果您的zip文件很大,您的代码将保留所有内容在内存中。
我不了解MVC3模型是否有助于解决此问题。如果没有,可以使用匿名管道来反转流的方向,并消除了需要在内存中保存所有压缩数据的需求。
这就是我的意思:创建一个FileStreamResult
需要提供一个可读的流。如果使用MemoryStream,则需要先写入它,然后将其定位回位置0,再将其传递给FileStreamResult
构造函数。这意味着该zip文件的所有内容必须在某个时刻连续地保存在内存中。
假设您可以为FileStreamResult
构造函数提供一个可读的流,使读者能够在您写入数据的正好那一刻进行读取。这就是匿名管道流所做的事情。它允许您的代码使用可写流,而MVC代码获得可读流。
以下是示例代码:
static Stream GetPipedStream(Action<Stream> writeAction)
{
AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream();
ThreadPool.QueueUserWorkItem(s =>
{
using (pipeServer)
{
writeAction(pipeServer);
pipeServer.WaitForPipeDrain();
}
});
return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString());
}
public FileStreamResult DownloadPDF()
{
var readable =
GetPipedStream(output => {
using(var zip = new ZipFile())
{
foreach(Bla bla in Blas)
{
zip.AddEntry(bla.filename + ".pdf", (name,stream) => {
var thisBla = GetBlaFromName(name);
Document document = new Document();
PdfWriter.GetInstance(document, stream).CloseStream = false;
document.Open();
// write PDF Content for thisBla to PdfWriter
document.Close();
});
}
zip.Save(output);
}
});
var fileResult = new FileStreamResult(readable, System.Net.Mime.MediaTypeNames.Application.Zip);
fileResult.FileDownloadName = "MultiplePDFs.zip";
return fileResult;
}
我没有尝试过这个方法,但它应该可以工作。相比你写的方式,它更加节省内存。缺点是使用了命名管道和多个匿名函数,使其变得更加复杂。
只有当zip内容超过1MB时才有意义。如果您的zip文件小于1MB,则可以使用我上面展示的第一种方式。
补充说明
为什么您不能依赖匿名方法中bla
的值?
有两个关键点。首先,foreach循环定义了一个名为bla
的变量,每次
通过循环时都会取一个不同的值。这似乎很明显,但值得明确说明。
其次,匿名方法被作为参数传递给ZipFile.AddEntry()
方法,而它不会在foreach循环运行时运行。
事实上,匿名方法会在每个条目被添加时重复调用,即在
ZipFile.Save()
执行的时候。如果在匿名方法中引用bla
,
它会得到最后一个分配给bla
的值,因为那是ZipFile.Save()
运行时bla
所保存的值。
这是延迟执行导致了困难。
你想要的是foreach循环中每个不同的bla
值都可以在匿名函数被调用时访问 -
稍后,在foreach循环之外。您可以使用一个实用方法(GetBlaForName()
)来完成此操作,如我上面所示。
您也可以使用附加闭包来完成此操作,就像这样:
Action<String,Stream> GetEntryWriter(Bla bla)
{
return new Action<String,Stream>((name,stream) => {
Document document = new Document();
PdfWriter.GetInstance(document, stream).CloseStream = false;
document.Open();
// write PDF Content for bla to PdfWriter
document.Close();
};
}
foreach(var bla in Blas)
{
zip.AddEntry(bla.filename + ".pdf", GetEntryWriter(bla));
}
GetEntryWriter
返回一个方法 - 实际上是一个操作(Action),它只是一个有类型的方法。每次循环时,都会创建一个新的该操作(Action)的实例,并引用不同的bla值。该操作(Action)直到ZipFile.Save()
时才被调用。