And then there was Then

08 Sep 2012

In his excellent post Processing Sequences of Asynchronous Operations with Tasks, Stephen Toub discusses how a series of asynchronous operations can be run one after the other in a pre-.NET 4.5 world (a world in which I currently reside, both at work and at home). I won’t go into the details here - you should just read his post - but suffice to say that an implementation of a set of Then extension methods is desirable as a functional equivalent to the await keyword. This allows us to chain together asynchronous operations with ease and with better performance than that attainable with ContinueWith on its own:

Note that due to the use of optional arguments, there are more overload permutations here than apparent at first glance. Broadly, there are four supported scenarios:

A non-generic antecedent task THEN a non-generic successor task.

A non-generic antecedent task THEN a generic successor task.

A generic antecedent task THEN a non-generic successor task.

A generic antecedent task THEN a generic successor task.

Each scenario comes in two “flavours”. The first flavour requires that the caller provide a Func that returns the successor Task. The second flavour allows you to specify the task logic in an Action or Func, which will be automatically wrapped in a Task for you.

Four scenarios and two flavours means eight overloads, many of which include optional arguments. The result is a great deal of flexibility in how you use Then:

InitializeAsync().Then(x=>Console.WriteLine("Initialize step done."))// an action that is wrapped in a Task for us.Then(x=>DownloadDataAsync())// a method that returns a Task.Then(// a func that is wrapped in a Task for usx=>{Console.WriteLine("Download step done: "+x.Result);returnx.Result;}).Then(x=>// a func that returns the Task with which to continue{if(x.Result.Contains("MAGIC")){returnProcessMagicAsync();}else{returnProcessNonMagicAsync();}}).Then(x=>Console.WriteLine("Processing step done: "+x.Result));// another action

Right, on to the implementation then. To improve maintainability, I really wanted to ensure I had only a single implementation of the core Then logic, no matter the number of overloads I made available. This presented a problem in that the core implementation would need to be generic, but then the non-generic overloads would not be able to call it (because their Task instances are not generic). To that end, I created a simple method that takes a non-generic Task and wraps it as a Task<bool>:

If and when the non-generic Task succeeds, the wrapper Task<bool> assumes a result of true. If it is cancelled or fails, that cancellation or failure propagates to the wrapper Task<bool> too. So now any Task can be treated as a Task<bool>, thus allowing our non-generic overloads to call into our generic core implementation.