5

Apparently, I'm not understanding how to use the ContinueWith method. My goal is to execute a task, and when complete, return a message.

Here's my code:

    public string UploadFile()
    {
        if (Request.Content.IsMimeMultipartContent())
        {
            //Save file
            MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
            Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

            string filename = "Not set";

            task.ContinueWith(o =>
            {
                //File name
                filename = provider.BodyPartFileNames.First().Value;
            }, TaskScheduler.FromCurrentSynchronizationContext()); 

            return filename;
        }
        else
        {
            return "Invalid.";
        }
    }

The variable "filename" always returns "Not set". It seems the code within the ContinueWith method is never called. (It does get called if I debug through it line by line in VS.)

This method is being called in my ASP.NET Web API controller / Ajax POST.

What am I doing wrong here?

Rivka
  • 2,172
  • 10
  • 45
  • 74

4 Answers4

7

If you're using an asynchronous operation, the best approach would be to make your operation asynchronous as well, otherwise you'll lose on the advantages of the async call you're making. Try rewriting your method as follows:

public Task<string> UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
        Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

        return task.ContinueWith<string>(contents =>
        {
            return provider.BodyPartFileNames.First().Value;
        }, TaskScheduler.FromCurrentSynchronizationContext()); 
    }
    else
    {
        // For returning non-async stuff, use a TaskCompletionSource to avoid thread switches
        TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
        tcs.SetResult("Invalid.");
        return tcs.Task;
    }
}
carlosfigueira
  • 85,035
  • 14
  • 131
  • 171
  • Wow, thank you! This worked for me. Thanks to everyone else too - appreciate all your help. – Rivka May 08 '12 at 22:16
  • I have a question about the TaskScheduler.FromCurrentSynchronizationContext - my understanding is that's really a UI method (synching with the UI thread), but I am wondering whether there are any reasons that might be used within a webapi method. Are there any? – AlexGad May 09 '12 at 05:05
  • The synchronization context can also be used to store some thread-local variables, and make sure they're repopulated when the control returns to the continuation. An example would be the `Thread.CurrentPrincipal`. If I remember correctly, the ASP.NET runtime defines a synchronization context for that case as well. – carlosfigueira May 09 '12 at 13:47
  • If you are using .Net 4.0 - see this thread: http://stackoverflow.com/questions/15201255/request-content-readasmultipartasync-never-returns – Daniel Leiszen Apr 18 '14 at 10:40
2

The reasons for your variable not being set are:

  • the tasks are instantiated, but not run.
  • even if the tasks ran, the function would probably return before they finished running so, it would still return "Not set". The fix for this is waiting for the final task (the one setting fileName) to finish.

Your code could be fixed like this:

public string UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));
        Task<IEnumerable<HttpContent>> task = Request.Content.ReadAsMultipartAsync(provider);

        string filename = "Not set";

        var finalTask = task.ContinueWith(o =>
            {
                //File name
                filename = provider.BodyPartFileNames.First().Value;
            }, TaskScheduler.FromCurrentSynchronizationContext()); 

        task.Start();

        finalTask.Wait();

        return filename;
    }
    else
    {
        return "Invalid.";
    }
}

The additions are the following:

  • assigned the return value of task.ContinueWith to a variable called finalTask. We need this task, because we'll wait for it to finish
  • started the task (the task.Start(); line)
  • waited for the final task to finish before returning (finalTask.Wait();)

If possible, please consider not implementing this asynchronously, because in the end it's synchronous (you're waiting for it to finish) and the current implementation adds complexity that could probably be avoided.

Consider doing something along these lines (if possible):

public string UploadFile()
{
    if (Request.Content.IsMimeMultipartContent())
    {
        //Save file
        MultipartFormDataStreamProvider provider = new MultipartFormDataStreamProvider(HttpContext.Current.Server.MapPath("~/Files"));

        Request.Content.ReadAsMultipart(provider); // don't know if this is really valid.

        return provider.BodyPartFileNames.First().Value;
    }
    else
    {
        return "Invalid.";
    }
}

Disclaimer: I have not actually executed the above code; I just wrote it to illustrate what should be done.

Cristian Lupascu
  • 39,078
  • 16
  • 100
  • 137
  • 2
    You should not be calling `Wait()` as it will result in blocking the thread. Instead this `UploadFile()` function itself should be asynchronous. – marcind May 08 '12 at 17:14
  • I tried your first suggestion, but got an error on the start method: Start may not be called on a task with null action. Regarding non-asynchronously - I'm not sure. I don't think there's such a method called ReadAsMultipart (AFAIK). – Rivka May 08 '12 at 17:38
  • @marcind You are right, it's an awkward construct. This is why I suggested the synchronous alternative, if possible. That's because we have two tasks executed sequentially and we need control back when they are finished - I think there's nothing asynchronous here. – Cristian Lupascu May 08 '12 at 19:29
  • @Rivka yes, I didn't say it works, I just suggested that you try to find a simpler alternative. Asynchronous code is not needed and only adds complexity in this case. – Cristian Lupascu May 08 '12 at 19:32
1

You should return the type Task<T> from the method, in this case it would be a Task<string>.

Scorpion-Prince
  • 3,574
  • 3
  • 18
  • 24
0

You are using an asynch operation. If you want to wait for its completion, you have to use the Wait method otherwise of your task:

task.ContinueWith(o =>
        {
            //File name
            filename = provider.BodyPartFileNames.First().Value;
        ).Wait();

return filename;

Edit: Some asynch methods start the task as soon as it is created, whereas other ask you to explicitly start them. You have to consult the documentation for each to be sure. In this case, it appears the task does start automatically.

Falanwe
  • 4,636
  • 22
  • 37
  • Tried this. It doesn't seem to return anything (not even "Not Set") using Wait(). – Rivka May 08 '12 at 17:58
  • Then it means the tasks are not run when they are created. I edited my answer accordingly. – Falanwe May 08 '12 at 19:07
  • Get the error Start may not be called on a task with null action. The status of task was "RanToCompletion". – Rivka May 08 '12 at 19:19
  • It means the task was already started (and completed...). The continuation should begin right away. If it does not, maybe the used scheduler cannot schedule the continuation. I would try removing the scheduler from the ContinueWith call. – Falanwe May 08 '12 at 19:27
  • Wait will block the thread in this case and hence the continuation will not execute. – Daniel Leiszen Apr 18 '14 at 09:57