0

I'm reading a file from user upload and it was working synchronously. I needed to change it in order to immediately send a "received" alert to the user, then read the file asynchronously while the user would periodically poll back to see if the read was finished.

Here is what my code looks like right now:

public FileUpload SaveFile(Stream stream)
{
        FileUpload uploadObj = //instantiate the return obj

        var task = Task.Run(async () => await ProcessFileAsync(stream));
        
        return upload;
}

public async Task ProcessFileAsync(Stream stream)
{
        StreamReader file = new StreamReader(stream);
        CsvReader csv = new CsvReader(file, CultureInfo.InvariantCulture);
        
        while (await csv.ReadAsync())
        {
           //read the file
        }
}

the issue I'm having is that by the time I call the csv.ReadAsync() method, the Stream object has been disposed. How do I access the Stream when I want the SaveFile() method to return a value to the user, but the act of returning disposes the Stream object?

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
Brandon Miller
  • 327
  • 1
  • 4
  • 11
  • How do you get your stream? Is it web application? – Guru Stron Aug 13 '20 at 22:18
  • yes the user is uploading through a webapp, then sending it to my server API, then that API passes the stream to my method here. – Brandon Miller Aug 13 '20 at 22:20
  • target framework .NET Core 2.1 – Brandon Miller Aug 13 '20 at 22:24
  • I think you have to copy the stream to a MemoryStream to keep it in memory and hand it to a different thread. – CodeCaster Aug 13 '20 at 22:33
  • Why would a reference to a MemoryStream not simply be disposed as well? – Brandon Miller Aug 13 '20 at 22:37
  • Without a good [mcve] there's no way to provide a good answer to the question. That said, @CodeCaster is incorrect and IMHO providing inferior advice to say that copying the stream is a good option. The real issue here is that you apparently are mixing synchronous and asynchronous code, disposing a `Stream` object that you have no business disposing. How best to fix that is impossible to say because of the lack of a [mcve]. But the short version is, you need to push the async up the stack so that the dispose happens only after all the async stuff is done. – Peter Duniho Aug 13 '20 at 22:44
  • While not necessarily an exact duplicate of the question you've asked, the question [Asynchronous methods in using statement](https://stackoverflow.com/q/33722968), and the answer I posted there, are very closely related to what you seem to be trying to do. If you need an actual answer, please improve your question. – Peter Duniho Aug 13 '20 at 22:46
  • What more would you need in order to be able to give an answer? I need to synchronously return the FileUpload object while kicking off the ProcressFileAsync method to do its thing. When I attempt to do this, the Stream is disposed of. How do preserve the Stream/pass it by value instead of reference into my method? – Brandon Miller Aug 13 '20 at 22:51
  • @Peter they want to process an uploaded file in the background while immediately returning a response to the client. Starting a new task to do so has its _own_ problems, but in order for that to work, they need to have a stream of their own, not one the ASP.NET framework will dispose for them. Copying it into a MemoryStream is one solution for that, but indeed not a good one, because they'll still be using a Task. They better save the file and have a background task continuously listening for new files to process. – CodeCaster Aug 13 '20 at 22:51
  • 1
    HTTP is a request-response protocol. The client sends a request, the server reads it and returns a response. In that order. In the case of a standard HTTP file upload, the file itself IS the request; you can't send back a response until it is read. You can *simulate* this sort of thing through AJAX (see [this question and answer](https://stackoverflow.com/questions/166221/how-can-i-upload-files-asynchronously)) but it requires more than one request-response pair. You can't make that happen from the server side alone. – John Wu Aug 13 '20 at 23:28
  • Why can't I send a response until it's read? In this case, my method is *not* reading it, it is just sending it to a different method to be read and then after it passes that along it is sending a response. It seems to me that it still conforms to request-reponse pattern by letting a different method process it while I send a "request received" response to the user. – Brandon Miller Aug 13 '20 at 23:48
  • @JohnWu it is entirely [within spec](https://stackoverflow.com/questions/18367824/how-to-cancel-http-upload-from-data-events/18370751#18370751) to send a response while the request body is still being sent. – CodeCaster Aug 14 '20 at 07:08

2 Answers2

2

The point here is that you're working within the constraints of ASP.NET, which abstracts away a lot of the underlying HTTP stuff.

When you say you want to process a user-uploaded file asynchronously, you want to step out of the normal order of doing things with HTTP and ASP.NET. You see, when a client sends a request with a body (the file), the server receives the request headers and kicks off ASP.NET to tell your application code that there's a new request incoming.

It hasn't even (fully) read the request body at this point. This is why you get a Stream to deal with the request, and not a string or a filename - the data doesn't have to be arrived at the server yet! Just the request headers, informing the web server about the request.

If you return a response at that point, for all HTTP and ASP.NET care, you're done with the request, and you cannot continue reading its body.

Now what you want to do, is to read the request body (the file), and process that after sending a response to the client. You can do that, but then you'll still have to read the request body - because if you return something from your action method before reading the request, the framework will think you're done with it and dispose the request stream. That's what's causing your exception.

If you'd use a string, or model binding, or anything that involves the framework reading the request body, then yes, your code will only execute once the body has been read.

The short-term solution that would appear to get you going, is to read the request stream into a stream that you own, not the framework:

var myStream = new MemoryStream();
await stream.CopyTo(myStream);
Task.Run(async () => await ProcessFileAsync(myStream));

Now you'll have read the entire request body and saved it in memory, so ASP.NET can safely dispose the request stream and send a response to the client.

But don't do this. Starting fire-and-forget tasks from a controller is a bad idea. Keeping uploaded files in memory is a bad idea.

What you actually should do, if you still want to do this out-of-band:

  • Save the incoming file as an actual, temporary file on your server
  • Send a response to the client with an identifier (the temporarily generated filename, for example a GUID)
  • Expose an endpoint that clients can use to request the status using said GUID
  • Have a background process continuously scan the directory for newly uploaded files and process them

For the latter you could hosted services or third-party tools like Hangfire.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
0

You'll need to either do this if the environment warrants:

var result = task.Result;
//do stuff

...or

public Task<FileUpload> SaveFile(Stream stream)
{
    var uploadObj = //instantiate the return obj

    await ProcessFileAsync(stream);
    
    return uploadObj;
}

See here for a thorough discussion on fire-and-forget if you go that route: Web Api - Fire and Forget

Colin
  • 4,025
  • 21
  • 40
  • so in the second example, doesn't the await command just end up executing it all synchronously? If SaveFile() waits until ProcessFileAsync() is completed before returning the FileUpload object I may as well just process the file inside the SaveFile() method. The user needs to receive the FileUpload object before the file is ever read. – Brandon Miller Aug 13 '20 at 23:07
  • @BrandonMiller - No. Await operates asynchronously, but it does wait till the process is complete. Async simply means that it allows other parts of the program to run while the async code "awaits" the result of a long-running process. The problem you have is that the request spins down as soon as you return something by default, so the file operation is essentially cancelled the way you're doing it (hence why it's disposed). See here for a discussion on fire and forget in ASP.NET https://stackoverflow.com/questions/36335345/web-api-fire-and-forget – Colin Aug 14 '20 at 03:26
  • So I tried this way and without fail, every single time the program executes the ProcessFile method to completion before it returns the uploadObj to the user. This is because the SaveFileMethod is awaiting the result of the ProcessFile method before it returns the uploadObj, so I need to find a way to return the the uploadObj back to the user either while I am processing that FileStream or before. – Brandon Miller Aug 17 '20 at 15:57
  • @BrandonMiller - Right. I explained why that is in my comment and provided a link to “fire-and-forget” thread that discusses the approach to returning a value before the file is uploaded, but I updated my answer to include it as well. – Colin Aug 17 '20 at 19:27