I want to add a button that will download a dynamically generated CSV file.
I think I need to use FileStreamResult
(or possibly FileContentResult
) but I have been unable to find an example that shows how to do this.
I've seen examples that create a physical file, and then download that. But my ideal solution would write directly to the response stream, which would be far more efficient than creating a file or first building the string in memory.
Has anyone seen an example of dynamically generating a file for download in Razor Pages (not MVC)?
So here's what I came up with.
Markup:
<a class="btn btn-success" asp-page-handler="DownloadCsv">
Download CSV
</a>
Handler:
public IActionResult OnGetDownloadCsv()
{
using MemoryStream memoryStream = new MemoryStream();
using CsvWriter writer = new CsvWriter(memoryStream);
// Write to memoryStream using SoftCircuits.CsvParser
writer.Flush(); // This is important!
FileContentResult result = new FileContentResult(memoryStream.GetBuffer(), "text/csv")
{
FileDownloadName = "Filename.csv""
};
return result;
}
This code works but I wish it used memory more efficiently. As is, it writes the entire file contents to memory, and then copies that memory to the result. So a large file would exist twice in memory before anything is written to the response stream. I was curious about FileStreamResult
but wasn't able to get that working.
If someone can improve on this, I'd gladly mark your answer as the accepted one.
UPDATE:
So I realized I can adapt the code above to use FileStreamResult
by replacing the last block with this:
memoryStream.Seek(0, SeekOrigin.Begin);
FileStreamResult result = new FileStreamResult(memoryStream, "text/csv")
{
FileDownloadName = "Filename.csv"
};
return result;
This works almost the same except that, instead of calling memoryStream.GetBuffer()
to copy all the bytes, it just passes the memory stream object. This is an improvement as I am not needlessly copying the bytes.
However, the downside is that I have to remove my two using
statements or else I'll get an exception:
ObjectDisposedException: Cannot access a closed Stream.
Looks like it's a trade off between copying the bytes an extra time or not cleaning up my streams and CSV writer.
In the end, I'm able to prevent the CSV writer from closing the stream when it's disposed, and since MemoryStream
does not have unmanaged resources there should be no harm in leaving it open.