I have always tried to explain state machines and how they work in C# when we are using await-async. Now, to make things easy for you, I ‘ll create a custom awaitable to showcase how you can create your own asynchronous operation like the async and await does.

How Await/Async Translates into State Machines

Before we go to the examples, let me explain how the compiler works in the background. When you mark a method with the async keyword and use await within it, the C# compiler does these:

  1. State Machine Generation: The compiler transforms your asynchronous method into a state machine. This state machine keeps track of where to resume execution after an await completes.
  2. Awaitable Pattern: The compiler leverages the awaitable pattern, which relies on types implementing certain methods and interfaces (GetAwaiter(), INotifyCompletion, etc.).
  3. Continuation Passing: Instead of blocking the thread, the method’s execution is paused, and a continuation is registered to resume execution once the awaited task completes.

What Are Awaitables?

In C#, the await keyword doesn’t just work with tasks. It can be used with any type that follows a specific pattern known as the awaitable pattern. This pattern requires the type to have a method called GetAwaiter() that returns an awaiter. The awaiter, in turn, must implement the INotifyCompletion interface and have IsCompleted and GetResult() methods.

Creating custom awaitables allows you to define how the await keyword interacts with your types. This can be useful when you want to provide asynchronous behavior without necessarily using Task or Task<T>. For example, you might have a custom synchronization context, or you’re interfacing with an API that doesn’t use tasks (I feel sorry for those using WCF still, I am a victim as well).

Building a Custom Awaitable

Let’s build a simple custom awaitable to see how it works. This is an example of a watcher that waits for a file to be created in a specific path.

public class FileCreatedAwaiter : INotifyCompletion
{
    private readonly string _filePath;
    private Action _continuation;
    private FileSystemWatcher _watcher;

    public FileCreatedAwaiter(string filePath)
    {
        _filePath = filePath;
        IsCompleted = File.Exists(_filePath);

        if (!IsCompleted)
        {
            _watcher = new FileSystemWatcher(Path.GetDirectoryName(_filePath))
            {
                Filter = Path.GetFileName(_filePath),
                EnableRaisingEvents = true
            };
            _watcher.Created += OnFileCreated;
        }
    }

    public bool IsCompleted { get; private set; }

    public FileInfo GetResult()
    {
        _watcher?.Dispose();

        return new FileInfo(_filePath);
    }

    public void OnCompleted(Action continuation)
    {
        if (IsCompleted)
        {
            continuation();
        }
        else
        {
            _continuation = continuation;
        }
    }

    private void OnFileCreated(object sender, FileSystemEventArgs e)
    {
        IsCompleted = true;
        _watcher.Dispose();
        _continuation?.Invoke();
    }
}

public class FileCreatedAwaitable
{
    private readonly string _filePath;

    public FileCreatedAwaitable(string filePath)
    {
        _filePath = filePath;
    }

    public FileCreatedAwaiter GetAwaiter()
    {
        return new FileCreatedAwaiter(_filePath);
    }
}

To use our file watcher we can do this:

string filePath = @"C:\Temp\example.txt";
Console.WriteLine("Waiting for the file to be created...");

FileInfo createdFile = await new FileCreatedAwaitable(filePath);

Console.WriteLine("File has been created!");
Console.WriteLine($"File Name: {createdFile.Name}");
Console.WriteLine($"File Size: {createdFile.Length} bytes");
Console.WriteLine($"Created On: {createdFile.CreationTime}");

Now let me explain the example. When you instantiate FileCreatedAwaitable, it stores the file path you’re interested in. The FileCreatedAwaiter checks if the file already exists. If it does, IsCompleted is set to true, and the await will continue without delay. If the file doesn’t exist, it sets up a FileSystemWatcher to monitor the directory for the file’s creation. The OnCompleted method is called by the compiler when the await is encountered. If the file is not yet created, it stores the continuation (the rest of the code after the await) to be called later. When the file is created, the OnFileCreated event handler is invoked, which sets IsCompleted to true and calls the stored continuation. The GetResult() method disposes of the FileSystemWatcher to free up resources.

Conclusion

Creating custom awaitables in C# allows you to extend the await keyword’s functionality to your own types. This can be help if you are integrating with non-task-based APIs.

Affiliate promo

If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!

Leave a comment

Trending