So I built a service that collects information, say, log information, and sends it down to a client. This information is normally streamed right to a web browser via SignalR, but sometimes there is a lot of it, and scrolling through it on the web view can be quite a pain. It would be nice to be able to download the data as a tab-delimited file or something like that, so I can just open it with my favorite text editor and use the search functionality. The service doesn’t persist the data forever, either, so if I want to save the information to look at later, I have to save it somehow.
Since I’m using an Angular SPA, where all server access is via WebAPI. The information doesn’t exist on disk, its all in memory, so you can’t necessarily just open a FileStream and return that.
MemoryStream
To make a stream from stuff you have in memory, you use a MemoryStream. Basically, its just an array of bytes. You can write bytes directly to it with the Write method, or you can use a StreamWriter to make it easier to write basic types:
// MemoryStreams need to be disposed, so put them in a using block | |
using (MemoryStream ms = new MemoryStream()) | |
{ | |
// A StreamWriter makes it easier to write basic types to a stream | |
StreamWriter sw = new StreamWriter(ms); | |
sw.WriteLine("Here I am, writing a stream"); | |
} |
So you can easily put your data in to a stream, but then you need to return it in the right way from the WebAPI.
FileStreamResult
To return a file stream, you can use a FileStreamResult. This lets you specify the stream, the MIME type, and some other options (like the file name).
The Controller has a shortcut for this – you can just return File:
[HttpGet("Download/{id}")] | |
public async Task<IActionResult> DownloadFile(string id) | |
{ | |
// Get the data | |
var data = await _myService.GetData(id); | |
// Note that we are NOT using a "using" block here. This is intentional as | |
// we don't want the stream to be closed prematurely. The ASP.NET will close | |
// the stream when the transfer is complete. | |
MemoryStream ms = new MemoryStream(); | |
StreamWriter sw = new StreamWriter(sw); | |
foreach (var dataPoint in data.Points) | |
{ | |
// Write the data | |
await sw.WriteLineAsync($"{dataPoint.TimeStamp}\t{dataPoint.Value}"); | |
} | |
// Rewinds the stream to the beginning | |
// Since we were writing to the stream, the position in the stream was | |
// advanced all the way to the end. We need to Seek to the beginning of | |
// the stream so that the FileResultStream is in the appropriate position | |
// when it is handed off to the browser. | |
ms.Seek(0, SeekOrigin.Begin); | |
return File(ms, MediaTypeNames.Text.Plain, $"{id}.txt"); | |
} |
There’s a couple of “gotchas,” which is why I’m writing this blog post. First, note the lack of a using block around the MemoryStream. This is because the using block will dispose/close the stream, which might be premature (before the file is finished being downloaded). ASP.NET will handle getting rid of this stream for you once the transfer is complete (part of the FileStreamResult), so you don’t need to worry about cleaning it up.
Second, note the Seek method being invoked on the MemoryStream to “rewind” it to the beginning. When you’re writing to a stream, you’re advancing the stream. So, when you’re done writing, you need to effectively rewind the stream to make sure the FileStreamResult is at the beginning to begin the download.
Callback MemoryStream – Darchuk.NET
[…] the last post I talked about streaming data back via a WebAPI. On the front end, I had a “download” […]
ASP.NET CORE WEBAPI文件下载 - CodingNote.cc
[…] https://darchuk.net/2019/05/31/asp-net-core-web-api-returning-a-filestream/ […]
ASP.NET CORE WEBAPI文件下载 – Python量化投资
[…] https://darchuk.net/2019/05/31/asp-net-core-web-api-returning-a-filestream/ […]