MemoryStream кажется закрытым после NPOI workbook.write?

Я использую NPOI для преобразования DataTable в Excel в проекте веб-API ASP.NET.

Но я ничего не получил от ответа. Вот мой код:

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;
}

Я установил точку останова для проверки ms.Length после workbook.Write(ms), но он возвращает исключение: System.ObjectDisposedException.

Где я неправ?


person fankt    schedule 08.04.2014    source источник
comment
Вариант xlsx NPOI делает это, а другой - нет. Это просто странность библиотеки, которая отстой. Вы можете обойти это, выполнив ms.ToArray() и передав его в новый MemoryStream, но это довольно грустно и расточительно.   -  person alun    schedule 08.04.2014
comment
@alun Спасибо, все работает! Но это действительно расточительно, как вы сказали ... Я отмечу это как обходной путь и посмотрю, можно ли будет это сделать в будущем. Еще раз спасибо.   -  person fankt    schedule 09.04.2014
comment
@alun Спасибо. Я использовал stream.getbuffer(), который сгенерировал xlsx, но выдал сообщение ... Corrupt data .. в MS Excel. Изменение stream.getbuffer на ms.ToArray() устранило проблему.   -  person maddog    schedule 16.08.2014
comment
В моем случае Response.BinaryWrite(ms.ToArray()) и Response.ContentType = application/vnd.openxmlformats-officedocument.spreadsheetml.sheet исправили проблему. см. пример goo.gl/m3xPa7   -  person Brij    schedule 05.11.2015
comment
Попробуйте установить: result.Content = new ByteArrayContent(ms.GetBuffer());   -  person Alex Nguyen    schedule 29.03.2016


Ответы (4)


Обновление от 03.01.2020: Как указал Флориан Дендорфер, существует override, добавленный в октябре 2018 г. для предотвращения закрытия потока. Пожалуйста, сначала попробуйте перегрузку, прежде чем использовать этот обходной путь (и проголосуйте за ответ Флориана!)

Оставив оригинальный ответ для исторических целей.


Другой обходной путь к этой проблеме... который не использует несколько объектов MemoryStream.

Создайте класс NpoiMemoryStream, который наследует MemoryStream и переопределяет метод Close:

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();
    }
}

Затем используйте этот поток следующим образом:

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

В какой-то момент между сбросом и поиском NPOI попытается закрыть поток, но, поскольку мы переопределили Close(), а флаг AllowClose равен false, мы можем оставить поток открытым. Затем верните AllowClose значение true, чтобы обычные механизмы утилизации могли его закрыть.

Не поймите меня неправильно... это все еще хак, который не нужно реализовывать... но он немного чище с точки зрения использования памяти.

person Joe the Coder    schedule 23.05.2016
comment
Какова цель использования ms.Flush()? MSDN говорит здесь что это переопределяет метод Stream.Flush(), поэтому никаких действий не выполняется. Почему вообще NPOI попытается закрыть поток. Разве не разумно оставить его открытым, поскольку мы только что записали некоторые данные и хотим их использовать? - person alanextar; 28.04.2021

Не знаю, нужно ли это еще, но есть overload

Write(Stream stream, bool leaveOpen)

где, если вы установите leaveOpen = true, ваш MemoryStream останется открытым

person Florian Dendorfer    schedule 27.03.2019
comment
Это самый разумный ответ на этот вопрос! - person Peter; 17.12.2019
comment
Доступно только для XSSFWorkbook. При использовании интерфейсов этот подход будет недоступен - person gangfish; 15.12.2020
comment
Что-то я не вижу такой перегруженности Write. Даже в XSSFWorkbook. - person papadi; 26.02.2021

Как указано выше alun, а также в этот вопрос, который вы можете передать потоку в другой 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;
}

От необходимости делать это немного пахнет кодом. Однако это необходимо только при выводе файлов .xlsx из-за того, как сторонние библиотеки обрабатывают поток.

person Josh Stella    schedule 12.04.2016
comment
Я должен сказать, что мне это решение нравится больше, чем переопределение класса, поскольку оно чище. - person Daniel Mošmondor; 14.01.2017
comment
Это решение не является более чистым. В случае большого файла он будет дублировать все его содержимое в памяти. - person papadi; 26.02.2021

Я сталкивался с похожими проблемами с API, которые закрывают/удаляют потоки, которыми они не владеют. Я не знаком с NPOI, но предполагаю, что метод Write принимает Stream, а не MemoryStream. Если это так, вы можете создать класс-оболочку Stream, который перенаправляет все вызовы (чтение/запись/поиск и т. д.) во внутренний поток (в данном случае ваш MemoryStream), но не перенаправляет вызовы на закрытие/удаление. Передайте оболочку методу Write, когда он вернет, ваш MemoryStream должен содержать все содержимое и все еще быть «открытым».

Кроме того, вам, вероятно, потребуется ms.Seek(0, SeekOrigin.Begin). После вызова Write ваш поток памяти будет расположен в конце потока, поэтому, если вы попытаетесь прочитать из этой позиции, он окажется пустым.

person MarkPflug    schedule 12.04.2016
comment
+1, это работает хорошо, является наиболее удобным для памяти и представляет класс для повторного использования, когда ситуация возникает снова. Вы можете переопределить и перенаправить все методы потока (кроме Close и Dispose — оставьте их пустыми NO-OP) в обернутый поток всего за несколько минут программирования. Я назвал свой NoCloseStream, чтобы точно знать, что он делает. - person Bruce Pierson; 27.09.2016