In my last post, I described the async method modifier and the await operator in .NET that make it simple to write asynchronous code and promised to deliver an example showing how it works. This is that demonstration.
A good way to think about using async and await is to think of a chain of phone calls for notification of some event, like a weather cancellation for an activity for youth sports. Ok, I know that we live in a world where this would be handled in a batch with email, SMS, and/or social media instead of a phone tree, but back in the dark ages (when I was a child), a coach or other team authority would notify a set of parents, who would notify another set, and down a chain. This may not be something we’d realistically face in today’s world, but it is an instructive thought exercise to illustrate asynchronous execution.
Let’s say that we have a youth sports team and because of severe weather, a game scheduled for this evening needs to be canceled. Let’s construct a responsibility graph that involves chains of one person responsible for notifying another person until everyone has gotten the message. Let’s say we have a coach for the team named Abigail. Abigail is responsible for starting a notification chain and verification that notifications have been received.
Abigail needs to phone Ben, Bridgette, and Brock, to let them know the game is cancelled. Ben needs to call Clyde, Cody, and Corrine. Corrine calls Dan, Dennis, and Dori.
Let us now think about what happens when Abigail makes a call (assuming the happy path):
- She first makes a connection with Ben.
- She informs him that there will not be a game tonight.
- She passes a list of the persons Ben should call.
- She instructs Ben to call her back upon completion of his task.
Upon receipt of the message itself and instructions for further dissemination, Ben and Abigail sever their connection. Ben proceeds on making his prescribed calls, while Abigail continues with the other persons for which she is responsible for communication. She does this by calling Bridgette and Brock in series. Upon hanging up with Brock, she then awaits getting calls back from the three persons she has notified. Upon receipt of all three returned calls, she proceeds to do something else.
Ben does something similar. After hanging up with Abigail, he get Clyde on the phone and delivers the information about the cancellation of the sporting event and passes him a list of names to notify. Upon completion of this communication, they hang up. Clyde makes his calls while Ben calls Cody and Corrine. Upon completion of these calls, he awaits getting calls back.
The following code is a start of a sample implementation of this (admittedly contrived) example. The source repository with the history of how I arrived at this code is available on GitHub.
We start with a TestFixture calling the CancelPractice method on a Coach object and asserting that notification happens on each of the parents that are assigned as those that should be directly notified by the coach.
using System;
using System.Threading.Tasks;
using NUnit.Framework;
using Should;
namespace AsyncAwaitExample
{
[TestFixture]
public class WhenCoachCancelsPractice
{
private Coach _coach;
private Parent _level1Parent0;
private Parent _level1Parent1;
private Parent _level1Parent2;
private Parent _level2Parent0;
private Parent _level2Parent1;
private Parent _level2Parent2;
[SetUp]
public void Setup()
{
_level1Parent1 = new Parent("parent1");
_level1Parent2 = new Parent("parent2");
_level2Parent0 = new Parent("parent3");
_level2Parent1 = new Parent("parent4");
_level2Parent2 = new Parent("parent5");
_level1Parent0 = new Parent("parent0", _level2Parent0, _level2Parent1, _level2Parent2);
_coach = new Coach(_level1Parent0, _level1Parent1, _level1Parent2);
}
private async Task ExecuteCancelPractice()
{
await _coach.CancelPractice();
Console.WriteLine("Coach has finished");
}
[Test]
public void ShouldNotCallLevel1Parent0WithoutCancellation()
{
AssertParentNotNotified(_level1Parent0);
}
[Test]
public void ShouldNotCallLevel1Parent1WithoutCancellation()
{
AssertParentNotNotified(_level1Parent1);
}
[Test]
public void ShouldNotCallLevel1Parent2WithoutCancellation()
{
AssertParentNotNotified(_level1Parent2);
}
[Test]
public void ShouldNotCallLevel2Parent0WithoutCancellation()
{
AssertParentNotNotified(_level2Parent0);
}
[Test]
public void ShouldNotCallLevel2Parent1WithoutCancellation()
{
AssertParentNotNotified(_level2Parent1);
}
[Test]
public void ShouldNotCallLevel2Parent2WithoutCancellation()
{
AssertParentNotNotified(_level2Parent2);
}
[Test]
public async Task ShouldCallLevel1Parent0()
{
await ExecuteCancelPractice();
AssertParentNotified(_level1Parent0);
}
[Test]
public async Task ShouldCallLevel1Parent1()
{
await ExecuteCancelPractice();
AssertParentNotified(_level1Parent1);
}
[Test]
public async Task ShouldCallLevel1Parent2()
{
await ExecuteCancelPractice();
AssertParentNotified(_level1Parent2);
}
[Test]
public async Task ShouldCallLevel2Parent0()
{
await ExecuteCancelPractice();
AssertParentNotified(_level2Parent0);
}
[Test]
public async Task ShouldCallLevel2Parent1()
{
await ExecuteCancelPractice();
AssertParentNotified(_level2Parent1);
}
[Test]
public async Task ShouldCallLevel2Parent2()
{
await ExecuteCancelPractice();
AssertParentNotified(_level2Parent2);
}
private void AssertParentNotNotified(Parent parent)
{
parent.Notified.ShouldBeFalse();
}
private void AssertParentNotified(Parent parent)
{
parent.Notified.ShouldBeTrue();
}
}
}
(Please note that the coach is level 0, so please do not comment that calling the first level of parents level 1 makes me something less than a proper nerd.)
This sets up a hierarchy where the parent name “parent0” is responsible for notification of three other parents. The tests assert that the proper notifications are being made and the console output shows the sequence. I could have written more tests to try to verify the proper sequence, but that would just be more code than necessary to demonstrate the point and I think this adequately communicates what we are trying to accomplish here. I could also refactor to get rid of the duplication of the Notify method in both the Coach and Parent Classes, but, again, this has gone far enough to demonstrate the use of async and await without any further coding.
And the implementation classes:
using System;
using System.Linq;
using System.Threading.Tasks;
namespace AsyncAwaitExample
{
public class Coach
{
private readonly Parent[] _rootNotificationParents;
public Coach(params Parent[] rootNotificationParents)
{
_rootNotificationParents = rootNotificationParents;
}
public async Task CancelPractice()
{
Console.WriteLine("Starting to Call parents");
var parentAwaitables = _rootNotificationParents.Select(parent => parent.Notify()).ToArray();
Console.WriteLine("finished outgoing calls, waiting for calls back");
await Task.WhenAll(parentAwaitables);
Console.WriteLine("Done notifying parents");
}
}
}
using System;
using System.Linq;
using System.Threading.Tasks;
namespace AsyncAwaitExample
{
public class Parent
{
private readonly string _name;
private readonly Parent[] _otherParentsForNotification;
public bool Notified { get; set; }
public Parent(string name, params Parent[] otherParentsForNotification)
{
_name = name;
_otherParentsForNotification = otherParentsForNotification;
}
public async virtual Task Notify()
{
Console.WriteLine("{0} receiving notification", _name);
Notified = true;
// the phone conversation will take a finite time, so modeling that with a delay:
await Task.Delay(1000);
await NotifyOtherParents();
Console.WriteLine("{0} done with notification, calling back notifier", _name);
}
private async Task NotifyOtherParents()
{
Console.WriteLine("{0} Starting to Call other parents", _name);
var parentAwaitables = _otherParentsForNotification.Select(parent => parent.Notify()).ToArray();
Console.WriteLine("{0} finished outgoing calls, waiting for calls back", _name);
await Task.WhenAll(parentAwaitables);
Console.WriteLine("{0} Done notifying parents", _name);
}
}
}
The resulting console output is this:
Starting to Call parents
parent0 receiving notification
parent1 receiving notification
parent2 receiving notification
finished outgoing calls, waiting for calls back
parent2 Starting to Call other parents
parent2 finished outgoing calls, waiting for calls back
parent2 Done notifying parents
parent0 Starting to Call other parents
parent3 receiving notification
parent1 Starting to Call other parents
parent1 finished outgoing calls, waiting for calls back
parent1 Done notifying parents
parent1 done with notification, calling back notifier
parent4 receiving notification
parent5 receiving notification
parent2 done with notification, calling back notifier
parent0 finished outgoing calls, waiting for calls back
parent4 Starting to Call other parents
parent4 finished outgoing calls, waiting for calls back
parent4 Done notifying parents
parent4 done with notification, calling back notifier
parent5 Starting to Call other parents
parent5 finished outgoing calls, waiting for calls back
parent5 Done notifying parents
parent5 done with notification, calling back notifier
parent3 Starting to Call other parents
parent3 finished outgoing calls, waiting for calls back
parent3 Done notifying parents
parent3 done with notification, calling back notifier
parent0 Done notifying parents
parent0 done with notification, calling back notifier
Done notifying parents
Coach has finished