using DotNetZip in a webservice

Nov 8, 2010 at 9:09 PM

I'm a bit of a newbie. Not quite sure what I'm doing...

I have a webservice which is passed a string of image filenames I want to zip. I am using the below code which works fine in a traditional aspx page, but doesn't work in my webservice. There are no error messages or anything, but the zip file doesn't start downloading like it would if this code was in a .aspx page. Perhaps its something to do with response.outputStream in the webservice?

 

 

	HttpContext.Current.Response.Clear()
        HttpContext.Current.Response.BufferOutput = False

        Dim c As System.Web.HttpContext = System.Web.HttpContext.Current
        Dim archiveName As String = String.Format("archive-{0}.zip", DateTime.Now.ToString("yyyy-MMM-dd-HHmmss"))
        HttpContext.Current.Response.ContentType = "application/zip"
        HttpContext.Current.Response.AddHeader("content-disposition", "filename=" + archiveName)

        Using zip As New ZipFile()

            ' filesToInclude is a string[] or List
            'zip.AddFiles(filesToInclude, "files")
            zip.AddFile("example_file.jpg", "")

            zip.Save(HttpContext.Current.Response.OutputStream)
        End Using
        HttpContext.Current.Response.Close()

 

Has anyone got dotnetzip working in a webservice? Or is there a work around I can use (I need to use the webservice as a starting point as that is where my list of images comes from).

 

Also, is using response.outputStream ok if many users will be downloading several large zip files at a time? Does it directly consume system memory?

 

Thanks for your help!

 

Coordinator
Nov 9, 2010 at 4:53 PM

Yes, DotNetZip works in a webservice.  The way you're trying to do it is not right. 

The HttpContext is something that is present in a web page.  In the original .NET Web services runtime, aka ASMX, HttpContext was present and available inside the code for the webservice. But, using HttpContext is a pretty sketchy thing to do, especially now that WCF is the preferred web services (and communications) runtime for .NET these days.  Within WCF, there is no HttpContext, because WCF is not tightly bound to http.

If you're using WCF, then you're going to want to stream the zip content out.  Check this page for some background on how to do that: http://msdn.microsoft.com/en-us/library/ms731913.aspx

But... you're in luck!  Your question has been asked before, and I put some effort into working out an efficient solution with the previous person.  We had a back-and-forth, that you can read here. The solution involves the use of the ZipOutputStream from DotNetZip, and anonymous pipes, via the AnonymousPipeClientStream  and AnonymousPipeServerStream  classes that were added to .NET in v3.5.

To sum it up, you can define a WCF interface like this: 

using System.ServiceModel; 

[ServiceContract] 
public interface IImageService 
{
  [OperationContract]
  public Stream GetFileZip(string[] filenames);

}

(for more on defining interfaces in WCF, see this page on msdn)

The code implementing the interface is then, like this:

public Stream GetFileZip(string[] filenames)
{
    return GetPipedStream(s =>
        {
            int n;
            byte[] bytes = new byte[1024]; // any size will do
            using (ZipOutputStream zos = new ZipOutputStream(s, true))
            {
                for (int i = 0; i < filenames.Length; i++)
                {
                    zos.PutNextEntry(filenames[i]);  // the i'th filename
                    using (FileStream fs = File.OpenRead(filename[i]))
                    {
                        while((n = fs.Read(bytes,0,bytes.Length))>0)
                        {
                            zos.Write(bytes, 0, n);
                        }
                    }
                }
            }
        });
}

Obviously you'd need to put logic in there to detect and handle error conditions - such as when the specified filename does not exist.  I left that stuff out for clarity.

The GetPipedStream is a utility method defined this way:

static Stream GetPipedStream(Action<Stream> writeAction) 
{ 
    AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(); 
    ThreadPool.QueueUserWorkItem(s => 
    { 
        using (pipeServer) 
        { 
            writeAction(pipeServer); 
            pipeServer.WaitForPipeDrain(); 
        } 
    }); 
    return new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 
} 

Don't worry about the code for GetPipedStream - it's boilerplate and you don't need to understand it too much, in order to use it.

What you get with this is a WCF service that zips up the files on a given list, and streams the resulting zipfile back to the requester. It does not create a temporary zip file in the server filesystem, instead it just streams out the zip to the requester. It should be very memory efficient and very fast.  You can use it over any WCF transport channel that supports streams - http, tcp, or other.  Because of the use of anonymous pipes it works only with .NET 3.5 and later.  

 

Coordinator
Nov 9, 2010 at 4:56 PM

ps: if you don't like the idea of using anonymous pipes, or if you don't truly understand that code and you don't want to use code you don't understand, the alternative is, within the operation logic (GetFileZip above), to create a temporary zip file in the filesystem, then stream back the bytes from that file, and after completion, clean up the temporary file from the filesystem.  I don't like this idea as much because it involves extra IO, which is unnecessary.  

 

Nov 10, 2010 at 12:47 AM

cool - thanks for all this!

I'll give it a go now!

Nov 10, 2010 at 1:16 AM

Sorry for the probably stupid question, but where do I put the getFileZip and getPipedStream code? In the same service interface as defined above? And then how do I call this from my webservice?

Thanks for your help

Coordinator
Nov 10, 2010 at 2:15 AM

No.

In .NET, an interface doesn't get code.  It defines a collection of method signatures.

You need to implement the service interface in a concrete service class. The GetFileZip method goes in that class.  The GetPipedStream is just a utility method - you can put it in the service class or put it in a separate utility class if you like.

This stuff is just WCF 101.  You should read up a little on it before proceeding. 

Here's a good overview.  But there are lots and lots of similar articles describing the steps to get started building a webservice with WCF. If that article doesn't please you, find another one.

Good luck.