Awaitable Pattern

How do you determine what types could be awaited ? That is one question that often comes to mind and the most common answer would be

  • Task
  • Task<TResult>
  • void – Though it should be strictly avoided

However, are we truely restricted to them ? What are the other Types that could be awaited ? The anwer lies in the awaitable pattern.

Awaitable Pattern

The awaitable pattern requires to have a parameterless instance or static non-void method GetAwaiter that returns an Awaitable Type.

public T GetAwaiter()

Where T, the awaiter Type implements
* INotifyCompletion or ICriticallyNotifyCompletion
* Has a boolean instance property IsCompleted
* Non-generic parameterless instance method GetResult

Approach 01 – Use TaskAwaiter

Let’s begin by an example that reusing Task or Task<TResult> awaiter instead of creating our own awaiter. For demonstration purpose, we assume a requirement where in we should be able to use the Process Class to execute given command asynchronously and return the result. Ideally, we should be able to do the following.

var result = await "dir"

The above command should be able to execute “dir” command using Process and return the result. We will begin by writing the GetAwaiter extension method for string.

public static class CommandExtension
{
public static TaskAwaiter GetAwaiter(this string command)
{
var tcs = new TaskCompletionSource();
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/C {command}";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.EnableRaisingEvents = true;
process.Exited += (s, e) => tcs.TrySetResult(process.StandardOutput.ReadToEnd());
process.Start();
return tcs.Task.GetAwaiter();
}
}

The above code reuses the Awaiter for Task<TResult>. The method initiates the Process and use TaskCompletionSource to set the result in the Exited event of Process. If you examine the source code of TaskAwaiter , you can observe that it implements the ICriticallyNotifyCompletion interface and has the IsCompleted Property as well as GetResult method.

Let us now write some demonstrative code.

private async void btnDemoUsingTaskAwaiter_Click(object sender, EventArgs e)
{
AppendToLog($"Started Method {nameof(btnDemoUsingTaskAwaiter_Click)}");
await InvokeAsyncCall();
AppendToLog($"Continuing Method {nameof(btnDemoUsingTaskAwaiter_Click)}");
}
private async Task InvokeAsyncCall()
{
AppendToLog($"Starting Method {nameof(InvokeAsyncCall)}");
var result = await "dir";
AppendToLog($"Recieved Result, Continuing Method {nameof(InvokeAsyncCall)}");
AppendToLog(result);
AppendToLog($"Ending Method {nameof(InvokeAsyncCall)}");
}
public void AppendToLog(string message)
{
logText.Text += $"{Environment.NewLine}{message}";
}

Approach 02 – Implement Custom Awaiter

Let us now assume another situation where-in, the method is invoked in a non-UI thread, and the continuation requires you to update Controls in UI (in other words, needs UI Thread). For purpose of learning, let us find a solution for the problem by implementing a Custom Awaiter.

We will begin by defining our Custom Awaiter that satisfies the laws defined in the Awaitable Pattern section above.

public static class CommandExtension
{
public static UIThreadAwaiter GetAwaiter(this string command)
{
var tcs = new TaskCompletionSource();
Task.Run(() =>
{
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = $"/C {command}";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.EnableRaisingEvents = true;
process.Exited += (s, e) => tcs.TrySetResult(process.StandardOutput.ReadToEnd());

process.Start();
});

return new UIThreadAwaiter(tcs.Task.GetAwaiter().GetResult());
}
}
public class UIThreadAwaiter : INotifyCompletion
{
bool isCompleted = false;
string resultFromProcess;

public UIThreadAwaiter(string result)
{
resultFromProcess = result;
}
public bool IsCompleted => isCompleted;
public void OnCompleted(Action continuation)
{
if (Application.OpenForms[0].InvokeRequired)
Application.OpenForms[0].BeginInvoke((Delegate)continuation);

}

public string GetResult()
{
return resultFromProcess;
}
}

The UIThreadAwaiter implements the INotifyCompletion interface. As one could asssume from the code above, the ability to use UI Thread for continuation tasks are executed with the help of BeginInvoke.

Let us now write some demo code to demonstrate the custom awaiter.

private void btnExecuteOnDifferentThread_Click(object sender, EventArgs e)
{
AppendToLog($"Started Method {nameof(btnExecuteOnDifferentThread_Click)}");
Task.Run(() => InvokeAsyncCall()).ConfigureAwait(false);
AppendToLog($"Continuing Method {nameof(btnExecuteOnDifferentThread_Click)}");
}

private async Task InvokeAsyncCall()
{
var result = await "dir";
AppendToLog($"Recieved Result, Continuing Method {nameof(InvokeAsyncCall)}");
AppendToLog(result);
AppendToLog($"Ending Method {nameof(InvokeAsyncCall)}");
}
public void AppendToLog(string message)
{
try
{
txtLog.Text += $"{Environment.NewLine}{message}";
}
catch (Exception ex)
{
var errorMessage = $"Exception:{ex.Message}{Environment.NewLine}{Environment.NewLine}Message:{message}";
MessageBox.Show(errorMessage, "Error");
}
}

private async void btnExecuteOnSameThread_Click(object sender, EventArgs e)
{
AppendToLog($"Started Method {nameof(btnExecuteOnDifferentThread_Click)}");
await InvokeAsyncCall();
AppendToLog($"Continuing Method {nameof(btnExecuteOnDifferentThread_Click)}");
}

As demonstrated in examples above, the await keyword is not restricted to few types. Instead, we could create our Custom Awaiter which statisfies the Awaitable Pattern.

Complete code for this post is available on my Github