Note: I've substantially rewritten this question because the original premise was flawed.
What I'm ultimately trying to achieve is to stream hundreds of thousands of records out of a database as a downloadable CSV file without spanking the server's memory, but I also want the browser to show the download of indeterminate size starting immediately and growing as more records are retrieved.
I've established that Kestrel is flushing content as I write it out (regardless of how I compose the output). I know this because when I request the file with curl I see it streaming into stdout one part at a time. However when I request the file in a browser (Chrome or Firefox) it doesn't show a downloading file until the last record has been emitted, which makes for a bad user experience because the download URL just appears to be broken. The following simple controller action demonstrates the problem.
What do I need to do to this code to make Chrome immediately pop up the download bar and show the file as downloading as soon as I've emitted the headers?
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase {
[HttpGet("[action]")]
public async Task Test() {
Response.StatusCode = 200;
Response.ContentType = "text/plain";
Response.ContentLength = null;
Response.StatusCode = StatusCodes.Status200OK;
Response.Headers.Add(HeaderNames.ContentDisposition, new ContentDispositionHeaderValue("attachment") { FileName = "test.txt" }.ToString());
await Response.StartAsync();
for (var i = 0; i < 5; ++i) {
await Response.WriteAsync($"Iteration {i} at {DateTime.Now}\r\n");
await Response.Body.FlushAsync();
await Task.Delay(2000);
}
}
}
Solved it. I changed the content type to application/octet-stream and now it behaves as I wanted it to.
Full working solution:
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase {
[HttpGet("[action]")]
public async Task Test() {
Response.StatusCode = 200;
Response.ContentType = "application/octet-stream";
Response.ContentLength = null;
Response.StatusCode = StatusCodes.Status200OK;
Response.Headers.Add(HeaderNames.ContentDisposition, new ContentDispositionHeaderValue("attachment") { FileName = "test.txt" }.ToString());
await Response.StartAsync();
for (var i = 0; i < 5; ++i) {
await Response.WriteAsync($"Iteration {i} at {DateTime.Now}\r\n");
await Response.Body.FlushAsync();
await Task.Delay(2000);
}
}
}