This is a truly asynchronous method:
public Task<string> ProcessRequest()
{
var textFile = File.OpenText("file.txt");
var readTask = textFile.ReadToEndAsync();
readTask.ContinueWith(previousTask => textFile.Dispose());
return readTask;
}
If you run this method with a large file or a file on a slow drive the execution will return to caller long before file reading ends. In Stephen Cleary's example the caller will get back control only when the result ("foo") is finished calculating.
Dispose must be in ContinueWith because the method execution will return to caller before file reading is complete so file can't be closed in ProcessRequest method.
One can of course start their own task.
public Task<string> ProcessRequest(CancellationToken cancellationToken)
{
var readTask = Task.Run(() =>
{
using (var textFile = File.OpenText("file.txt"))
{
var text = textFile.ReadToEnd();
cancellationToken.ThrowIfCancellationRequested();
var processedText = text.Replace("foo", "bar");
return processedText;
}
});
return readTask;
}
It is a good practice to have a CancellationToken and periodically check if cancellation was requested to allow long running operarions to be cancelled.
Edit 1
As @Stephen Cleary highlighted the first sample and this result in approximately or maybe exactly the same CIL:
public async Task<string> ProcessRequest()
{
using (var textFile = File.OpenText("file.txt"))
{
var s = await textFile.ReadToEndAsync();
return s;
}
}
Basically the compiler will transform the code following await textFile.ReadToEndAsync() into ContinueWith.
Each syntax has its benefits, my preference is that 1-2 lines (i.e. dispose and log) go into ContinueWith, more complex continuation uses await.