Using ASP.NET Web API, how can a controller return

2019-02-02 01:21发布

问题:

How can I create a Web API controller that generates and returns a compressed zip file streamed from a collection of in-memory JPEG files (MemoryStream objects). I'm attempting to use DotNetZip Library. I found this example: http://www.4guysfromrolla.com/articles/092910-1.aspx#postadlink. But the Response.OutputStream is not available in Web API and so that technique doesn't quite work. Therefore I tried saving the zip file to a new MemoryStream; but it threw. Lastly, I tried using PushStreamContent. Here's my code:

    public HttpResponseMessage Get(string imageIDsList) {
        var imageIDs = imageIDsList.Split(',').Select(_ => int.Parse(_));
        var any = _dataContext.DeepZoomImages.Select(_ => _.ImageID).Where(_ => imageIDs.Contains(_)).Any();
        if (!any) {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
        var dzImages = _dataContext.DeepZoomImages.Where(_ => imageIDs.Contains(_.ImageID));
        using (var zipFile = new ZipFile()) {
            foreach (var dzImage in dzImages) {
                var bitmap = GetFullSizeBitmap(dzImage);
                var memoryStream = new MemoryStream();
                bitmap.Save(memoryStream, ImageFormat.Jpeg);
                var fileName = string.Format("{0}.jpg", dzImage.ImageName);
                zipFile.AddEntry(fileName, memoryStream);
            }
            var response = new HttpResponseMessage(HttpStatusCode.OK);
            var memStream = new MemoryStream();
            zipFile.Save(memStream); //Null Reference Exception
            response.Content = new ByteArrayContent(memStream.ToArray());
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = string.Format("{0}_images.zip", dzImages.Count()) };
            return response;
        }
    }

zipFile.Save(memStream) throws null reference. But neither zipFile nor memStream are null and there is no internal exception. So I'm not sure what's causing the null reference. I've very little experience with Web API, memory streams, and I've never used DotNetZipLibrary before. This is a follow up to this question: Want an efficient ASP.NET Web API controller that can reliably return 30 to 50 ~3MB JPEGs

Any ideas? thanks!

回答1:

A more generic approach would work like this:

using Ionic.Zip; // from NUGET-Package "DotNetZip"

public HttpResponseMessage Zipped()
{
    using (var zipFile = new ZipFile())
    {
        // add all files you need from disk, database or memory
        // zipFile.AddEntry(...);

        return ZipContentResult(zipFile);
    }
}

protected HttpResponseMessage ZipContentResult(ZipFile zipFile)
{
    // inspired from http://stackoverflow.com/a/16171977/92756
    var pushStreamContent = new PushStreamContent((stream, content, context) =>
    {
        zipFile.Save(stream);
        stream.Close(); // After save we close the stream to signal that we are done writing.
    }, "application/zip");

    return new HttpResponseMessage(HttpStatusCode.OK) {Content = pushStreamContent};
}

The ZipContentResult method could also live in a base class and be used from any other action in any api controller.



回答2:

The PushStreamContent class can be used in this case to eliminate the need for the MemoryStream, at least for the whole zip file. It can be implemented like this:

    public HttpResponseMessage Get(string imageIDsList)
    {
        var imageIDs = imageIDsList.Split(',').Select(_ => int.Parse(_));
        var any = _dataContext.DeepZoomImages.Select(_ => _.ImageID).Where(_ => imageIDs.Contains(_)).Any();
        if (!any)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
        }
        var dzImages = _dataContext.DeepZoomImages.Where(_ => imageIDs.Contains(_.ImageID));
        var streamContent = new PushStreamContent((outputStream, httpContext, transportContent) =>
            {
                try
                {
                    using (var zipFile = new ZipFile())
                    {
                        foreach (var dzImage in dzImages)
                        {
                            var bitmap = GetFullSizeBitmap(dzImage);
                            var memoryStream = new MemoryStream();
                            bitmap.Save(memoryStream, ImageFormat.Jpeg);
                            memoryStream.Position = 0;
                            var fileName = string.Format("{0}.jpg", dzImage.ImageName);
                            zipFile.AddEntry(fileName, memoryStream);
                        }
                        zipFile.Save(outputStream); //Null Reference Exception
                    }
                }
                finally
                {
                    outputStream.Close();
                }
            });
        streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
        streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = string.Format("{0}_images.zip", dzImages.Count()),
        };

        var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = streamContent
            };

        return response;
    }

Ideally it would be possible to make this even more dynamically created using the ZipOutputStream class to dynamically create the zip instead of using ZipFile. In that case the MemoryStream for each bitmap would not be needed.



回答3:

public HttpResponseMessage GetItemsInZip(int id)
    {           
            var itemsToWrite = // return array of objects based on id;

            // create zip file stream
            MemoryStream archiveStream = new MemoryStream();
            using (ZipArchive archiveFile = new ZipArchive(archiveStream, ZipArchiveMode.Create, true))
            {
                foreach (var item in itemsToWrite)
                {
                    // create file streams
                    // add the stream to zip file

                    var entry = archiveFile.CreateEntry(item.FileName);
                    using (StreamWriter sw = new StreamWriter(entry.Open()))
                    {
                        sw.Write(item.Content);
                    }
                }
            }

            // return the zip file stream to http response content                
            HttpResponseMessage responseMsg = new HttpResponseMessage(HttpStatusCode.OK);                
            responseMsg.Content = new ByteArrayContent(archiveStream.ToArray());
            archiveStream.Dispose();
            responseMsg.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test.zip" };
            responseMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");

            return responseMsg;          
    }

Used Framework .NET 4.6.1 with MVC 5



回答4:

I just had the same problem as you.

zipFile.Save(outputStream); //I got Null Reference Exception here.

The problem was that I was adding files from a memory stream like so:

zip.AddEntry(fileName, ms);

All you have to do is change it to this:

zip.AddEntry(fileName, ms.ToArray());

It seems that when the writer decides to actually write the file and tries to read the stream, the stream is garbage collected or sth...

Cheers!