37

I am using NPOI to convert DataTable to Excel in a ASP.NET Web API project.

But the I got nothing from the response. Here's my code:

public HttpResponseMessage GetExcelFromDataTable(DataTable dt)
{
    IWorkbook workbook = new XSSFWorkbook(); // create *.xlsx file, use HSSFWorkbook() for creating *.xls file.
    ISheet sheet1 = workbook.CreateSheet();
    IRow row1 = sheet1.CreateRow(0);
    for (int i = 0; dt.Columns.Count > i; i++)
    {
        row1.CreateCell(i).SetCellValue(dt.Columns[i].ColumnName);
    }

    for (int i = 0; dt.Rows.Count > i; i++)
    {
        IRow row = sheet1.CreateRow(i + 1);
        for (int j = 0; dt.Columns.Count > j; j++)
        {
            row.CreateCell(j).SetCellValue(dt.Rows[i][j].ToString());
        }
    }

    MemoryStream ms = new MemoryStream();
    workbook.Write(ms);
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    result.Content = new StreamContent(ms);
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
    result.Content.Headers.ContentDisposition.FileName = string.Format("{0}.xlsx", dt.TableName);
    return result;
}

I set a break point to inspect the ms.Length after workbook.Write(ms), but it return a exception : System.ObjectDisposedException.

Where did I go wrong?

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
fankt
  • 1,007
  • 1
  • 7
  • 16
  • 15
    The xlsx variant of NPOI does that while the other one does not. It's just a wierdness of the library which kind of sucks. You can work around it by doing ms.ToArray() and feeding that into a new MemoryStream but it's kind of sad and wasteful. – alun Apr 08 '14 at 08:31
  • 1
    @alun Thank you, it works! But it's really wasteful like you said...I will mark it as a work around, and see if it can be slove in the future. Thank you again. – fankt Apr 09 '14 at 01:24
  • @alun Thank you. I was using stream.getbuffer() which generated the xlsx but gave a "...corrupt data.." message in MS Excel. Changing the stream.getbuffer to ms.ToArray() fixed the issue. – maddog Aug 15 '14 at 20:03
  • In my case Response.BinaryWrite(ms.ToArray()) and Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" fixed the issue. see example http://goo.gl/m3xPa7 – Brij Nov 05 '15 at 14:53
  • Try to set: result.Content = new ByteArrayContent(ms.GetBuffer()); – Alex Nguyen Mar 29 '16 at 09:25

4 Answers4

46

Update 1/3/2020: As Florian Dendorfer pointed out, there is an override added in October 2018 to prevent the stream from closing. Please try the overload first before using this workaround (and upvote Florian's answer!)

Leaving original answer for historical purposes.


Another workaround to this issue...which doesn't use multiple MemoryStream objects.

Create a NpoiMemoryStream class that inherits MemoryStream, and overrides the Close method:

public class NpoiMemoryStream : MemoryStream
{
    public NpoiMemoryStream()
    {
        // We always want to close streams by default to
        // force the developer to make the conscious decision
        // to disable it.  Then, they're more apt to remember
        // to re-enable it.  The last thing you want is to
        // enable memory leaks by default.  ;-)
        AllowClose = true;
    }

    public bool AllowClose { get; set; }

    public override void Close()
    {
        if (AllowClose)
            base.Close();
    }
}

Then, use that stream like this:

var ms = new NpoiMemoryStream();
ms.AllowClose = false;
workbook.Write(ms);
ms.Flush();
ms.Seek(0, SeekOrigin.Begin);
ms.AllowClose = true;

At some point between the flush and seek, NPOI will attempt to close the stream, but since we overrode Close() and the AllowClose flag is false, we can keep the stream open. Then, set AllowClose back to true so normal disposal mechanisms can close it.

Don't get me wrong...this is still a hack that shouldn't need to be implemented...but it's a bit cleaner from a memory usage standpoint.

Joe the Coder
  • 1,775
  • 1
  • 19
  • 22
  • What is the purpose to use `ms.Flush()`? MSDN says [here](https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream.flush?view=net-5.0) that this "Overrides the `Stream.Flush()` method so that no action is performed." Why at all "NPOI will attempt to close the stream". Isn't it reasonably to leave it opened since we just wrote some data and want to use it? – alanextar Apr 28 '21 at 08:16
  • 1
    This is disgustingly beautiful. Thanks so much for this hack. Don't remove this answer since the overload doesn't exist on word docs, only on worksheets. – Gaspa79 Jun 25 '23 at 14:16
14

I don't know if this is still needed, but there's an overload

Write(Stream stream, bool leaveOpen)

where, if you set leaveOpen = true, leaves your MemoryStream open

Skandix
  • 1,916
  • 6
  • 27
  • 36
  • This is the sanest answer to this question! – Peter Dec 17 '19 at 00:42
  • 7
    Only available for XSSFWorkbook. When using interfaces this aproach won't be available – gangfish Dec 15 '20 at 11:07
  • 1
    Somehow I don't see such overload of Write. Not even in XSSFWorkbook. – papadi Feb 26 '21 at 05:07
  • @papadi, as user 'gangfish' mentioned, you need to look into XSSFWorkBook to find this overload. I too did not find it initially, but later I noticed I was looking in 'POIXMLDocument' class instead of XSSFWorkBook – Yash Nov 16 '21 at 22:56
7

As alun stated above, and also in this question you can feed the stream into another MemoryStream:

...
MemoryStream ms = new MemoryStream();
using(MemoryStream tempStream = new MemoryStream)
{
    workbook.Write(tempStream);
    var byteArray = tempStream.ToArray();
    ms.Write(byteArray, 0, byteArray.Length);
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    result.Content = new StreamContent(ms);
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
    result.Content.Headers.ContentDisposition.FileName = string.Format("{0}.xlsx", dt.TableName);
    return result;
}

There is a little code smell from having to do this. However, this is only necessary when outputting .xlsx files due to the way the 3rd party libraries involved handle the stream.

Community
  • 1
  • 1
Josh Stella
  • 117
  • 2
  • 6
4

I've encountered similar issues with APIs that close/dispose streams that they don't own. I'm not familiar with NPOI, but I assume that the Write method is accepting Stream, not MemoryStream. If that is the case, you can create a wrapper Stream class that forwards all calls (read/write/seek, etc) to the inner stream (your MemoryStream in this case), but doesn't forward calls to close/dispose. Pass the wrapper to the Write method, when it returns your MemoryStream should contain all the content and still be "open".

Additionally, you'll probably need to ms.Seek(0, SeekOrigin.Begin). After the call to Write your memory stream will be positioned at the end of the stream, so if you try to read from that position it will appear emtpy.

MarkPflug
  • 28,292
  • 8
  • 46
  • 54
  • 1
    +1, this works well, is the most memory-friendly, and presents a reusable class when the situation arises again. You can override and forward all the stream methods (except `Close` and `Dispose` - leave those as empty NO-OPs) to the "wrapped" stream with just a few minutes of coding. I called mine `NoCloseStream` so that I knew exactly what it was doing. – Bruce Pierson Sep 27 '16 at 02:36
  • 1
    For people directly using NPOI, probably best route is `Write` override if it exists, but I came here because `ExcelMapper`, based on NPOI, has the same problem and no override. This answer helped me solve issue with `ExcelMapper`. – Milan Oct 23 '21 at 00:32